LSST Applications g0f08755f38+82efc23009,g12f32b3c4e+e7bdf1200e,g1653933729+a8ce1bb630,g1a0ca8cf93+50eff2b06f,g28da252d5a+52db39f6a5,g2bbee38e9b+37c5a29d61,g2bc492864f+37c5a29d61,g2cdde0e794+c05ff076ad,g3156d2b45e+41e33cbcdc,g347aa1857d+37c5a29d61,g35bb328faa+a8ce1bb630,g3a166c0a6a+37c5a29d61,g3e281a1b8c+fb992f5633,g414038480c+7f03dfc1b0,g41af890bb2+11b950c980,g5fbc88fb19+17cd334064,g6b1c1869cb+12dd639c9a,g781aacb6e4+a8ce1bb630,g80478fca09+72e9651da0,g82479be7b0+04c31367b4,g858d7b2824+82efc23009,g9125e01d80+a8ce1bb630,g9726552aa6+8047e3811d,ga5288a1d22+e532dc0a0b,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc28159a63d+37c5a29d61,gcf0d15dbbd+2acd6d4d48,gd7358e8bfb+778a810b6e,gda3e153d99+82efc23009,gda6a2b7d83+2acd6d4d48,gdaeeff99f8+1711a396fd,ge2409df99d+6b12de1076,ge79ae78c31+37c5a29d61,gf0baf85859+d0a5978c5a,gf3967379c6+4954f8c433,gfb92a5be7c+82efc23009,gfec2e1e490+2aaed99252,w.2024.46
LSST Data Management Base Package
Loading...
Searching...
No Matches
photodiode.py
Go to the documentation of this file.
1# This file is part of ip_isr.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21"""
22Photodiode storage class.
23"""
24
25__all__ = ["PhotodiodeCalib"]
26
27import numpy as np
28from astropy.table import Table
29
30from lsst.ip.isr import IsrCalib
31
32
34 """Independent current measurements from photodiode for linearity
35 calculations.
36
37 Parameters
38 ----------
39 timeSamples : `list` or `numpy.ndarray`
40 List of samples the photodiode was measured at.
41 currentSamples : `list` or `numpy.ndarray`
42 List of current measurements at each time sample.
43 log : `logging.Logger`, optional
44 Log to write messages to. If `None` a default logger will be used.
45 **kwargs :
46 Additional parameters. These will be passed to the parent
47 constructor with the exception of:
48
49 ``"integrationMethod"``
50 Name of the algorithm to use to integrate the current
51 samples. Allowed values are ``DIRECT_SUM``,
52 ``TRIMMED_SUM``, and ``CHARGE_SUM`` (`str`).
53 ``"currentScale"``
54 Scale factor to apply to the current samples for the
55 ``CHARGE_SUM`` integration method. A typical value
56 would be `-1`, to flip the sign of the integrated charge.
57 """
58
59 _OBSTYPE = 'PHOTODIODE'
60 _SCHEMA = 'Photodiode'
61 _VERSION = 1.0
62
63 def __init__(self, timeSamples=None, currentSamples=None, **kwargs):
64 if timeSamples is not None and currentSamples is not None:
65 if len(timeSamples) != len(currentSamples):
66 raise RuntimeError(f"Inconsitent vector lengths: {len(timeSamples)} vs {len(currentSamples)}")
67 else:
68 self.timeSamples = np.array(timeSamples).ravel()
69 self.currentSamples = np.array(currentSamples).ravel()
70 else:
71 self.timeSamples = np.array([]).ravel()
72 self.currentSamples = np.array([]).ravel()
73
74 super().__init__(**kwargs)
75
76 if 'integrationMethod' in kwargs:
77 self.integrationMethod = kwargs.pop('integrationMethod')
78 else:
79 self.integrationMethod = 'DIRECT_SUM'
80
81 if 'currentScale' in kwargs:
82 self.currentScale = kwargs.pop('currentScale')
83 else:
84 self.currentScale = 1.0
85
86 if 'day_obs' in kwargs:
87 self.updateMetadata(day_obs=kwargs['day_obs'])
88 if 'seq_num' in kwargs:
89 self.updateMetadata(seq_num=kwargs['seq_num'])
90
91 self.requiredAttributesrequiredAttributesrequiredAttributes.update(['timeSamples', 'currentSamples', 'integrationMethod'])
92
93 @classmethod
94 def fromDict(cls, dictionary):
95 """Construct a PhotodiodeCalib from a dictionary of properties.
96
97 Parameters
98 ----------
99 dictionary : `dict`
100 Dictionary of properties.
101
102 Returns
103 -------
104 calib : `lsst.ip.isr.PhotodiodeCalib`
105 Constructed photodiode data.
106
107 Raises
108 ------
109 RuntimeError
110 Raised if the supplied dictionary is for a different
111 calibration type.
112 """
113 calib = cls()
114
115 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
116 raise RuntimeError(f"Incorrect photodiode supplied. Expected {calib._OBSTYPE}, "
117 f"found {dictionary['metadata']['OBSTYPE']}")
118
119 calib.setMetadata(dictionary['metadata'])
120
121 calib.timeSamples = np.array(dictionary['timeSamples']).ravel()
122 calib.currentSamples = np.array(dictionary['currentSamples']).ravel()
123 calib.integrationMethod = dictionary.get('integrationMethod', "DIRECT_SUM")
124
125 calib.updateMetadata()
126 return calib
127
128 def toDict(self):
129 """Return a dictionary containing the photodiode properties.
130
131 The dictionary should be able to be round-tripped through.
132 `fromDict`.
133
134 Returns
135 -------
136 dictionary : `dict`
137 Dictionary of properties.
138 """
139 self.updateMetadata()
140
141 outDict = {}
142 outDict['metadata'] = self.getMetadata()
143
144 outDict['timeSamples'] = self.timeSamples.tolist()
145 outDict['currentSamples'] = self.currentSamples.tolist()
146
147 outDict['integrationMethod'] = self.integrationMethod
148
149 return outDict
150
151 @classmethod
152 def fromTable(cls, tableList):
153 """Construct calibration from a list of tables.
154
155 This method uses the `fromDict` method to create the
156 calibration after constructing an appropriate dictionary from
157 the input tables.
158
159 Parameters
160 ----------
161 tableList : `list` [`astropy.table.Table`]
162 List of tables to use to construct the crosstalk
163 calibration.
164
165 Returns
166 -------
167 calib : `lsst.ip.isr.PhotodiodeCalib`
168 The calibration defined in the tables.
169 """
170 dataTable = tableList[0]
171
172 metadata = dataTable.meta
173 inDict = {}
174 inDict['metadata'] = metadata
175 if 'OBSTYPE' not in metadata:
176 inDict['metadata']['OBSTYPE'] = cls._OBSTYPE_OBSTYPE
177 inDict['integrationMethod'] = metadata.pop('INTEGRATION_METHOD', 'DIRECT_SUM')
178
179 for key in ('TIME', 'Elapsed Time', ):
180 if key in dataTable.columns:
181 inDict['timeSamples'] = dataTable[key]
182 for key in ('CURRENT', 'Signal', ):
183 if key in dataTable.columns:
184 inDict['currentSamples'] = dataTable[key]
185
186 return cls().fromDict(inDict)
187
188 def toTable(self):
189 """Construct a list of tables containing the information in this
190 calibration.
191
192 The list of tables should create an identical calibration
193 after being passed to this class's fromTable method.
194
195 Returns
196 -------
197 tableList : `list` [`astropy.table.Table`]
198 List of tables containing the photodiode calibration
199 information.
200 """
201 self.updateMetadata()
202 catalog = Table([{'TIME': self.timeSamples,
203 'CURRENT': self.currentSamples}])
204 inMeta = self.getMetadata().toDict()
205 outMeta = {k: v for k, v in inMeta.items() if v is not None}
206 outMeta.update({k: "" for k, v in inMeta.items() if v is None})
207 outMeta['INTEGRATION_METHOD'] = self.integrationMethod
208 catalog.meta = outMeta
209
210 return [catalog]
211
212 @classmethod
213 def readTwoColumnPhotodiodeData(cls, filename):
214 """Construct a PhotodiodeCalib by reading the simple column format.
215
216 Parameters
217 ----------
218 filename : `str`
219 File to read samples from.
220
221 Returns
222 -------
223 calib : `lsst.ip.isr.PhotodiodeCalib`
224 The calibration defined in the file.
225 """
226 import os.path
227
228 rawData = np.loadtxt(filename, dtype=[('time', 'float'), ('current', 'float')])
229
230 basename = os.path.basename(filename)
231 cleaned = os.path.splitext(basename)[0]
232 _, _, day_obs, seq_num = cleaned.split("_")
233
234 return cls(timeSamples=rawData['time'], currentSamples=rawData['current'],
235 day_obs=int(day_obs), seq_num=int(seq_num))
236
237 def integrate(self):
238 """Integrate the current.
239
240 Raises
241 ------
242 RuntimeError
243 Raised if the integration method is not known.
244 """
245 if self.integrationMethod == 'DIRECT_SUM':
246 return self.integrateDirectSum()
247 elif self.integrationMethod == 'TRIMMED_SUM':
248 return self.integrateTrimmedSum()
249 elif self.integrationMethod == 'CHARGE_SUM':
250 return self.integrateChargeSum()
251 else:
252 raise RuntimeError(f"Unknown integration method {self.integrationMethod}")
253
255 """Integrate points.
256
257 This uses numpy's trapezoidal integrator.
258
259 Returns
260 -------
261 sum : `float`
262 Total charge measured.
263 """
264 return np.trapz(self.currentSamples, x=self.timeSamples)
265
267 """Integrate points with a baseline level subtracted.
268
269 This uses numpy's trapezoidal integrator.
270
271 Returns
272 -------
273 sum : `float`
274 Total charge measured.
275
276 See Also
277 --------
278 lsst.eotask.gen3.eoPtc
279 """
280 currentThreshold = ((max(self.currentSamples) - min(self.currentSamples))/5.0
281 + min(self.currentSamples))
282 lowValueIndices = np.where(self.currentSamples < currentThreshold)
283 baseline = np.median(self.currentSamples[lowValueIndices])
284 return np.trapz(self.currentSamples - baseline, self.timeSamples)
285
287 """For this method, the values in .currentSamples are actually the
288 integrated charge values as measured by the ammeter for each
289 sampling interval. We need to do a baseline subtraction,
290 based on the charge values when the LED is off, then sum up
291 the corrected signals.
292
293 Returns
294 -------
295 sum : `float`
296 Total charge measured.
297 """
298 dt = self.timeSamples[1:] - self.timeSamples[:-1]
299 # The .currentSamples values are the current integrals over
300 # the interval preceding the current time stamp, so omit the
301 # first value.
302 charge = self.currentScale*self.currentSamples[1:]
303 # The current per interval to use for baseline subtraction
304 # without assuming all of the dt values are the same:
305 current = charge/dt
306 # To determine the baseline current level, exclude points with
307 # signal levels > 5% of the maximum (measured relative to the
308 # overall minimum), and extend that selection 2 entries on
309 # either side to avoid otherwise low-valued points that sample
310 # the signal ramp and which should not be included in the
311 # baseline estimate.
312 dy = np.max(current) - np.min(current)
313 signal, = np.where(current > dy/20. + np.min(current))
314 imin = signal[0] - 2
315 imax = signal[-1] + 2
316 bg = np.concatenate([np.arange(0, imin), np.arange(imax, len(current))])
317 bg_current = np.sum(charge[bg])/np.sum(dt[bg])
318 # Return the background-subtracted total charge.
319 return np.sum(charge - bg_current*dt)
int min
int max
updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition calibType.py:207
__init__(self, timeSamples=None, currentSamples=None, **kwargs)
Definition photodiode.py:63