25 from scipy.stats
import median_abs_deviation
as mad
26 from scipy.signal
import fftconvolve
27 from scipy.optimize
import leastsq
28 from .astierCovFitParameters
import FitParameters
36 """Compute the "a" coefficients of the Antilogus+14 (1402.0725) model as in
37 Guyonnet+15 (1501.01577, eq. 16, the slope of cov/var at a given flux mu in electrons).
39 Eq. 16 of 1501.01577 is an approximation to the more complete model in Astier+19 (1905.08677).
44 Covariance model from Eq. 20 in Astier+19.
47 Mean signal in electrons
54 aCoeffsOld: `numpy.array`
55 Slope of cov/var at a given flux mu in electrons.
59 Returns the "a" array, computed this way, to be compared to the actual a_array from the full model
62 covModel = np.array(covModel)
63 var = covModel[0, 0, 0]
65 return covModel[0, :, :]/(var*muEl)
69 """Make covariances array from tuple.
73 inputTuple: `numpy.recarray`
74 Recarray with rows with at least (mu, cov, var, i, j, npix), where:
75 mu : 0.5*(m1 + m2), where:
76 mu1: mean value of flat1
77 mu2: mean value of flat2
78 cov: covariance value at lag(i, j)
79 var: variance(covariance value at lag(0, 0))
82 npix: number of pixels used for covariance calculation.
84 maxRangeFromTuple: `int`
85 Maximum range to select from tuple.
90 Covariance arrays, indexed by mean signal mu.
93 Variance arrays, indexed by mean signal mu.
96 List of mean signal values.
101 The input tuple should contain the following rows:
102 (mu, cov, var, i, j, npix), with one entry per lag, and image pair.
103 Different lags(i.e. different i and j) from the same
104 image pair have the same values of mu1 and mu2. When i==j==0, cov
107 If the input tuple contains several video channels, one should
108 select the data of a given channel *before* entering this
109 routine, as well as apply(e.g.) saturation cuts.
111 The routine returns cov[k_mu, j, i], vcov[(same indices)], and mu[k]
112 where the first index of cov matches the one in mu.
114 This routine implements the loss of variance due to
115 clipping cuts when measuring variances and covariance, but this should happen inside
116 the measurement code, where the cuts are readily available.
119 if maxRangeFromTuple
is not None:
120 cut = (inputTuple[
'i'] < maxRangeFromTuple) & (inputTuple[
'j'] < maxRangeFromTuple)
121 cutTuple = inputTuple[cut]
123 cutTuple = inputTuple
125 muTemp = cutTuple[
'mu']
126 ind = np.argsort(muTemp)
128 cutTuple = cutTuple[ind]
131 xx = np.hstack(([mu[0]], mu))
132 delta = xx[1:] - xx[:-1]
133 steps, = np.where(delta > 0)
134 ind = np.zeros_like(mu, dtype=int)
138 muVals = np.array(np.unique(mu))
139 i = cutTuple[
'i'].astype(int)
140 j = cutTuple[
'j'].astype(int)
141 c = 0.5*cutTuple[
'cov']
143 v = 0.5*cutTuple[
'var']
145 cov = np.ndarray((len(muVals), np.max(i)+1, np.max(j)+1))
146 var = np.zeros_like(cov)
148 var[ind, i, j] = v**2/n
157 return cov, var, muVals
161 """ Copy array over 4 quadrants prior to convolution.
165 inputarray: `numpy.array`
166 Input array to symmetrize.
174 targetShape =
list(inputArray.shape)
175 r1, r2 = inputArray.shape[-1], inputArray.shape[-2]
176 targetShape[-1] = 2*r1-1
177 targetShape[-2] = 2*r2-1
178 aSym = np.ndarray(tuple(targetShape))
179 aSym[..., r2-1:, r1-1:] = inputArray
180 aSym[..., r2-1:, r1-1::-1] = inputArray
181 aSym[..., r2-1::-1, r1-1::-1] = inputArray
182 aSym[..., r2-1::-1, r1-1:] = inputArray
188 """A class to calculate 2D polynomials"""
195 self.coeff, _, rank, _ = np.linalg.lstsq(G, z.ravel())
197 self.coeff, _, rank, _ = np.linalg.lstsq((w.ravel()*G.T).T, z.ravel()*w.ravel())
201 G = np.zeros(x.shape + (ncols,))
202 ij = itertools.product(range(self.
orderx+1), range(self.
ordery+1))
203 for k, (i, j)
in enumerate(ij):
204 G[..., k] = x**i * y**j
211 return np.dot(G, self.coeff)
215 """A class to fit the models in Astier+19 to flat covariances.
217 This code implements the model(and the fit thereof) described in
218 Astier+19: https://arxiv.org/pdf/1905.08677.pdf
219 For the time being it uses as input a numpy recarray (tuple with named tags) which
220 contains one row per covariance and per pair: see the routine makeCovArray.
224 inputTuple: `numpy.recarray`
225 Tuple with at least (mu, cov, var, i, j, npix), where:
226 mu : 0.5*(m1 + m2), where:
227 mu1: mean value of flat1
228 mu2: mean value of flat2
229 cov: covariance value at lag(i, j)
230 var: variance(covariance value at lag(0, 0))
233 npix: number of pixels used for covariance calculation.
235 maxRangeFromTuple: `int`
236 Maximum range to select from tuple.
239 def __init__(self, inputTuple, maxRangeFromTuple=8):
244 self.
logger = lsstLog.Log.getDefaultLogger()
247 """Subtract a background/offset to the measured covariances.
252 Maximum lag considered
255 First lag from where to start the offset subtraction.
258 Degree of 2D polynomial to fit to covariance to define offse to be subtracted.
260 assert(startLag < self.
r)
261 for k
in range(len(self.
mu)):
263 w = self.
sqrtW[k, ...] + 0.
265 i, j = np.meshgrid(range(sh[0]), range(sh[1]), indexing=
'ij')
267 w[:startLag, :startLag] = 0
268 poly =
Pol2d(i, j, self.
cov[k, ...], polDegree+1, w=w)
269 back = poly.eval(i, j)
270 self.
cov[k, ...] -= back
279 """Make a copy of params"""
280 cop = copy.deepcopy(self)
282 if hasattr(self,
'params'):
287 """ Performs a crude parabolic fit of the data in order to start
288 the full fit close to the solution.
296 self.
params[
'c'].fix(val=0.)
298 a = self.
params[
'a'].full.reshape(self.
r, self.
r)
299 noise = self.
params[
'noise'].full.reshape(self.
r, self.
r)
300 gain = self.
params[
'gain'].full[0]
309 for i
in range(self.
r):
310 for j
in range(self.
r):
312 parsFit = np.polyfit(self.
mu, self.
cov[:, i, j] - model[:, i, j],
313 2, w=self.
sqrtW[:, i, j])
315 a[i, j] += parsFit[0]
316 noise[i, j] += parsFit[2]*gain*gain
318 gain = 1./(1/gain+parsFit[1])
319 self.
params[
'gain'].full[0] = gain
328 """Return an array of free parameter values (it is a copy)."""
329 return self.
params.free + 0.
332 """Set parameter values."""
337 """Computes full covariances model (Eq. 20 of Astier+19).
341 mu: `numpy.array`, optional
342 List of mean signals.
346 covModel: `numpy.array`
351 By default, computes the covModel for the mu's stored(self.mu).
353 Returns cov[Nmu, self.r, self.r]. The variance for the PTC is cov[:, 0, 0].
354 mu and cov are in ADUs and ADUs squared. To use electrons for both,
355 the gain should be set to 1. This routine implements the model in Astier+19 (1905.08677).
357 The parameters of the full model for C_ij(mu) ("C_ij" and "mu" in ADU^2 and ADU, respectively)
358 in Astier+19 (Eq. 20) are:
360 "a" coefficients (r by r matrix), units: 1/e
361 "b" coefficients (r by r matrix), units: 1/e
362 noise matrix (r by r matrix), units: e^2
365 "b" appears in Eq. 20 only through the "ab" combination, which is defined in this code as "c=ab".
367 sa = (self.
r, self.
r)
368 a = self.
params[
'a'].full.reshape(sa)
369 c = self.
params[
'c'].full.reshape(sa)
370 gain = self.
params[
'gain'].full[0]
371 noise = self.
params[
'noise'].full.reshape(sa)
373 aEnlarged = np.zeros((int(sa[0]*1.5)+1, int(sa[1]*1.5)+1))
374 aEnlarged[0:sa[0], 0:sa[1]] = a
377 cEnlarged = np.zeros((int(sa[0]*1.5)+1, int(sa[1]*1.5)+1))
378 cEnlarged[0:sa[0], 0:sa[1]] = c
380 a2 = fftconvolve(aSym, aSym, mode=
'same')
381 a3 = fftconvolve(a2, aSym, mode=
'same')
382 ac = fftconvolve(aSym, cSym, mode=
'same')
383 (xc, yc) = np.unravel_index(np.abs(aSym).argmax(), a2.shape)
385 a1 = a[np.newaxis, :, :]
386 a2 = a2[np.newaxis, xc:xc + range, yc:yc + range]
387 a3 = a3[np.newaxis, xc:xc + range, yc:yc + range]
388 ac = ac[np.newaxis, xc:xc + range, yc:yc + range]
389 c1 = c[np.newaxis, ::]
393 bigMu = mu[:, np.newaxis, np.newaxis]*gain
395 covModel = (bigMu/(gain*gain)*(a1*bigMu+2./3.*(bigMu*bigMu)*(a2 + c1) +
396 (1./3.*a3 + 5./6.*ac)*(bigMu*bigMu*bigMu)) + noise[np.newaxis, :, :]/gain**2)
398 covModel[:, 0, 0] += mu/gain
403 """'a' matrix from Astier+19(e.g., Eq. 20)"""
404 return self.
params[
'a'].full.reshape(self.
r, self.
r)
407 """'b' matrix from Astier+19(e.g., Eq. 20)"""
408 return self.
params[
'c'].full.reshape(self.
r, self.
r)/self.
getA()
411 """'c'='ab' matrix from Astier+19(e.g., Eq. 20)"""
412 return np.array(self.
params[
'c'].full.reshape(self.
r, self.
r))
414 def _getCovParams(self, what):
415 """Get covariance matrix of parameters from fit"""
416 indices = self.
params[what].indexof()
417 i1 = indices[:, np.newaxis]
418 i2 = indices[np.newaxis, :]
426 """Get covariance matrix of "a" coefficients from fit"""
434 """Square root of diagonal of the parameter covariance of the fitted "a" matrix"""
436 sigA = np.sqrt(self.
_getCovParams(
'a').diagonal()).reshape((self.
r, self.
r))
442 """Get covariance matrix of "a" coefficients from fit
446 aval = self.
getA().flatten()
447 factor = np.outer(aval, aval)
449 return covb.reshape((self.
r, self.
r, self.
r, self.
r))
452 """Get covariance matrix of "c" coefficients from fit"""
454 return cova.reshape((self.
r, self.
r, self.
r, self.
r))
457 """Get error on fitted gain parameter"""
465 """Get covariances of noise matrix from fit"""
467 return covNoise.reshape((self.
r, self.
r, self.
r, self.
r))
470 """Square root of diagonal of the parameter covariance of the fitted "noise" matrix"""
473 noise = np.sqrt(covNoise.diagonal()).reshape((self.
r, self.
r))
479 """Get gain (e/ADU)"""
480 return self.
params[
'gain'].full[0]
483 """Get readout noise (e^2)"""
484 return self.
params[
'noise'].full[0]
487 """Get error on readout noise parameter"""
488 ronSqrt = np.sqrt(np.fabs(self.
getRon()))
491 ronErr = 0.5*(noiseSigma/np.fabs(self.
getRon()))*ronSqrt
497 """Get noise matrix"""
498 return self.
params[
'noise'].full.reshape(self.
r, self.
r)
501 """Get mask of Cov[i,j]"""
502 weights = self.
sqrtW[:, i, j]
507 """Set "a" and "b" coeffcients forfull Astier+19 model (Eq. 20). "c=a*b"."""
508 self.
params[
'a'].full = a.flatten()
509 self.
params[
'c'].full = a.flatten()*b.flatten()
513 """Calculate weighted chi2 of full-model fit."""
517 """To be used in weightedRes"""
518 if params
is not None:
521 return((covModel-self.
cov)*self.
sqrtW)
524 """Weighted residuas.
531 coeffs, cov, _, mesg, ierr = leastsq(c.weightedRes, c.getParamValues(), full_output=True)
533 return self.
wres(params).flatten()
536 """Fit measured covariances to full model in Astier+19 (Eq. 20)
541 Initial parameters of the fit.
542 len(pInit) = #entries(a) + #entries(c) + #entries(noise) + 1
543 len(pInit) = r^2 + r^2 + r^2 + 1, where "r" is the maximum lag considered for the
544 covariances calculation, and the extra "1" is the gain.
545 If "b" is 0, then "c" is 0, and len(pInit) will have r^2 fewer entries.
547 nSigma : `float`, optional
548 Sigma cut to get rid of outliers.
550 maxFitIter : `int`, optional
551 Number of iterations for full model fit.
556 Fit parameters (see "Notes" below).
560 The parameters of the full model for C_ij(mu) ("C_ij" and "mu" in ADU^2 and ADU, respectively)
561 in Astier+19 (Eq. 20) are:
563 "a" coefficients (r by r matrix), units: 1/e
564 "b" coefficients (r by r matrix), units: 1/e
565 noise matrix (r by r matrix), units: e^2
568 "b" appears in Eq. 20 only through the "ab" combination, which is defined in this code as "c=ab".
575 while nOutliers != 0:
577 params, paramsCov, _, mesg, ierr = leastsq(self.
weightedRes, pInit, full_output=
True)
580 sig = mad(wres[wres != 0], scale=
'normal')
581 mask = (np.abs(wres) > (nSigma*sig))
582 self.
sqrtW.flat[mask] = 0
583 nOutliers = mask.sum()
585 if counter == maxFitIter:
586 self.
logger.
warn(f
"Max number of iterations,{maxFitIter}, reached in fitFullModel")
593 """Number of degrees of freedom
597 mask.sum() - len(self.params.free): `int`
598 Number of usable pixels - number of parameters of fit.
600 mask = self.
sqrtW != 0
602 return mask.sum() - len(self.
params.free)
604 def getFitData(self, i, j, divideByMu=False, unitsElectrons=False, returnMasked=False):
605 """Get measured signal and covariance, cov model, weigths, and mask at covariance lag (i, j).
610 Lag for covariance matrix.
613 Lag for covariance matrix.
615 divideByMu: `bool`, optional
616 Divide covariance, model, and weights by signal mu?
618 unitsElectrons : `bool`, optional
619 mu, covariance, and model are in ADU (or powers of ADU) If tthis parameter is true, these are
620 multiplied by the adequte factors of the gain to return quantities in electrons
621 (or powers of electrons).
623 returnMasked : `bool`, optional
624 Use mask (based on weights) in returned arrays (mu, covariance, and model)?
629 list of signal values (mu).
631 covariance: `numpy.array`
632 Covariance arrays, indexed by mean signal mu (self.cov[:, i, j]).
634 covarianceModel: `numpy.array`
635 Covariance model (model).
637 weights: `numpy.array`
640 mask : `numpy.array`, optional
641 Boolean mask of the covariance at (i,j).
645 Using a CovFit object, selects from (i, j) and returns
646 mu*gain, self.cov[:, i, j]*gain**2 model*gain**2, and self.sqrtW/gain**2
647 in electrons or ADU if unitsElectrons=False.
655 covariance = self.
cov[:, i, j]*(gain**2)
656 covarianceModel = self.
evalCovModel()[:, i, j]*(gain**2)
657 weights = self.
sqrtW[:, i, j]/(gain**2)
662 weights = weights[mask]
663 covarianceModel = covarianceModel[mask]
665 covariance = covariance[mask]
669 covarianceModel /= mu
672 return mu, covariance, covarianceModel, weights, mask