Skip to content

Commit

Permalink
Expanded Batoid configurability.
Browse files Browse the repository at this point in the history
  • Loading branch information
jfcrenshaw committed Dec 15, 2023
1 parent 7270bed commit 752cb1c
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 12 deletions.
2 changes: 2 additions & 0 deletions policy/instruments/AuxTel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ defocalOffset: 32.8e-3 # defocal offset in meters
pixelSize: 10.0e-6 # pixel size in meters
wavelength: 677.0e-9 # effective wavelength, in meters
batoidModelName: AuxTel.yaml # name used to load the Batoid model
batoidOffsetValue: 0.8e-3 # batoid offset in meter (default = defocalOffset)
batoidOffsetOptic: M2 # the name of the batoid element to offset (default = Detector)

maskParams:
BaffleM2cInner:
Expand Down
95 changes: 92 additions & 3 deletions python/lsst/ts/wep/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ class Instrument:
photometric bands, and the names of these bands will be filled in at
runtime using the strings specified in the BandLabel enum in
jf_wep.utils.enums.
batoidOffsetOptic : str, optional
The optic to offset in the Batoid model in order to simulate
defocal images. When this is None, it defaults to "Detector".
batoidOffsetValue : float, optional
The value, in meters, to offset the optic in the Batoid model to
simulate defocal images. When this is None, it defaults to the
defocalOffset.
maskParams : dict, optional
Dictionary of mask parameters. Each key in this dictionary corresponds
to a different mask element. The corresponding values are dictionaries
Expand All @@ -79,6 +86,7 @@ class Instrument:
to determine the center of the circle
- radius: list of polynomial coefficients (in meters) for np.polyval
to determine the radius of the circle
None defaults to an empty dictionary.
"""

def __init__(
Expand All @@ -93,6 +101,8 @@ def __init__(
refBand: Union[BandLabel, str, None] = None,
wavelength: Union[float, dict, None] = None,
batoidModelName: Optional[str] = None,
batoidOffsetOptic: Optional[str] = None,
batoidOffsetValue: Optional[float] = None,
maskParams: Optional[dict] = None,
) -> None:
# Merge keyword arguments with defaults from configFile
Expand All @@ -107,6 +117,8 @@ def __init__(
refBand=refBand,
wavelength=wavelength,
batoidModelName=batoidModelName,
batoidOffsetOptic=batoidOffsetOptic,
batoidOffsetValue=batoidOffsetValue,
maskParams=maskParams,
)

Expand Down Expand Up @@ -393,6 +405,78 @@ def batoidModelName(self, value: Optional[str]) -> None:
self._getIntrinsicZernikesCached.cache_clear()
self._getOffAxisCoeffCached.cache_clear()

@property
def batoidOffsetOptic(self) -> Union[str, None]:
"""The optic that is offset in the Batoid model."""
if self.batoidModelName is None:
return None
elif self._batoidOffsetOptic is None:
return "Detector"
else:
return self._batoidOffsetOptic

@batoidOffsetOptic.setter
def batoidOffsetOptic(self, value: Union[str, None]) -> None:
"""Set the optic that is offset in the Batoid model.
This is the optic to offset in the Batoid model in order to simulate
defocal images. When this is None, it defaults to "Detector".
Parameters
----------
value : str or None
The name of the optic to be offset in the Batoid model.
Raises
------
RuntimeError
If no Batoid model is set
TypeError
If value is not a string or None
ValueError
If the optic is not found in the Batoid model
"""
if value is not None and self.batoidModelName is None:
raise RuntimeError("There is no Batoid model set.")
elif value is not None and not isinstance(value, str):
raise TypeError("batoidOffsetOptic must be a string or None.")
elif value is not None and value not in self.getBatoidModel()._names:
raise ValueError(f"Optic {value} not found in the Batoid model.")

self._batoidOffsetOptic = value

@property
def batoidOffsetValue(self) -> Union[float, None]:
"""The amount in meters by which the optic is offset in the Batoid model."""
if self.batoidModelName is None:
return None
elif self._batoidOffsetValue is None:
return self.defocalOffset
else:
return self._batoidOffsetValue

@batoidOffsetValue.setter
def batoidOffsetValue(self, value: Union[str, None]) -> None:
"""Set the amount in meters by which the optic is offset in the batoid model.
Parameters
----------
value : float or None
The offset value. If None, this value is defaulted to
self.defocalOffset.
Raises
------
RuntimeError
If no Batoid model is set
"""
if value is not None and self.batoidModelName is None:
raise RuntimeError("There is no Batoid model set.")
elif value is None:
self._batoidOffsetValue = value
else:
self._batoidOffsetValue = np.abs(float(value))

@lru_cache(10)
def getBatoidModel(
self, band: Union[BandLabel, str] = BandLabel.REF
Expand Down Expand Up @@ -555,8 +639,9 @@ def _getOffAxisCoeffCached(
# Offset the focal plane
defocalType = DefocalType(defocalType)
defocalSign = +1 if defocalType == DefocalType.Extra else -1
offset = defocalSign * self.defocalOffset
batoidModel = batoidModel.withGloballyShiftedOptic("Detector", [0, 0, offset])
optic = self.batoidOffsetOptic
offset = defocalSign * self.batoidOffsetValue
batoidModel = batoidModel.withGloballyShiftedOptic(optic, [0, 0, offset])

# Get the off-axis model Zernikes in wavelengths
zkIntrinsic = batoid.zernikeTA(
Expand Down Expand Up @@ -653,14 +738,18 @@ def maskParams(self, value: Optional[dict]) -> None:
to determine the center of the circle
- radius: list of polynomial coefficients (in meters) for np.polyval
to determine the radius of the circle
None defaults to an empty dictionary.
Raises
------
TypeError
If value is not a dictionary or None
"""
if isinstance(value, dict) or value is None:
if isinstance(value, dict):
self._maskParams = value
elif value is None:
self._maskParams = dict()
else:
raise TypeError("maskParams must be a dictionary or None.")

Expand Down
15 changes: 8 additions & 7 deletions tests/test_imageMapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,33 +60,34 @@ def _createImageWithBatoid(nPixels, fieldAngle, defocalType, instrument):

# Get the Batoid model from the instrument
defocalSign = +1 if defocalType == "extra" else -1
offset = defocalSign * instrument.defocalOffset
optic = instrument.getBatoidModel()
optic = optic.withGloballyShiftedOptic("Detector", [0, 0, offset])
offset = defocalSign * instrument.batoidOffsetValue
optic = instrument.batoidOffsetOptic
model = instrument.getBatoidModel()
model = model.withGloballyShiftedOptic(optic, [0, 0, offset])

# We need to get the image position of the chief ray
rays = RayVector.fromStop(
x=0,
y=0,
optic=optic,
optic=model,
wavelength=instrument.wavelength["ref"],
theta_x=np.deg2rad(fieldAngle)[0],
theta_y=np.deg2rad(fieldAngle)[1],
)
optic.trace(rays)
model.trace(rays)
x0 = rays.x
y0 = rays.y

# Now map the pupil grid onto the image
rays = RayVector.fromStop(
x=x.flatten(),
y=y.flatten(),
optic=optic,
optic=model,
wavelength=instrument.wavelength["ref"],
theta_x=np.deg2rad(fieldAngle)[0],
theta_y=np.deg2rad(fieldAngle)[1],
)
optic.trace(rays)
model.trace(rays)

# Now we need to bin the unvignetted rays in the image grid
# Get the image grid from the instrument and convert to meters
Expand Down
27 changes: 25 additions & 2 deletions tests/test_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ def testGetBatoidModel(self):
batoidModel = Instrument().getBatoidModel()
self.assertIsInstance(batoidModel, CompoundOptic)

def testBadBatoidOffsetOptic(self):
with self.assertRaises(RuntimeError):
inst = Instrument()
inst.batoidModelName = None
inst.batoidOffsetOptic = "Detector"
with self.assertRaises(TypeError):
Instrument(batoidOffsetOptic=1)
with self.assertRaises(ValueError):
Instrument(batoidOffsetOptic="fake")

def testDefaultBatoidOffsetOptic(self):
inst = Instrument(batoidOffsetOptic=None)
self.assertEqual(inst.batoidOffsetOptic, "Detector")

def testBadBatoidOffsetValue(self):
with self.assertRaises(RuntimeError):
inst = Instrument()
inst.batoidModelName = None
inst.batoidOffsetValue = 1

def testDefaultBatoidOffsetValue(self):
inst = Instrument(batoidOffsetValue=None)
self.assertEqual(inst.batoidOffsetValue, inst.defocalOffset)

def testGetIntrinsicZernikes(self):
inst = Instrument()

Expand Down Expand Up @@ -121,8 +145,7 @@ def testBadMaskParams(self):
def testDefaultMaskParams(self):
inst = Instrument()
inst.maskParams = None
self.assertIsInstance(inst.maskParams, dict)
self.assertListEqual(list(inst.maskParams), ["pupilOuter", "pupilInner"])
self.assertEqual(inst.maskParams, dict())

def testCreatePupilGrid(self):
uImage, vImage = Instrument().createPupilGrid()
Expand Down

0 comments on commit 752cb1c

Please sign in to comment.