Skip to content

Commit

Permalink
Implemented at interactive level
Browse files Browse the repository at this point in the history
  • Loading branch information
jfcrenshaw committed Nov 11, 2024
1 parent 2ac3aa9 commit 2d35f34
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 163 deletions.
2 changes: 1 addition & 1 deletion doc/versionHistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Version History
-------------

* enabled sparse Zernike estimation
* removed jmax and return4Up configs in favor of nollIndices configs
* removed most jmax and return4Up configs in favor of nollIndices configs
* removed return4Up from estimator WfEstimator and WfAlgorithm
* added makeSparse and makeDense to Zernike utils

Expand Down
34 changes: 19 additions & 15 deletions python/lsst/ts/wep/estimation/danish.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def history(self) -> dict:
following entries
- "image" - the image that is being fit
- "variance" - the background variance that was used for fitting
- "nollIndices" - Noll indices for which Zernikes are estimated
- "zkStart" - the starting Zernike coefficients
- "lstsqResult" - dictionary of results returned by least_squares
- "zkFit" - the Zernike coefficients fit to the donut
Expand All @@ -140,6 +141,7 @@ def _estimateSingleZk(
self,
image: Image,
zkStart: np.ndarray,
nollIndices: np.ndarray,
instrument: Instrument,
factory: danish.DonutFactory,
saveHistory: bool,
Expand All @@ -152,6 +154,8 @@ def _estimateSingleZk(
The ts_wep image of the donut stamp
zkStart : np.ndarray
The starting point for the Zernikes
nollIndices : np.ndarray
Noll indices for which to estimate Zernikes
instrument : Instrument
The ts_wep Instrument
factory : danish.DonutFactory
Expand All @@ -170,22 +174,16 @@ def _estimateSingleZk(
if np.isfinite(image.blendOffsets).sum() > 0:
warnings.warn("Danish is currently only setup for non-blended donuts.")

# Determine the maximum Noll index
jmax = len(zkStart) + 3

# Create reference Zernikes by adding off-axis coefficients to zkStart
zkStart = np.pad(zkStart, (4, 0))
offAxisCoeff = instrument.getOffAxisCoeff(
*image.fieldAngle,
image.defocalType,
image.bandLabel,
jmaxIntrinsic=jmax,
return4Up=False,
nollIndicesModel=np.arange(0, 79),
nollIndicesIntr=nollIndices,
)
size = max(zkStart.size, offAxisCoeff.size)
zkRef = np.zeros(size)
zkRef[: zkStart.size] = zkStart
zkRef[: offAxisCoeff.size] += offAxisCoeff
zkRef = offAxisCoeff.copy()
zkRef[nollIndices] += zkStart

# Create the background mask if it does not exist
if image.maskBackground is None:
Expand Down Expand Up @@ -213,14 +211,14 @@ def _estimateSingleZk(
model = danish.SingleDonutModel(
factory,
z_ref=zkRef,
z_terms=np.arange(4, jmax + 1),
z_terms=nollIndices,
thx=np.deg2rad(image.fieldAngle[0]),
thy=np.deg2rad(image.fieldAngle[1]),
npix=img.shape[0],
)

# Create the initial guess for the model parameters
x0 = [0.0, 0.0, 1.0] + [0.0] * (jmax - 3)
x0 = [0.0, 0.0, 1.0] + [0.0] * len(nollIndices)

# Use scipy to optimize the parameters
try:
Expand All @@ -238,7 +236,7 @@ def _estimateSingleZk(
zkFit = np.array(zkFit)

# Add the starting zernikes back into the result
zkSum = zkFit + zkStart[4:]
zkSum = zkFit + zkStart

# Flag that we didn't hit GalSimFFTSizeError
galSimFFTSizeError = False
Expand All @@ -258,8 +256,8 @@ def _estimateSingleZk(
except GalSimFFTSizeError:
# Fill dummy objects
result = None
zkFit = np.full(jmax - 3, np.nan)
zkSum = np.full(jmax - 3, np.nan)
zkFit = np.full_like(zkStart, np.nan)
zkSum = np.full_like(zkStart, np.nan)
if saveHistory:
modelImage = np.full_like(img, np.nan)

Expand All @@ -271,6 +269,7 @@ def _estimateSingleZk(
hist = {
"image": img.copy(),
"variance": backgroundStd**2,
"nollIndices": nollIndices.copy(),
"zkStart": zkStart.copy(),
"lstsqResult": result,
"zkFit": zkFit.copy(),
Expand All @@ -290,6 +289,7 @@ def _estimateZk(
I2: Optional[Image],
zkStartI1: np.ndarray,
zkStartI2: Optional[np.ndarray],
nollIndices: np.ndarray,
instrument: Instrument,
saveHistory: bool,
) -> np.ndarray:
Expand All @@ -305,6 +305,8 @@ def _estimateZk(
The starting Zernikes for I1 (in meters, for Noll indices >= 4)
zkStartI2 : np.ndarray or None
The starting Zernikes for I2 (in meters, for Noll indices >= 4)
nollIndices : np.ndarray
Noll indices for which you wish to estimate Zernike coefficients.
instrument : Instrument
The Instrument object associated with the DonutStamps.
saveHistory : bool
Expand Down Expand Up @@ -334,6 +336,7 @@ def _estimateZk(
zk1, hist[I1.defocalType.value] = self._estimateSingleZk(
I1,
zkStartI1,
nollIndices,
instrument,
factory,
saveHistory,
Expand All @@ -344,6 +347,7 @@ def _estimateZk(
zk2, hist[I2.defocalType.value] = self._estimateSingleZk(
I2,
zkStartI2,
nollIndices,
instrument,
factory,
saveHistory,
Expand Down
36 changes: 29 additions & 7 deletions python/lsst/ts/wep/estimation/tie.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
binArray,
createZernikeBasis,
createZernikeGradBasis,
makeSparse,
)
from scipy.ndimage import gaussian_filter

Expand Down Expand Up @@ -447,6 +448,8 @@ def history(self) -> dict:
full OPD.
- "pupil" - model pupil image, in the case where only one image
was passed to the estimator.
- "nollIndices" - array of Noll indices corresponding to the
estimated Zernikes values.
Each iteration of the solver is then stored under indices >= 1.
The entry for each iteration is also a dictionary, containing
Expand Down Expand Up @@ -570,7 +573,7 @@ def _expSolve(
self,
I0: np.ndarray,
dIdz: np.ndarray,
jmax: int,
nollIndices: np.ndarray,
instrument: Instrument,
) -> np.ndarray:
"""Solve the TIE directly using a Zernike expansion.
Expand All @@ -581,8 +584,8 @@ def _expSolve(
The beam intensity at the exit pupil
dIdz : np.ndarray
The z-derivative of the beam intensity across the exit pupil
jmax : int
The maximum Zernike Noll index to estimate
nollIndices : np.ndarray
Noll indices for which you wish to estimate Zernike coefficients.
instrument : Instrument, optional
The Instrument object associated with the DonutStamps.
(the default is the default Instrument)
Expand All @@ -593,8 +596,11 @@ def _expSolve(
Numpy array of the Zernike coefficients estimated from the image
or pair of images, in nm.
"""
# Get Zernike Bases
# Get the pupil grid
uPupil, vPupil = instrument.createPupilGrid()

# Get Zernike Bases
jmax = nollIndices.max()
zk = createZernikeBasis(
uPupil,
vPupil,
Expand All @@ -608,6 +614,11 @@ def _expSolve(
obscuration=instrument.obscuration,
)

# Down-select to the Zernikes we are solving for
zk = makeSparse(zk, nollIndices)
dzkdu = makeSparse(dzkdu, nollIndices)
dzkdv = makeSparse(dzkdv, nollIndices)

# Calculate quantities for the linear system
# See Equations 43-45 of https://sitcomtn-111.lsst.io
b = np.einsum("ab,jab->j", dIdz, zk, optimize=True)
Expand All @@ -626,6 +637,7 @@ def _estimateZk(
I2: Optional[Image], # type: ignore[override]
zkStartI1: np.ndarray,
zkStartI2: Optional[np.ndarray],
nollIndices: np.ndarray,
instrument: Instrument,
saveHistory: bool,
) -> np.ndarray:
Expand All @@ -642,6 +654,8 @@ def _estimateZk(
zkStartI2 : np.ndarray or None
The starting Zernikes for I2 (in meters, for Noll indices >= 4)
Can be None.
nollIndices : np.ndarray
Noll indices for which you wish to estimate Zernike coefficients.
instrument : Instrument
The Instrument object associated with the DonutStamps.
saveHistory : bool
Expand All @@ -660,9 +674,11 @@ def _estimateZk(
RuntimeError
If the solver is not supported
"""
# Rescale instrument pixel size to account for binning
if self.binning > 1:
instrument = instrument.copy()
instrument.pixelSize *= self.binning

# Create the ImageMapper for centering and image compensation
imageMapper = ImageMapper(instConfig=instrument, opticalModel=self.opticalModel)

Expand All @@ -674,6 +690,7 @@ def _estimateZk(
zkStartI2,
)

# Bin images to reduce resolution?
if self.binning > 1:
if intra is not None:
intra.image = binArray(intra.image, self.binning)
Expand Down Expand Up @@ -706,6 +723,7 @@ def _estimateZk(
"zkStartExtra": None if extra is None else zkStartExtra.copy(),
"zkStartMean": zkStartMean.copy(),
"pupil": None if pupil is None else pupil.copy(),
"nollIndices": nollIndices.copy(),
}

# Initialize Zernike arrays at zero
Expand All @@ -719,7 +737,7 @@ def _estimateZk(
compSequence = iter(self.compSequence)

# Determine the maximum Noll index we will solve for
jmax = len(zkStartMean) + 3
jmax = nollIndices.max()

# Set the caustic and converged flags to False
caustic = False
Expand All @@ -735,7 +753,7 @@ def _estimateZk(
# The gain scales how much of previous residual we incorporate
# Everything past jmaxComp is set to zero
zkComp += self.compGain * zkResid
zkComp[(jmaxComp - 3) :] = 0
zkComp[nollIndices > jmaxComp] = 0

# Center the images
recenter = (i == 0) or (np.max(np.abs(zkComp - zkCenter)) > self.centerTol)
Expand All @@ -748,6 +766,7 @@ def _estimateZk(
else imageMapper.centerOnProjection(
intra,
zkCenter + zkStartIntra,
nollIndices,
isBinary=self.centerBinary,
**self.maskKwargs,
)
Expand All @@ -758,6 +777,7 @@ def _estimateZk(
else imageMapper.centerOnProjection(
extra,
zkCenter + zkStartExtra,
nollIndices,
isBinary=self.centerBinary,
**self.maskKwargs,
)
Expand All @@ -770,6 +790,7 @@ def _estimateZk(
else imageMapper.mapImageToPupil(
intraCent,
zkComp + zkStartIntra,
nollIndices,
masks=None if i == 0 else intraComp.masks, # noqa: F821
**self.maskKwargs,
)
Expand All @@ -780,6 +801,7 @@ def _estimateZk(
else imageMapper.mapImageToPupil(
extraCent,
zkComp + zkStartExtra,
nollIndices,
masks=None if i == 0 else extraComp.masks, # noqa: F821
**self.maskKwargs,
)
Expand Down Expand Up @@ -817,7 +839,7 @@ def _estimateZk(
)

# Estimate the Zernikes
zkResid = self._expSolve(I0, dIdz, jmax, instrument)
zkResid = self._expSolve(I0, dIdz, nollIndices, instrument)

# If estimating with a single donut, double the coefficients
# This is because the simulated pupil image is aberration free
Expand Down
Loading

0 comments on commit 2d35f34

Please sign in to comment.