Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calculate centers #461

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
163 changes: 158 additions & 5 deletions imsim/stamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ def setup(self, config, base, xsize, ysize, ignore, logger):

req = {}
opt = {'camera': str, 'diffraction_fft': dict,
'airmass': float, 'rawSeeing': float, 'band': str}
'airmass': float, 'rawSeeing': float, 'band': str,
'centroid': dict}
if self.vignetting:
req['det_name'] = str
else:
Expand Down Expand Up @@ -247,6 +248,9 @@ def setup(self, config, base, xsize, ysize, ignore, logger):
else:
world_pos = None

if 'centroid' in params and 'n_photons' not in params['centroid']:
raise ValueError('n_photons not found in centroid config')

return xsize, ysize, image_pos, world_pos

def _getGoodPhotImageSize(self, obj_list, keep_sb_level, pixel_scale):
Expand Down Expand Up @@ -758,9 +762,10 @@ def draw(self, prof, image, method, offset, config, base, logger):
else:
bp_for_drawImage = bandpass

# Put the psfs at the start of the photon_ops.
# Probably a little better to put them a bit later than the start in some cases
# (e.g. after TimeSampler, PupilAnnulusSampler), but leave that as a todo for now.
# Put the psfs at the start of the photon_ops. Probably a little
# better to put them a bit later than the start in some cases (e.g.
# after TimeSampler, PupilAnnulusSampler), but leave that as a todo
# for now.
photon_ops = psfs + photon_ops

if faint:
Expand All @@ -783,10 +788,158 @@ def draw(self, prof, image, method, offset, config, base, logger):
poisson_flux=False)
base['realized_flux'] = image.added_flux

if 'centroid' in config:
xvals, yvals = _get_photon_positions(
gal=gal,
rng=self.rng,
bp_for_drawImage=bp_for_drawImage,
image=image,
sensor=sensor,
photon_ops=photon_ops,
offset=offset,
config=config['centroid'],
)
cenres = _get_robust_centroids(xvals, yvals)

# these can be saved in the config using @xcentroid, @ycentroid
base['xcentroid'] = cenres['x']
base['xcentroid_err'] = cenres['xerr']
base['ycentroid'] = cenres['y']
base['ycentroid_err'] = cenres['yerr']

return image


def _get_photon_positions(
gal,
rng,
bp_for_drawImage,
image,

sensor,
photon_ops,
offset,
config,
):
"""
draw another image with fixed number of photons
and use to get centroid. We can't do this above
because with maxN the photons cannot be saved

100_000 photons should give centroid to a few miliarcsec
"""

timage = image.copy()
gal.drawImage(
bp_for_drawImage,
image=timage,

method='phot',
n_photons=config['n_photons'],
sensor=sensor,
photon_ops=photon_ops,
poisson_flux=False,
save_photons=True,

offset=offset,
rng=rng,
)

photx = timage.photons.x
photy = timage.photons.y

flux = timage.photons.flux
wvig, = np.where(flux == 0)

logic = np.isnan(photx) | np.isnan(photy)
wnan, = np.where(logic)

# flux == 0 means it was vignetted
wgood, = np.where(
np.isfinite(photx)
& np.isfinite(photy)
& (timage.photons.flux > 0)
)

# location of true center, actually in big image
imcen = image.true_center
xvals = imcen.x + photx[wgood]
yvals = imcen.y + photy[wgood]

return xvals, yvals


def _get_robust_centroids(xvals, yvals):
xcen, _, xerr = sigma_clip(xvals)
ycen, _, yerr = sigma_clip(yvals)

return {
'x': xcen,
'xerr': xerr,
'y': ycen,
'yerr': yerr,
}


def sigma_clip(arrin, niter=4, nsig=4):
"""
Calculate the mean, sigma, error of an array with sigma clipping.

parameters
----------
arr: array or sequence
A numpy array or sequence
niter: int, optional
number of iterations, defaults to 4
nsig: float, optional
number of sigma, defaults to 4

returns
-------
mean, stdev, err
"""

arr = np.array(arrin, ndmin=1, copy=False)

if len(arr.shape) > 1:
raise ValueError(
'only 1-dimensional arrays suppored, got {arr.shape}'
)

indices = np.arange(arr.size)
nold = arr.size

mn, sig, err = _get_sigma_clip_stats(arr)

for i in range(1, niter + 1):

w, = np.where((np.abs(arr[indices] - mn)) < nsig * sig)

if w.size == 0:
# everything clipped, nothing to do but report latest
# statistics
break

if w.size == nold:
break

indices = indices[w]
nold = w.size

mn, sig, err = _get_sigma_clip_stats(arr[indices])

return mn, sig, err


def _get_sigma_clip_stats(arr):
mn = arr.mean()
sig = arr.std()
err = sig / np.sqrt(arr.size)
return mn, sig, err


# Pick the right function to be _fix_seds.
if galsim.__version_info__ < (2,5):
if galsim.__version_info__ < (2, 5):
LSST_SiliconBuilder._fix_seds = LSST_SiliconBuilder._fix_seds_24
else:
LSST_SiliconBuilder._fix_seds = LSST_SiliconBuilder._fix_seds_25
Expand Down
44 changes: 44 additions & 0 deletions tests/test_stamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,51 @@ def new_toplevel_only(self, object_types):
np.testing.assert_allclose(realized_X, predict, rtol=rtol)


def test_lsst_silicon_builder_calculates_centroid() -> None:
"""
LSST_SiliconBuilder.draw passes the correct list of photon_ops to
prof.drawImage.

This fails because no flux is actually put in the image, but I'm
not sure why
"""
lsst_silicon = create_test_lsst_silicon(faint=False)
image = galsim.Image(ncol=256, nrow=256)
offset = galsim.PositionD(0,0)
config = create_test_config()
config['stamp']['centroid'] = {
'n_photons': 1000,
}

prof = galsim.Gaussian(sigma=2) * galsim.SED('vega.txt', 'nm', 'flambda')
method = 'phot'
logger = galsim.config.LoggerWrapper(None)

if galsim.__version_info__ < (2,5):
phot_type = 'galsim.ChromaticTransformation'
else:
# In GalSim 2.5+, this is now a SimpleChromaticTransformation
phot_type = 'galsim.SimpleChromaticTransformation'

with mock.patch(phot_type+'.drawImage', return_value=image) as mock_drawImage:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use mock. Have this call the real drawImage function, and then it will produce an actual output image. The above tests that use mock were testing something very specific about how parameters were being passed to drawImage, which mock lets you test. I think you want to actually call the drawImage function and check that the centroid values get computed correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I didn't know what that context did, but I found I had to use it because otherwise I got this error:

imsim/stamp.py:778: in draw
    image = gal.drawImage(bp_for_drawImage,
../../miniforge3/envs/stack/lib/python3.11/site-packages/galsim/chromatic.py:2136: in drawImage
    image = ChromaticObject.drawImage(self, bandpass, image, integrator, **kwargs)
../../miniforge3/envs/stack/lib/python3.11/site-packages/galsim/chromatic.py:485: in drawImage
    image = prof0.drawImage(image=image, **kwargs)
../../miniforge3/envs/stack/lib/python3.11/site-packages/galsim/gsobject.py:1775: in drawImage
    added_photons, photons = prof.drawPhot(imview, gain, add_to_image,
../../miniforge3/envs/stack/lib/python3.11/site-packages/galsim/gsobject.py:2383: in drawPhot
    op.applyTo(photons, local_wcs, rng)
imsim/photon_ops.py:90: in applyTo
    v = self.photon_velocity(photon_array, local_wcs, rng)
imsim/photon_ops.py:189: in photon_velocity
    return self.rubin_diffraction.photon_velocity(
imsim/photon_ops.py:267: in photon_velocity
    v = photon_velocity(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

photon_array = galsim.PhotonArray(100, x=array([-4.95703380547105, -1.7774137125821585, 5.4679574148088355, 1.3853752380310527, -11.0...28, 567.8319745320754, 610.7805936794955, 588.7526624330898, 631.0916929335601, 659.
8606846139876, 662.0577956513312]))
xy_to_v = <imsim.photon_ops.XyToV object at 0x7165d0d3b350>, get_n = <bound method Medium.getN of ConstMedium(1.0)>

    def photon_velocity(photon_array, xy_to_v: "XyToV", get_n) -> np.ndarray:
        """Computes the velocity of a photon array."""
    
>       assert photon_array.hasAllocatedPupil()
E       AssertionError

imsim/photon_ops.py:123: AssertionError

Copy link
Contributor

@rmjarvis rmjarvis Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to either use an atmosphere (as PSF or photon_op) or PupilAnnulusSampler. The RubinDiffractionOptics photon_op requires that something else has already populated the pupil plane positions. In normal imsim usage, it's the AtmosphericPSF. But you can also do it manually with PupilAnnulusSampler (or PupilImageSampler works too, but you probably don't want to use that one.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also had to add a TimeSampler

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that sounds right.

image.added_flux = lsst_silicon.phot_flux
lsst_silicon.draw(
prof,
image,
method,
offset,
config=config["stamp"],
base=config,
logger=logger,
)
for c in ('x', 'y'):
for n in ('', '_err'):
name = f'{c}centroid{n}'
assert name in base


if __name__ == "__main__":
# test_lsst_silicon_builder_calculates_centroid()
testfns = [v for k, v in vars().items() if k[:5] == 'test_' and callable(v)]
for testfn in testfns:
testfn()
Loading