Skip to content

Commit cc00a73

Browse files
authored
Merge pull request #55 from lsst-ts/develop
Merge `develop` into `main`
2 parents e4bfdb0 + 02dcd7a commit cc00a73

File tree

14 files changed

+111
-47
lines changed

14 files changed

+111
-47
lines changed

conftest.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,16 @@ def pytest_addoption(parser: Parser) -> None:
1919
default=False,
2020
help="Run WEP pipeline before all tests.",
2121
)
22+
parser.addoption(
23+
"--skip-pretest",
24+
action="store_true",
25+
default=False,
26+
help="Skip pre-test WEP pipeline run.",
27+
)
2228

2329

2430
def pytest_configure(config: Config) -> None:
25-
if config.getoption("--run-pretest"):
31+
if config.getoption("--run-pretest") and not config.getoption("--skip-pretest"):
2632
print("Running pre-test command...")
2733

2834
# Set up the butler repository config
@@ -80,7 +86,7 @@ def pytest_configure(config: Config) -> None:
8086

8187

8288
def pytest_unconfigure(config: Config) -> None:
83-
if config.getoption("--run-pretest"):
89+
if config.getoption("--run-pretest") and not config.getoption("--skip-pretest"):
8490
print("Running cleanup...")
8591
for runName in [
8692
config.testInfo["runNameCwfs"],

doc/news/DM-53134.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Correctly handling FAM by shifting entire camera in batoid model.

doc/news/DM-53632.bugfix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed bug with intrinsics in unpaired Zernike calculation task.

policy/instruments/LsstCam.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ wavelength:
2020
z: 866.8e-9
2121
y: 973.9e-9
2222
batoidModelName: LSST_{band} # name used to load the Batoid model
23+
batoidOffsetOptic: Detector
2324

2425
maskParams: # center and radius are in meters, theta in degrees
2526
M1:

python/lsst/ts/wep/estimation/wfAlgorithm.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,16 +283,16 @@ def estimateZk(
283283
if startWithIntrinsic or returnWfDev:
284284
zkIntrinsicI1 = instrument.getIntrinsicZernikes(
285285
*I1.fieldAngle,
286-
I1.bandLabel,
287-
nollIndices,
286+
band=I1.bandLabel,
287+
nollIndices=nollIndices,
288288
)
289289
zkIntrinsicI2 = (
290290
None
291291
if I2 is None
292292
else instrument.getIntrinsicZernikes(
293293
*I2.fieldAngle,
294-
I2.bandLabel,
295-
nollIndices,
294+
band=I2.bandLabel,
295+
nollIndices=nollIndices,
296296
)
297297
)
298298

python/lsst/ts/wep/imageMapper.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,7 +1267,7 @@ def createImageMasks(
12671267
# Get the intrinsic Zernikes
12681268
zkCoeff = self.instrument.getIntrinsicZernikes(
12691269
*image.fieldAngle,
1270-
image.bandLabel,
1270+
band=image.bandLabel,
12711271
)
12721272

12731273
# Get the image grid inside the pupil
@@ -1376,7 +1376,7 @@ def getProjectionSize(
13761376
# Get the intrinsic Zernikes
13771377
zkCoeff = self.instrument.getIntrinsicZernikes(
13781378
*dummyImage.fieldAngle,
1379-
dummyImage.bandLabel,
1379+
band=dummyImage.bandLabel,
13801380
)
13811381

13821382
# Project the pupil onto the image plane
@@ -1445,7 +1445,7 @@ def centerOnProjection(
14451445
# Get the intrinsic Zernikes
14461446
zkCoeff = self.instrument.getIntrinsicZernikes(
14471447
*image.fieldAngle,
1448-
image.bandLabel,
1448+
band=image.bandLabel,
14491449
)
14501450

14511451
# Create the image template
@@ -1513,7 +1513,7 @@ def mapPupilToImage(
15131513
# Get the intrinsic Zernikes
15141514
zkCoeff = self.instrument.getIntrinsicZernikes(
15151515
*image.fieldAngle,
1516-
image.bandLabel,
1516+
band=image.bandLabel,
15171517
)
15181518

15191519
# Get the image grid inside the pupil
@@ -1606,7 +1606,7 @@ def mapImageToPupil(
16061606
# Get the intrinsic Zernikes
16071607
zkCoeff = self.instrument.getIntrinsicZernikes(
16081608
*image.fieldAngle,
1609-
image.bandLabel,
1609+
band=image.bandLabel,
16101610
)
16111611

16121612
# Construct the forward mapping

python/lsst/ts/wep/instrument.py

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,11 @@ def batoidModelName(self, value: str | None) -> None:
633633
@property
634634
def batoidOffsetOptic(self) -> str | None:
635635
"""The optic that is offset in the Batoid model."""
636-
return self._batoidOffsetOptic
636+
# Default to the detector if value not explicitly set
637+
if self._batoidOffsetOptic is None and self.batoidModelName is not None:
638+
return "Detector"
639+
else:
640+
return self._batoidOffsetOptic
637641

638642
@batoidOffsetOptic.setter
639643
def batoidOffsetOptic(self, value: str | None) -> None:
@@ -734,6 +738,7 @@ def _getIntrinsicZernikesCached(
734738
self,
735739
xAngle: float,
736740
yAngle: float,
741+
defocalType: DefocalType | None,
737742
band: BandLabel | str,
738743
jmax: int,
739744
) -> np.ndarray:
@@ -747,6 +752,9 @@ def _getIntrinsicZernikesCached(
747752
The x-component of the field angle in degrees.
748753
yAngle : float
749754
The y-component of the field angle in degrees.
755+
defocalType : DefocalType or str or None
756+
The DefocalType Enum or corresponding string, specifying which side
757+
of focus to model. If None, the model is not defocused.
750758
band : BandLabel or str, optional
751759
The BandLabel Enum or corresponding string, specifying which batoid
752760
model to load. Only relevant if self.batoidModelName contains
@@ -769,6 +777,13 @@ def _getIntrinsicZernikesCached(
769777
if batoidModel is None:
770778
return np.zeros(jmax + 1)
771779

780+
# Offset the focal plane
781+
if defocalType is not None:
782+
defocalType = DefocalType(defocalType)
783+
defocalSign = +1 if defocalType == DefocalType.Extra else -1
784+
offset = [0, 0, defocalSign * self.defocalOffset]
785+
batoidModel = batoidModel.withLocallyShiftedOptic(self.batoidOffsetOptic, offset)
786+
772787
# Get the wavelength
773788
if len(self.wavelength) > 1:
774789
wavelength = self.wavelength[band]
@@ -794,6 +809,7 @@ def getIntrinsicZernikes(
794809
self,
795810
xAngle: float,
796811
yAngle: float,
812+
defocalType: DefocalType | None = None,
797813
band: BandLabel | str = BandLabel.REF,
798814
nollIndices: Sequence[int] = tuple(np.arange(4, 79)),
799815
) -> np.ndarray:
@@ -805,6 +821,9 @@ def getIntrinsicZernikes(
805821
The x-component of the field angle in degrees.
806822
yAngle : float
807823
The y-component of the field angle in degrees.
824+
defocalType : DefocalType or str or None
825+
The DefocalType Enum or corresponding string, specifying which side
826+
of focus to model. If None, the model is not defocused.
808827
band : BandLabel or str, optional
809828
The BandLabel Enum or corresponding string, specifying which batoid
810829
model to load. Only relevant if self.batoidModelName contains
@@ -822,7 +841,13 @@ def getIntrinsicZernikes(
822841
nollIndices = np.array(nollIndices)
823842

824843
# Retrieve cached Zernikes
825-
zk = self._getIntrinsicZernikesCached(xAngle, yAngle, band, max(nollIndices))
844+
zk = self._getIntrinsicZernikesCached(
845+
xAngle=xAngle,
846+
yAngle=yAngle,
847+
defocalType=defocalType,
848+
band=band,
849+
jmax=max(nollIndices),
850+
)
826851

827852
return zk[nollIndices]
828853

@@ -831,7 +856,7 @@ def _getIntrinsicZernikesTACached(
831856
self,
832857
xAngle: float,
833858
yAngle: float,
834-
defocalType: DefocalType,
859+
defocalType: DefocalType | None,
835860
band: BandLabel | str,
836861
jmax: int,
837862
) -> np.ndarray:
@@ -843,9 +868,9 @@ def _getIntrinsicZernikesTACached(
843868
The x-component of the field angle in degrees.
844869
yAngle : float
845870
The y-component of the field angle in degrees.
846-
defocalType : DefocalType or str
871+
defocalType : DefocalType or str or None
847872
The DefocalType Enum or corresponding string, specifying which side
848-
of focus to model.
873+
of focus to model. If None, the model is not defocused.
849874
band : BandLabel or str
850875
The BandLabel Enum or corresponding string, specifying which
851876
batoid model to load. Only relevant if self.batoidModelName
@@ -877,10 +902,11 @@ def _getIntrinsicZernikesTACached(
877902
return np.zeros(jmax + 1)
878903

879904
# Offset the focal plane
880-
defocalType = DefocalType(defocalType)
881-
defocalSign = +1 if defocalType == DefocalType.Extra else -1
882-
offset = [0, 0, defocalSign * self.defocalOffset]
883-
batoidModel = batoidModel.withLocallyShiftedOptic("Detector", offset)
905+
if defocalType is not None:
906+
defocalType = DefocalType(defocalType)
907+
defocalSign = +1 if defocalType == DefocalType.Extra else -1
908+
offset = [0, 0, defocalSign * self.defocalOffset]
909+
batoidModel = batoidModel.withLocallyShiftedOptic(self.batoidOffsetOptic, offset)
884910

885911
# Get the wavelength
886912
if len(self.wavelength) > 1:
@@ -909,7 +935,7 @@ def getOffAxisCoeff(
909935
self,
910936
xAngle: float,
911937
yAngle: float,
912-
defocalType: DefocalType,
938+
defocalType: DefocalType | None,
913939
band: BandLabel | str = BandLabel.REF,
914940
nollIndicesModel: Sequence = tuple(np.arange(4, 79)),
915941
nollIndicesIntr: Sequence = tuple(np.arange(4, 79)),
@@ -922,9 +948,9 @@ def getOffAxisCoeff(
922948
The x-component of the field angle in degrees.
923949
yAngle : float
924950
The y-component of the field angle in degrees.
925-
defocalType : DefocalType or str
951+
defocalType : DefocalType or str or None
926952
The DefocalType Enum or corresponding string, specifying which side
927-
of focus to model.
953+
of focus to model. If None, the model is not defocused.
928954
band : BandLabel or str, optional
929955
The BandLabel Enum or corresponding string, specifying which
930956
batoid model to load. Only relevant if self.batoidModelName
@@ -952,19 +978,20 @@ def getOffAxisCoeff(
952978

953979
# Get zernikeTA
954980
zkTA = self._getIntrinsicZernikesTACached(
955-
xAngle,
956-
yAngle,
957-
defocalType,
958-
band,
959-
max(nollIndicesModel),
981+
xAngle=xAngle,
982+
yAngle=yAngle,
983+
defocalType=defocalType,
984+
band=band,
985+
jmax=max(nollIndicesModel),
960986
)
961987

962988
# Get regular intrinsic zernikes
963989
zk = self._getIntrinsicZernikesCached(
964-
xAngle,
965-
yAngle,
966-
band,
967-
max(nollIndicesIntr),
990+
xAngle=xAngle,
991+
yAngle=yAngle,
992+
defocalType=None,
993+
band=band,
994+
jmax=max(nollIndicesIntr),
968995
)
969996

970997
# Subtract intrinsics from zernikeTA

python/lsst/ts/wep/task/calcZernikesTask.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def _unpackStampData(self, stamp: DonutStamp) -> tuple[u.Quantity, u.Quantity, u
250250
if stamp is None:
251251
fieldAngle = np.array(np.nan, dtype=pos2f_dtype) * u.deg
252252
centroid = np.array((np.nan, np.nan), dtype=pos2f_dtype) * u.pixel
253-
intrinsics = np.full_like(self.nollIndices, np.nan) * u.micron
253+
intrinsics = np.full(len(self.nollIndices), np.nan) * u.micron
254254
else:
255255
fieldAngle = np.array(stamp.calcFieldXY(), dtype=pos2f_dtype) * u.deg
256256
centroid = (

python/lsst/ts/wep/utils/plotUtils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ def plotMapperResiduals(
429429
uImage, vImage, *_ = mapper._constructForwardMap(
430430
uPupil,
431431
vPupil,
432-
mapper.instrument.getIntrinsicZernikes(*angle, band, jmax=22),
432+
mapper.instrument.getIntrinsicZernikes(*angle, band=band, nollIndices=np.arange(4, 23)),
433433
Image(np.zeros((1, 1)), angle, defocalType, band),
434434
)
435435

tests/task/test_calcZernikesUnpairedTask.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ def testWithAndWithoutPairs(self) -> None:
130130
collections=["LSSTCam/aos/intrinsic"],
131131
)
132132

133+
# Modify the intrinsic table to allow explicit value testing below
134+
intrinsicTable["Z4"] = 50e-9 # nm
135+
133136
# Loop over EstimateZernikes subtasks
134137
for subtask in [EstimateZernikesTieTask, EstimateZernikesDanishTask]:
135138
# Calculate Zernikes with stamps paired
@@ -145,14 +148,22 @@ def testWithAndWithoutPairs(self) -> None:
145148
config.estimateZernikes.retarget(subtask)
146149
unpairedTask = CalcZernikesUnpairedTask(config=config)
147150

148-
extraZk = unpairedTask.run(donutStampsExtra, intrinsicTable).outputZernikesAvg
149-
intraZk = unpairedTask.run(donutStampsIntra, intrinsicTable).outputZernikesAvg
151+
outputExtra = unpairedTask.run(donutStampsExtra, intrinsicTable)
152+
outputIntra = unpairedTask.run(donutStampsIntra, intrinsicTable)
153+
extraZk = outputExtra.outputZernikesAvg
154+
intraZk = outputIntra.outputZernikesAvg
150155
meanZk = np.mean([extraZk, intraZk], axis=0)
151156

152157
# Check that results are similar
153158
diff = np.sqrt(np.sum((meanZk - pairedZk) ** 2))
154159
self.assertLess(diff, 0.17)
155160

161+
# Check the stored intrinsic Z4 is close to modified value
162+
for struct in [outputExtra, outputIntra]:
163+
table = struct.zernikes
164+
intrinsicZ4 = table[table["label"] == "average"]["Z4_intrinsic"][0]
165+
self.assertTrue(np.isclose(intrinsicZ4.to_value("nm"), 50.0))
166+
156167
def testTable(self) -> None:
157168
# Load data from butler
158169
donutStampsExtra = self.butler.get(

0 commit comments

Comments
 (0)