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

Refactor image/stamp workflow #424

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
62e8a7b
Refactor image/stamp workflow
g-braeunlich Sep 27, 2023
85cb394
Add more doc for the photon pooling code
g-braeunlich Dec 12, 2023
4920810
Log photon objects and fix faint object flux
welucas2 Feb 8, 2024
e386d09
Fix test_image.py photon object list
welucas2 Feb 8, 2024
84115b2
Saner batch sizes and photon pooling checkpoints to separate dataset
welucas2 Mar 14, 2024
8ead410
Move photon pooling functions into photon_pooling.py and add docstrings.
welucas2 Mar 21, 2024
b93f3d1
Automatically add Shift photon operator for LSST_Image runs.
welucas2 Apr 1, 2024
d789e81
Determine each photon object's sub-pixel offset and the adjustment to…
welucas2 Apr 17, 2024
d474e34
Use copyFrom() rather than assignAt() to merge photon arrays.
welucas2 Apr 17, 2024
17de2ad
Ensure Shift photon op is used automatically with RubinOptics.
welucas2 Apr 19, 2024
ba11bcb
Fix rebase and separate tests for LSST_Image and LSST_PhotonPoolingIm…
welucas2 May 24, 2024
0413770
Correct use of nbatch_per_checkpoint for LSST_Image.
welucas2 May 24, 2024
d481c01
Re-add Shift operator manually and correct photon shooting decision.
welucas2 May 24, 2024
b786cb0
Correct auto-insertion of Shift photon operator and remove build_gal …
welucas2 May 24, 2024
0948e3c
Merge branch 'main' into u/g-braeunlich/refactor-image
rmjarvis Aug 14, 2024
0a2ab42
Fix typo in imsim-config.yaml and remove nbatch_photons.
welucas2 Aug 20, 2024
b26b77b
Rename PhotonStampBuilder to LSST_PhotonsBuilder and have it inherit …
welucas2 Aug 21, 2024
cc1686e
build_obj() gets the stamp builder from valid_stamp_types rather than…
welucas2 Sep 6, 2024
c751dbe
Photon pooling with LSST_PhotonPoolingImage now has build_stamps() us…
welucas2 Sep 18, 2024
98f1024
StellarObject becomes ObjectCache. object, PSF, offset and fft_flux a…
welucas2 Oct 15, 2024
587fc48
Create new config template for use in photon pooling runs.
welucas2 Oct 15, 2024
a45300b
LSST_PhotonPoolingImage checks during setup that the LSST_Photons sta…
welucas2 Oct 15, 2024
c5c553a
Remove Shift photon op. Applied within RubinOptics for original pipel…
welucas2 Aug 21, 2024
d897327
Move LSST_PhotonPoolingImageBuilder into photon_pooling.py and make t…
welucas2 Oct 25, 2024
9f1e73b
Use simple seq of DeltaFunctions in test_image config, rather than ne…
welucas2 Nov 19, 2024
430240b
Rename StellarObject in comments and stellar_obj variable
welucas2 Nov 19, 2024
c3b4737
make_batches adds remainder objects to early batches starting at the …
welucas2 Nov 19, 2024
268e1f7
Add partitioning and batching tests for photon pooling.
welucas2 Nov 20, 2024
eeb67dc
Make create_full_image and set_config_image_pos private members of LS…
welucas2 Nov 20, 2024
d1fe74d
PhotonPooling creates a local WCS with full_image.true_center rather …
welucas2 Nov 20, 2024
8f0d002
Batch photon objects with lower flux than nbatch as though they were …
welucas2 Nov 22, 2024
6bc019f
Add test for partitioning of low flux photon objects and strengthen o…
welucas2 Nov 22, 2024
98bc06c
Update LSST_ImageBuilder.buildImage docstring.
welucas2 Dec 5, 2024
575fc60
Fix docstrings, rename offset_adjustment to offset, check stamp.type …
welucas2 Dec 5, 2024
ddefdec
Rename ObjectCache to ObjectInfo.
welucas2 Dec 5, 2024
ed242c3
Simplify __main__in test_image.py.
welucas2 Dec 5, 2024
73197a0
Return empty photon array with image in LSST_Photons if drawing nothing.
welucas2 Dec 5, 2024
b439a92
Offset photons using just stamp_center when drawing the stamps.
welucas2 Dec 5, 2024
a6232c6
Nicer way of batching photon objects and conserving total flux.
welucas2 Dec 5, 2024
c672108
Use BandpassRatio as a photon op (if correct to do so) within LSST_Ph…
welucas2 Dec 5, 2024
1aeedd3
Remove unused local_wcs from XyToV and the various photon_velocity me…
welucas2 Dec 5, 2024
387c4e0
Have RubinOptics apply shifts using stamp_center rather than image_pos.
welucas2 Dec 6, 2024
fafaead
Update most photon op tests to resemble updated self-consistent tests…
welucas2 Dec 10, 2024
bf6cb43
Allow RubinDiffraction to shift photons and fix tests to use this opt…
welucas2 Dec 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions config/imsim-config-photon-pooling.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This imSim config file performs almost the same function as the base
# imsim-config.yaml and can be used as a template for other input configs.
# The only difference is that it overrides the image and stamp types in order
# to use the photon pooling pipeline.

modules:
- imsim
template: imsim-config

# Photon pooling requires the following two changes to the base
# imsim-config.yaml, which otherwise performs the rest of the
# configuration.

# LSST_PhotonPoolingImages firstly determine how all objects are to be drawn,
# then batch the FFT and photon objects separately. FFT objects are drawn
# one-by-one first. Each photon batch contains all of the brighter photon
# objects, at 1/nbatch their total flux. Faint objects are drawn in one go after
# being randomly inserted into the photon batches at full flux. Once the photons
# from all the objects in a batch have been generated by the LSST_Photons stamp,
# the image pools and draws them.

# LSST_Photons is a special stamp type that must be used with
# LSST_PhotonPoolingImage. FFT objects are drawn as normal. Photon shooting
# objects are however drawn with a special NullSensor which does nothing,
# leaving photon stamps to be returned with the photons only.

image.type: LSST_PhotonPoolingImage
stamp.type: LSST_Photons
15 changes: 13 additions & 2 deletions config/imsim-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,25 @@ image:
# a small numer of photons.

# The objects are processed in batches.

# If using LSST_Image (non-photon pooling), nbatch is the overal number of batches in
# which to draw the objects.
# The default number of batches is 100, but you can change it if desired.
nbatch: 100

# If using photon pooling, FFT and photon objects are batched separately.
# Bright photon objects are treated across nbatch batches, but at 1/nbatch of
# their flux. Faint photon objects are placed at their full flux in random batches.
# There will probably be few enough FFT objects to do in a single batch, but this
# can be controlled in photon pooling images with nbatch_fft if desired.
nbatch_fft: 1

# If checkpointing is turned on, then the checkpoint file will be updated after every
# group of nbatch_per_checkpoint batches. This factor helps lower the overall checkpoint
# rate when running thousands of processes concurrently; otherwise, the contention
# from lots of concurrent checkpoint writes can overwhelm the file system.
# Even if not checkpointing, using batches helps keep the memory down, since a bunch of
# temporary data can be purged after each batch.
# The default number of batches is 100, but you can change it if desired.
nbatch: 100
nbatch_per_checkpoint: 1

det_name: $det_name
Expand Down
1 change: 1 addition & 0 deletions imsim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@
from .sag import *
from .process_info import *
from .table_row import *
from .photon_pooling import *
305 changes: 178 additions & 127 deletions imsim/lsst_image.py

Large diffs are not rendered by default.

129 changes: 74 additions & 55 deletions imsim/photon_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import numpy as np

import batoid
from galsim import Bandpass, PhotonArray, PhotonOp, GaussianDeviate
from galsim import Bandpass, PhotonArray, PhotonOp, GaussianDeviate, PositionD
from galsim.config import RegisterPhotonOpType, PhotonOpBuilder, GetAllParams
from galsim.config import BuildBandpass, GalSimConfigError
from galsim.celestial import CelestialCoord
Expand Down Expand Up @@ -32,11 +32,12 @@ class RubinOptics(PhotonOp):
The ICRF coordinate of light that reaches the boresight. Note that this
is distinct from the spherical coordinates of the boresight with respect
to the ICRF axes.
sky_pos : galsim.CelestialCoord
image_pos : galsim.PositionD
img_wcs : galsim.BaseWCS
stamp_center : galsim.PositionD
icrf_to_field : galsim.GSFitsWCS
det_name : str
camera : lsst.afw.cameraGeom.Camera
shift_photons : Optional, whether to shift photons at start. [default: False]
"""

_req_params = {
Expand All @@ -45,29 +46,35 @@ class RubinOptics(PhotonOp):
"det_name": str,
}

_opt_params = {
"shift_photons": bool,
}

def __init__(
self,
telescope,
boresight,
sky_pos,
image_pos,
img_wcs,
stamp_center,
icrf_to_field,
det_name,
camera
camera,
shift_photons=False
):
self.telescope = telescope
self.detector = camera[det_name]
self.boresight = boresight
self.sky_pos = sky_pos
self.image_pos = image_pos
self.img_wcs = img_wcs
self.stamp_center = stamp_center
self.icrf_to_field = icrf_to_field
self.shift_photons = shift_photons

def photon_velocity(self, photon_array, local_wcs, rng) -> np.ndarray:
def photon_velocity(self, photon_array, rng) -> np.ndarray:
"""Computes the velocity of the photons directly."""

return photon_velocity(
photon_array,
XyToV(local_wcs, self.icrf_to_field, self.sky_pos),
XyToV(self.icrf_to_field, self.img_wcs),
self.telescope.inMedium.getN,
)

Expand All @@ -87,7 +94,14 @@ def applyTo(self, photon_array, local_wcs=None, rng=None):
rng: A random number generator to use if needed. [default: None]
"""

v = self.photon_velocity(photon_array, local_wcs, rng)
# If a stamp_center has been provided apply it as a shift to the photons. This will
# probably only ever be when using LSST_Image, as LSST_PhotonPoolingImage
# positions the photons before they are pooled to be processed together.
if self.shift_photons and self.stamp_center is not None:
photon_array.x += self.stamp_center.x
photon_array.y += self.stamp_center.y

v = self.photon_velocity(photon_array, rng)

x = photon_array.pupil_u.copy()
y = photon_array.pupil_v.copy()
Expand All @@ -108,8 +122,9 @@ def applyTo(self, photon_array, local_wcs=None, rng=None):
)
traced = self.telescope.trace(ray_vec)
ray_vector_to_photon_array(traced, detector=self.detector, out=photon_array)
photon_array.x -= self.image_pos.x
photon_array.y -= self.image_pos.y
if self.stamp_center is not None:
photon_array.x -= self.stamp_center.x
photon_array.y -= self.stamp_center.y

def __str__(self):
return f"imsim.{type(self).__name__}()"
Expand Down Expand Up @@ -146,12 +161,13 @@ class RubinDiffractionOptics(RubinOptics):
The ICRF coordinate of light that reaches the boresight. Note that this
is distinct from the spherical coordinates of the boresight with respect
to the ICRF axes.
sky_pos : galsim.CelestialCoord
image_pos : galsim.PositionD
img_wcs : galsim.BaseWCS
stamp_center : galsim.PositionD
icrf_to_field : galsim.GSFitsWCS
det_name : str
camera : lsst.afw.cameraGeom.Camera
rubin_diffraction : Instance of a RubinDiffraction photon operator
shift_photons : Optional, whether to shift photons at start. [default: False]
"""

_req_params = {
Expand All @@ -166,29 +182,29 @@ class RubinDiffractionOptics(RubinOptics):
"azimuth": Angle,
"latitude": Angle,
"disable_field_rotation": bool,
"shift_photons": bool,
}

def __init__(
self,
telescope,
boresight,
sky_pos,
image_pos,
icrf_to_field,
stamp_center,
det_name,
camera,
rubin_diffraction: "RubinDiffraction",
shift_photons=False,
):
super().__init__(
telescope, boresight, sky_pos, image_pos, icrf_to_field, det_name, camera
telescope, boresight, rubin_diffraction.img_wcs, stamp_center, rubin_diffraction.icrf_to_field, det_name, camera, shift_photons
)
self.rubin_diffraction = rubin_diffraction

def photon_velocity(self, photon_array, local_wcs, rng) -> np.ndarray:
def photon_velocity(self, photon_array, rng) -> np.ndarray:
"""Computes the velocity of the photons after applying diffraction."""

return self.rubin_diffraction.photon_velocity(
photon_array, local_wcs=local_wcs, rng=rng
photon_array, rng=rng
)


Expand All @@ -205,26 +221,32 @@ class RubinDiffraction(PhotonOp):
for the spider diffraction.
altitude, azimuth : float
alt/az coordinates the telescope is pointing to (in rad).
sky_pos : galsim.CelestialCoord
img_wcs : galsim.BaseWCS
icrf_to_field : galsim.GSFitsWCS
stamp_center : Optional PositionD. [default: None]
shift_photons : Optional bool, whether to shift photons at start. [default: False]
"""

_req_params = {"altitude": Angle, "azimuth": Angle, "latitude": Angle}
_opt_params = {"disable_field_rotation": bool}
_opt_params = {"disable_field_rotation": bool, "stamp_center": PositionD, "shift_photons": bool}

def __init__(
self,
telescope,
latitude,
altitude,
azimuth,
sky_pos,
img_wcs,
icrf_to_field,
disable_field_rotation: bool = False,
stamp_center=None,
shift_photons=False,
):
self.telescope = telescope
self.sky_pos = sky_pos
self.img_wcs = img_wcs
self.icrf_to_field = icrf_to_field
self.stamp_center = stamp_center
self.shift_photons = shift_photons
if disable_field_rotation:
self.apply_diffraction_delta = lambda pos, v, _t, wavelength, geometry, distribution: apply_diffraction_delta(
pos, v, wavelength, geometry, distribution
Expand All @@ -249,7 +271,7 @@ def _rng(phi):

return _rng

def photon_velocity(self, photon_array, local_wcs, rng) -> np.ndarray:
def photon_velocity(self, photon_array, rng) -> np.ndarray:
"""Computes the velocity of the photons after applying diffraction.

This will not modify the photon array and only return the velocity.
Expand All @@ -258,13 +280,11 @@ def photon_velocity(self, photon_array, local_wcs, rng) -> np.ndarray:
Parameters
----------
photon_array: A `PhotonArray` to apply the operator to.
local_wcs: A `LocalWCS` instance defining the local WCS for the current photon
bundle in case the operator needs this information. [default: None]
rng: A random number generator to use if needed. [default: None]

Returns: ndarray of shape (n, 3), where n = photon_array.pupil_u.size()
Returns: ndarray of shape (n, 3), where n = photon_array.pupil_u.size()
"""
xy_to_v = XyToV(local_wcs, self.icrf_to_field, self.sky_pos)
xy_to_v = XyToV(self.icrf_to_field, self.img_wcs)
v = photon_velocity(
photon_array,
xy_to_v,
Expand Down Expand Up @@ -297,9 +317,16 @@ def applyTo(self, photon_array, local_wcs=None, rng=None):
rng: A random number generator to use if needed. [default: None]
"""

# If a stamp_center has been provided apply it as a shift to the photons. This will
# probably only ever be when using LSST_Image, as LSST_PhotonPoolingImage
# positions the photons before they are pooled to be processed together.
if self.shift_photons and self.stamp_center is not None:
photon_array.x += self.stamp_center.x
photon_array.y += self.stamp_center.y

assert photon_array.hasAllocatedPupil()
assert photon_array.hasAllocatedTimes()
xy_to_v = XyToV(local_wcs, self.icrf_to_field, self.sky_pos)
xy_to_v = XyToV(self.icrf_to_field, self.img_wcs)
# Convert xy coordinates to a cartesian 3d velocity vector of the photons
v = xy_to_v(photon_array.x, photon_array.y)

Expand All @@ -319,6 +346,11 @@ def applyTo(self, photon_array, local_wcs=None, rng=None):
)
photon_array.x, photon_array.y = xy_to_v.inverse(v)

# Shift photons back if they were shifted at the start.
if self.shift_photons and self.stamp_center is not None:
photon_array.x -= self.stamp_center.x
photon_array.y -= self.stamp_center.y

def __str__(self):
return f"imsim.{type(self).__name__}()"

Expand Down Expand Up @@ -362,7 +394,7 @@ def config_kwargs(config, base, cls, base_args=()):
return kwargs


_rubin_optics_base_args = ("sky_pos", "image_pos")
_rubin_optics_base_args = ("stamp_center",)


@photon_op_type("RubinOptics", input_type="telescope")
Expand All @@ -373,6 +405,7 @@ def deserialize_rubin_optics(config, base, _logger):
return RubinOptics(
telescope=telescope,
icrf_to_field=base["_icrf_to_field"],
img_wcs=base["current_image"].wcs,
camera=get_camera_cached(kwargs.pop("camera")),
**kwargs,
)
Expand All @@ -387,14 +420,13 @@ def deserialize_rubin_diffraction_optics(config, base, _logger):
latitude=kwargs.pop("latitude", RUBIN_LOC.lat.rad),
altitude=kwargs.pop("altitude"),
azimuth=kwargs.pop("azimuth"),
sky_pos=base["sky_pos"],
img_wcs=base["current_image"].wcs,
icrf_to_field=base["_icrf_to_field"],
disable_field_rotation=kwargs.pop("disable_field_rotation", False),
)

return RubinDiffractionOptics(
telescope=telescope,
icrf_to_field=base["_icrf_to_field"],
camera=get_camera_cached(kwargs.pop("camera")),
rubin_diffraction=rubin_diffraction,
**kwargs,
Expand All @@ -408,12 +440,13 @@ def get_camera_cached(camera_name: str):

@photon_op_type("RubinDiffraction", input_type="telescope")
def deserialize_rubin_diffraction(config, base, _logger):
kwargs = config_kwargs(config, base, RubinDiffraction, base_args=("sky_pos",))
kwargs = config_kwargs(config, base, RubinDiffraction)
telescope = base["det_telescope"]

return RubinDiffraction(
telescope=telescope,
icrf_to_field=base["_icrf_to_field"],
img_wcs=base["current_image"].wcs,
**kwargs,
)

Expand All @@ -423,25 +456,19 @@ class XyToV:
vector of the photons before entering the telescope.

The transform is composed of the chain
(x,y) -> (u,v) -> (ra,dec) -> (thx, thy) -> v.
(x,y) -> (ra,dec) -> (thx, thy) -> v.

The transform takes 2 vectors of shape (n,) and returns
a column major vector of shape (n,3).
"""

def __init__(self, local_wcs, icrf_to_field, sky_pos):
self.local_wcs = local_wcs
def __init__(self, icrf_to_field, img_wcs):
self.icrf_to_field = icrf_to_field
self.sky_pos = sky_pos
self.img_wcs = img_wcs

def __call__(self, x: np.ndarray, y: np.ndarray) -> np.ndarray:
# x/y to u/v
u = self.local_wcs._u(x, y)
v = self.local_wcs._v(x, y)
# u/v to ICRF
u_rad = np.deg2rad(u / 3600)
v_rad = np.deg2rad(v / 3600)
ra, dec = self.sky_pos.deproject_rad(u_rad, v_rad)
# x/y to ra/dec
ra, dec = self.img_wcs.xyToradec(x, y, units="rad")
# ICRF to field
thx, thy = self.icrf_to_field.radecToxy(ra, dec, units="rad")

Expand All @@ -453,15 +480,7 @@ def inverse(self, v_photon: np.ndarray) -> tuple:
)
# field to ICRF
ra, dec = self.icrf_to_field.xyToradec(thx, thy, units="rad")
# ICRF to u/v
u_rad, v_rad = self.sky_pos.project_rad(ra, dec)
u = 3600 * np.rad2deg(u_rad)
v = 3600 * np.rad2deg(v_rad)
# u/v to x/y
x = self.local_wcs._x(u, v)
y = self.local_wcs._y(u, v)

return (x, y)
return self.img_wcs.radecToxy(ra, dec, units="rad")


def ray_vector_to_photon_array(
Expand Down
Loading
Loading