Loading [MathJax]/extensions/tex2jax.js
LSST Applications g0fba68d861+05816baf74,g1ec0fe41b4+f536777771,g1fd858c14a+a9301854fb,g35bb328faa+fcb1d3bbc8,g4af146b050+a5c07d5b1d,g4d2262a081+6e5fcc2a4e,g53246c7159+fcb1d3bbc8,g56a49b3a55+9c12191793,g5a012ec0e7+3632fc3ff3,g60b5630c4e+ded28b650d,g67b6fd64d1+ed4b5058f4,g78460c75b0+2f9a1b4bcd,g786e29fd12+cf7ec2a62a,g8352419a5c+fcb1d3bbc8,g87b7deb4dc+7b42cf88bf,g8852436030+e5453db6e6,g89139ef638+ed4b5058f4,g8e3bb8577d+d38d73bdbd,g9125e01d80+fcb1d3bbc8,g94187f82dc+ded28b650d,g989de1cb63+ed4b5058f4,g9d31334357+ded28b650d,g9f33ca652e+50a8019d8c,gabe3b4be73+1e0a283bba,gabf8522325+fa80ff7197,gb1101e3267+d9fb1f8026,gb58c049af0+f03b321e39,gb665e3612d+2a0c9e9e84,gb89ab40317+ed4b5058f4,gcf25f946ba+e5453db6e6,gd6cbbdb0b4+bb83cc51f8,gdd1046aedd+ded28b650d,gde0f65d7ad+941d412827,ge278dab8ac+d65b3c2b70,ge410e46f29+ed4b5058f4,gf23fb2af72+b7cae620c0,gf5e32f922b+fcb1d3bbc8,gf67bdafdda+ed4b5058f4,w.2025.16
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
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
29from astropy.stats import sigma_clipped_stats
30
31from lsst.ip.isr import IsrCalib
32
33
35 """Independent current measurements from photodiode for linearity
36 calculations.
37
38 Parameters
39 ----------
40 timeSamples : `list` or `numpy.ndarray`
41 List of samples the photodiode was measured at.
42 currentSamples : `list` or `numpy.ndarray`
43 List of current measurements at each time sample.
44 log : `logging.Logger`, optional
45 Log to write messages to. If `None` a default logger will be used.
46 **kwargs :
47 Additional parameters. These will be passed to the parent
48 constructor with the exception of:
49
50 ``"integrationMethod"``
51 Name of the algorithm to use to integrate the current
52 samples. Allowed values are ``DIRECT_SUM``,
53 ``TRIMMED_SUM``, and ``CHARGE_SUM`` (`str`).
54 ``"currentScale"``
55 Scale factor to apply to the current samples for the
56 ``CHARGE_SUM`` integration method. A typical value
57 would be `-1`, to flip the sign of the integrated charge.
58 """
59
60 _OBSTYPE = 'PHOTODIODE'
61 _SCHEMA = 'Photodiode'
62 _VERSION = 1.0
63
64 def __init__(self, timeSamples=None, currentSamples=None, **kwargs):
65 if timeSamples is not None and currentSamples is not None:
66 if len(timeSamples) != len(currentSamples):
67 raise RuntimeError(f"Inconsitent vector lengths: {len(timeSamples)} vs {len(currentSamples)}")
68 else:
69 self.timeSamples = np.array(timeSamples).ravel()
70 self.currentSamples = np.array(currentSamples).ravel()
71 else:
72 self.timeSamples = np.array([]).ravel()
73 self.currentSamples = np.array([]).ravel()
74
75 super().__init__(**kwargs)
76
77 if 'integrationMethod' in kwargs:
78 self.integrationMethod = kwargs.pop('integrationMethod')
79 else:
80 self.integrationMethod = 'DIRECT_SUM'
81
82 if 'currentScale' in kwargs:
83 self.currentScale = kwargs.pop('currentScale')
84 else:
85 self.currentScale = 1.0
86
87 if 'day_obs' in kwargs:
88 self.updateMetadata(day_obs=kwargs['day_obs'])
89 if 'seq_num' in kwargs:
90 self.updateMetadata(seq_num=kwargs['seq_num'])
91
92 self.requiredAttributes.update(['timeSamples', 'currentSamples', 'integrationMethod'])
93
94 @classmethod
95 def fromDict(cls, dictionary):
96 """Construct a PhotodiodeCalib from a dictionary of properties.
97
98 Parameters
99 ----------
100 dictionary : `dict`
101 Dictionary of properties.
102
103 Returns
104 -------
105 calib : `lsst.ip.isr.PhotodiodeCalib`
106 Constructed photodiode data.
107
108 Raises
109 ------
110 RuntimeError
111 Raised if the supplied dictionary is for a different
112 calibration type.
113 """
114 calib = cls()
115
116 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
117 raise RuntimeError(f"Incorrect photodiode supplied. Expected {calib._OBSTYPE}, "
118 f"found {dictionary['metadata']['OBSTYPE']}")
119
120 calib.setMetadata(dictionary['metadata'])
121
122 calib.timeSamples = np.array(dictionary['timeSamples']).ravel()
123 calib.currentSamples = np.array(dictionary['currentSamples']).ravel()
124 calib.integrationMethod = dictionary.get('integrationMethod', "DIRECT_SUM")
125
126 calib.updateMetadata()
127 return calib
128
129 def toDict(self):
130 """Return a dictionary containing the photodiode properties.
131
132 The dictionary should be able to be round-tripped through.
133 `fromDict`.
134
135 Returns
136 -------
137 dictionary : `dict`
138 Dictionary of properties.
139 """
140 self.updateMetadata()
141
142 outDict = {}
143 outDict['metadata'] = self.getMetadata()
144
145 outDict['timeSamples'] = self.timeSamples.tolist()
146 outDict['currentSamples'] = self.currentSamples.tolist()
147
148 outDict['integrationMethod'] = self.integrationMethod
149
150 return outDict
151
152 @classmethod
153 def fromTable(cls, tableList):
154 """Construct calibration from a list of tables.
155
156 This method uses the `fromDict` method to create the
157 calibration after constructing an appropriate dictionary from
158 the input tables.
159
160 Parameters
161 ----------
162 tableList : `list` [`astropy.table.Table`]
163 List of tables to use to construct the crosstalk
164 calibration.
165
166 Returns
167 -------
168 calib : `lsst.ip.isr.PhotodiodeCalib`
169 The calibration defined in the tables.
170 """
171 dataTable = tableList[0]
172
173 metadata = dataTable.meta
174 inDict = {}
175 inDict['metadata'] = metadata
176 if 'OBSTYPE' not in metadata:
177 inDict['metadata']['OBSTYPE'] = cls._OBSTYPE
178 inDict['integrationMethod'] = metadata.pop('INTEGRATION_METHOD', 'DIRECT_SUM')
179
180 for key in ('TIME', 'Elapsed Time', ):
181 if key in dataTable.columns:
182 inDict['timeSamples'] = dataTable[key]
183 for key in ('CURRENT', 'Signal', ):
184 if key in dataTable.columns:
185 inDict['currentSamples'] = dataTable[key]
186
187 return cls().fromDict(inDict)
188
189 def toTable(self):
190 """Construct a list of tables containing the information in this
191 calibration.
192
193 The list of tables should create an identical calibration
194 after being passed to this class's fromTable method.
195
196 Returns
197 -------
198 tableList : `list` [`astropy.table.Table`]
199 List of tables containing the photodiode calibration
200 information.
201 """
202 self.updateMetadata()
203 catalog = Table([{'TIME': self.timeSamples,
204 'CURRENT': self.currentSamples}])
205 inMeta = self.getMetadata().toDict()
206 outMeta = {k: v for k, v in inMeta.items() if v is not None}
207 outMeta.update({k: "" for k, v in inMeta.items() if v is None})
208 outMeta['INTEGRATION_METHOD'] = self.integrationMethod
209 catalog.meta = outMeta
210
211 return [catalog]
212
213 @classmethod
214 def readTwoColumnPhotodiodeData(cls, filename):
215 """Construct a PhotodiodeCalib by reading the simple column format.
216
217 Parameters
218 ----------
219 filename : `str`
220 File to read samples from.
221
222 Returns
223 -------
224 calib : `lsst.ip.isr.PhotodiodeCalib`
225 The calibration defined in the file.
226 """
227 import os.path
228
229 rawData = np.loadtxt(filename, dtype=[('time', 'float'), ('current', 'float')])
230
231 basename = os.path.basename(filename)
232 cleaned = os.path.splitext(basename)[0]
233 _, _, day_obs, seq_num = cleaned.split("_")
234
235 return cls(timeSamples=rawData['time'], currentSamples=rawData['current'],
236 day_obs=int(day_obs), seq_num=int(seq_num))
237
238 def integrate(self):
239 """Integrate the current.
240
241 Raises
242 ------
243 RuntimeError
244 Raised if the integration method is not known.
245 """
246 if self.integrationMethod == 'DIRECT_SUM':
247 return self.integrateDirectSum()
248 elif self.integrationMethod == 'TRIMMED_SUM':
249 return self.integrateTrimmedSum()
250 elif self.integrationMethod == 'CHARGE_SUM':
251 return self.integrateChargeSum()
252 else:
253 raise RuntimeError(f"Unknown integration method {self.integrationMethod}")
254
256 """Integrate points.
257
258 This uses numpy's trapezoidal integrator.
259
260 Returns
261 -------
262 sum : `float`
263 Total charge measured.
264 """
265 return np.trapezoid(self.currentSamples, x=self.timeSamples)
266
268 """Integrate points with a baseline level subtracted.
269
270 This uses numpy's trapezoidal integrator.
271
272 Returns
273 -------
274 sum : `float`
275 Total charge measured.
276
277 See Also
278 --------
279 lsst.eotask.gen3.eoPtc
280 """
281 # Apply the current scale to pick up any sign flip in the
282 # current sample values.
283 cs = self.currentScale * self.currentSamples
284 currentThreshold = (max(cs) - min(cs))/5.0 + min(cs)
285 lowValueIndices = np.where(cs < currentThreshold)
286 baseline = sigma_clipped_stats(cs[lowValueIndices])[0]
287 return np.trapezoid(cs - baseline, self.timeSamples)
288
290 """For this method, the values in .currentSamples are actually the
291 integrated charge values as measured by the ammeter for each
292 sampling interval. We need to do a baseline subtraction,
293 based on the charge values when the LED is off, then sum up
294 the corrected signals.
295
296 Returns
297 -------
298 sum : `float`
299 Total charge measured.
300 """
301 dt = self.timeSamples[1:] - self.timeSamples[:-1]
302 # The .currentSamples values are the current integrals over
303 # the interval preceding the current time stamp, so omit the
304 # first value.
305 charge = self.currentScale*self.currentSamples[1:]
306 # The current per interval to use for baseline subtraction
307 # without assuming all of the dt values are the same:
308 current = charge/dt
309 # To determine the baseline current level, exclude points with
310 # signal levels > 5% of the maximum (measured relative to the
311 # overall minimum), and extend that selection 2 entries on
312 # either side to avoid otherwise low-valued points that sample
313 # the signal ramp and which should not be included in the
314 # baseline estimate.
315 dy = np.max(current) - np.min(current)
316 signal, = np.where(current > dy/20. + np.min(current))
317 imin = signal[0] - 2
318 imax = signal[-1] + 2
319 bg = np.concatenate([np.arange(0, imin), np.arange(imax, len(current))])
320 bg_current = np.sum(charge[bg])/np.sum(dt[bg])
321 # Return the background-subtracted total charge.
322 return np.sum(charge - bg_current*dt)
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:64