dkist-processing-common 11.7.0rc3__py3-none-any.whl → 11.7.1rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. changelog/271.misc.rst +1 -0
  2. dkist_processing_common/models/constants.py +21 -357
  3. dkist_processing_common/models/fits_access.py +25 -16
  4. dkist_processing_common/parsers/experiment_id_bud.py +4 -8
  5. dkist_processing_common/parsers/id_bud.py +19 -35
  6. dkist_processing_common/parsers/l0_fits_access.py +3 -3
  7. dkist_processing_common/parsers/l1_fits_access.py +21 -47
  8. dkist_processing_common/parsers/near_bud.py +4 -4
  9. dkist_processing_common/parsers/proposal_id_bud.py +5 -11
  10. dkist_processing_common/parsers/single_value_single_key_flower.py +1 -0
  11. dkist_processing_common/parsers/time.py +27 -147
  12. dkist_processing_common/tasks/mixin/quality/_metrics.py +4 -6
  13. dkist_processing_common/tasks/parse_l0_input_data.py +1 -267
  14. dkist_processing_common/tests/test_fits_access.py +44 -19
  15. dkist_processing_common/tests/test_parse_l0_input_data.py +5 -39
  16. dkist_processing_common/tests/test_quality_mixin.py +11 -3
  17. dkist_processing_common/tests/test_stems.py +10 -127
  18. dkist_processing_common/tests/test_task_parsing.py +6 -6
  19. {dkist_processing_common-11.7.0rc3.dist-info → dkist_processing_common-11.7.1rc1.dist-info}/METADATA +2 -2
  20. {dkist_processing_common-11.7.0rc3.dist-info → dkist_processing_common-11.7.1rc1.dist-info}/RECORD +22 -28
  21. changelog/267.feature.1.rst +0 -1
  22. changelog/267.feature.2.rst +0 -1
  23. changelog/267.feature.rst +0 -1
  24. changelog/267.misc.rst +0 -1
  25. changelog/267.removal.1.rst +0 -2
  26. changelog/267.removal.rst +0 -1
  27. dkist_processing_common/parsers/average_bud.py +0 -48
  28. {dkist_processing_common-11.7.0rc3.dist-info → dkist_processing_common-11.7.1rc1.dist-info}/WHEEL +0 -0
  29. {dkist_processing_common-11.7.0rc3.dist-info → dkist_processing_common-11.7.1rc1.dist-info}/top_level.txt +0 -0
@@ -2,12 +2,9 @@
2
2
 
3
3
  from astropy.io import fits
4
4
 
5
- from dkist_processing_common.models.fits_access import HEADER_KEY_NOT_FOUND
6
5
  from dkist_processing_common.models.fits_access import FitsAccessBase
7
6
  from dkist_processing_common.models.fits_access import MetadataKey
8
7
 
9
- NOT_A_FLOAT = -999
10
-
11
8
 
12
9
  class L1FitsAccess(FitsAccessBase):
13
10
  """
@@ -31,48 +28,25 @@ class L1FitsAccess(FitsAccessBase):
31
28
  ):
32
29
  super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
33
30
 
34
- self.elevation: float = self.header[MetadataKey.elevation]
35
- self.azimuth: float = self.header[MetadataKey.azimuth]
36
- self.table_angle: float = self.header[MetadataKey.table_angle]
37
- self.gos_level3_status: str = self.header[MetadataKey.gos_level3_status]
38
- self.gos_level3_lamp_status: str = self.header[MetadataKey.gos_level3_lamp_status]
39
- self.gos_polarizer_status: str = self.header[MetadataKey.gos_polarizer_status]
40
- self.gos_retarder_status: str = self.header[MetadataKey.gos_retarder_status]
41
- self.gos_level0_status: str = self.header[MetadataKey.gos_level0_status]
42
- self.time_obs: str = self.header[MetadataKey.time_obs]
43
- self.ip_id: str = self.header[MetadataKey.ip_id]
44
- self.instrument: str = self.header[MetadataKey.instrument]
45
- self.wavelength: float = self.header[MetadataKey.wavelength]
46
- self.proposal_id: str = self.header[MetadataKey.proposal_id]
47
- self.experiment_id: str = self.header[MetadataKey.experiment_id]
48
- self.num_dsps_repeats: int = self.header[MetadataKey.num_dsps_repeats]
49
- self.current_dsps_repeat: int = self.header[MetadataKey.current_dsps_repeat]
50
- self.fpa_exposure_time_ms: float = self.header[MetadataKey.fpa_exposure_time_ms]
51
- self.sensor_readout_exposure_time_ms: float = self.header[
52
- MetadataKey.sensor_readout_exposure_time_ms
53
- ]
54
- self.num_raw_frames_per_fpa: int = self.header[MetadataKey.num_raw_frames_per_fpa]
55
- self.camera_id: str = self.header[MetadataKey.camera_id]
56
- self.camera_name: str = self.header[MetadataKey.camera_name]
57
- self.camera_bit_depth: int = self.header[MetadataKey.camera_bit_depth]
58
- self.hardware_binning_x: int = self.header[MetadataKey.hardware_binning_x]
59
- self.hardware_binning_y: int = self.header[MetadataKey.hardware_binning_y]
60
- self.software_binning_x: int = self.header[MetadataKey.software_binning_x]
61
- self.software_binning_y: int = self.header[MetadataKey.software_binning_y]
62
- self.observing_program_execution_id: str = self.header[
63
- MetadataKey.observing_program_execution_id
64
- ]
65
- self.telescope_tracking_mode: str = self.header.get(
66
- MetadataKey.telescope_tracking_mode, HEADER_KEY_NOT_FOUND
67
- )
68
- self.coude_table_tracking_mode: str = self.header.get(
69
- MetadataKey.coude_table_tracking_mode, HEADER_KEY_NOT_FOUND
70
- )
71
- self.telescope_scanning_mode: str = self.header.get(
72
- MetadataKey.telescope_scanning_mode, HEADER_KEY_NOT_FOUND
73
- )
74
- self.light_level: float = self.header[MetadataKey.light_level]
75
- self.hls_version: str = self.header[MetadataKey.hls_version]
31
+ self._set_metadata_key_value(MetadataKey.elevation)
32
+ self._set_metadata_key_value(MetadataKey.azimuth)
33
+ self._set_metadata_key_value(MetadataKey.table_angle)
34
+ self._set_metadata_key_value(MetadataKey.gos_level3_status)
35
+ self._set_metadata_key_value(MetadataKey.gos_level3_lamp_status)
36
+ self._set_metadata_key_value(MetadataKey.gos_polarizer_status)
37
+ self._set_metadata_key_value(MetadataKey.gos_retarder_status)
38
+ self._set_metadata_key_value(MetadataKey.gos_level0_status)
39
+ self._set_metadata_key_value(MetadataKey.time_obs)
40
+ self._set_metadata_key_value(MetadataKey.ip_id)
41
+ self._set_metadata_key_value(MetadataKey.instrument)
42
+ self._set_metadata_key_value(MetadataKey.wavelength)
43
+ self._set_metadata_key_value(MetadataKey.proposal_id)
44
+ self._set_metadata_key_value(MetadataKey.experiment_id)
45
+ self._set_metadata_key_value(MetadataKey.num_dsps_repeats)
46
+ self._set_metadata_key_value(MetadataKey.current_dsps_repeat)
47
+ self._set_metadata_key_value(MetadataKey.fpa_exposure_time_ms)
48
+ self._set_metadata_key_value(MetadataKey.sensor_readout_exposure_time_ms)
49
+ self._set_metadata_key_value(MetadataKey.num_raw_frames_per_fpa)
76
50
 
77
51
  @property
78
52
  def gos_polarizer_angle(self) -> float:
@@ -80,7 +54,7 @@ class L1FitsAccess(FitsAccessBase):
80
54
  try:
81
55
  return float(self.header[MetadataKey.gos_polarizer_angle])
82
56
  except ValueError:
83
- return NOT_A_FLOAT # The angle is only used if the polarizer is in the beam
57
+ return -999 # The angle is only used if the polarizer is in the beam
84
58
 
85
59
  @property
86
60
  def gos_retarder_angle(self) -> float:
@@ -88,4 +62,4 @@ class L1FitsAccess(FitsAccessBase):
88
62
  try:
89
63
  return float(self.header[MetadataKey.gos_retarder_angle])
90
64
  except ValueError:
91
- return NOT_A_FLOAT # The angle is only used if the retarder is in the beam
65
+ return -999 # The angle is only used if the retarder is in the beam
@@ -66,14 +66,14 @@ class NearFloatBud(Stem):
66
66
  -------
67
67
  The mean value associated with this input key
68
68
  """
69
- value_list = list(self.key_to_petal_dict.values())
70
- biggest_value = max(value_list)
71
- smallest_value = min(value_list)
69
+ value_set = list(self.key_to_petal_dict.values())
70
+ biggest_value = max(value_set)
71
+ smallest_value = min(value_set)
72
72
  if biggest_value - smallest_value > self.tolerance:
73
73
  raise ValueError(
74
74
  f"{self.stem_name} values are not close enough. Max: {biggest_value}, Min: {smallest_value}, Tolerance: {self.tolerance}"
75
75
  )
76
- return mean(value_list)
76
+ return mean(value_set)
77
77
 
78
78
 
79
79
  class TaskNearFloatBud(NearFloatBud):
@@ -2,27 +2,21 @@
2
2
 
3
3
  from dkist_processing_common.models.constants import BudName
4
4
  from dkist_processing_common.models.fits_access import MetadataKey
5
- from dkist_processing_common.models.task_name import TaskName
6
5
  from dkist_processing_common.parsers.id_bud import ContributingIdsBud
7
- from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
6
+ from dkist_processing_common.parsers.id_bud import IdBud
8
7
 
9
8
 
10
- class ProposalIdBud(TaskUniqueBud):
9
+ class ProposalIdBud(IdBud):
11
10
  """Class to create a Bud for the proposal_id."""
12
11
 
13
12
  def __init__(self):
14
- super().__init__(
15
- constant_name=BudName.proposal_id,
16
- metadata_key=MetadataKey.proposal_id,
17
- ip_task_types=TaskName.observe,
18
- )
13
+ super().__init__(constant_name=BudName.proposal_id, metadata_key=MetadataKey.proposal_id)
19
14
 
20
15
 
21
16
  class ContributingProposalIdsBud(ContributingIdsBud):
22
- """Class to create a Bud for the supporting proposal_ids."""
17
+ """Class to create a Bud for the proposal_ids."""
23
18
 
24
19
  def __init__(self):
25
20
  super().__init__(
26
- constant_name=BudName.contributing_proposal_ids,
27
- metadata_key=MetadataKey.proposal_id,
21
+ stem_name=BudName.contributing_proposal_ids, metadata_key=MetadataKey.proposal_id
28
22
  )
@@ -2,6 +2,7 @@
2
2
 
3
3
  from enum import StrEnum
4
4
 
5
+ from dkist_processing_common.models.fits_access import MetadataKey
5
6
  from dkist_processing_common.models.flower_pot import Stem
6
7
  from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
7
8
 
@@ -22,6 +22,7 @@ from dkist_processing_common.parsers.single_value_single_key_flower import (
22
22
  )
23
23
  from dkist_processing_common.parsers.task import passthrough_header_ip_task
24
24
  from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
25
+ from dkist_processing_common.parsers.unique_bud import UniqueBud
25
26
 
26
27
 
27
28
  class ObsIpStartTimeBud(TaskUniqueBud):
@@ -35,52 +36,15 @@ class ObsIpStartTimeBud(TaskUniqueBud):
35
36
  )
36
37
 
37
38
 
38
- class TaskDatetimeBudBase(Stem):
39
- """
40
- Base class for making datetime-related buds.
41
-
42
- Returns a tuple of sorted values converted from datetimes to unix seconds.
43
-
44
- Complicated parsing of the header into a task type can be achieved by passing in a different
45
- header task parsing function.
46
-
47
- Parameters
48
- ----------
49
- stem_name
50
- The name for the constant to be defined
51
-
52
- metadata_key
53
- The metadata key associated with the constant
54
-
55
- ip_task_types
56
- Only consider objects whose parsed header IP task type matches a string in this list
57
-
58
- header_type_parsing_func
59
- The function used to convert a header into an IP task type
60
- """
61
-
62
- key_to_petal_dict: dict[str, float]
63
-
64
- def __init__(
65
- self,
66
- stem_name: str,
67
- metadata_key: str | StrEnum,
68
- ip_task_types: str | list[str],
69
- task_type_parsing_function: Callable = passthrough_header_ip_task,
70
- ):
71
- super().__init__(stem_name=stem_name)
39
+ class CadenceBudBase(UniqueBud):
40
+ """Base class for all Cadence Buds."""
72
41
 
73
- if isinstance(metadata_key, StrEnum):
74
- metadata_key = metadata_key.name
75
- self.metadata_key = metadata_key
76
- if isinstance(ip_task_types, str):
77
- ip_task_types = [ip_task_types]
78
- self.ip_task_types = [task.casefold() for task in ip_task_types]
79
- self.header_parsing_function = task_type_parsing_function
42
+ def __init__(self, constant_name: str):
43
+ super().__init__(constant_name, metadata_key=MetadataKey.time_obs)
80
44
 
81
45
  def setter(self, fits_obj: L0FitsAccess) -> float | Type[SpilledDirt]:
82
46
  """
83
- Store the metadata key datetime value as unix seconds if the task type is in the desired types.
47
+ If the file is an observe file, its DATE-OBS value is stored as unix seconds.
84
48
 
85
49
  Parameters
86
50
  ----------
@@ -88,45 +52,16 @@ class TaskDatetimeBudBase(Stem):
88
52
  The input fits object
89
53
  Returns
90
54
  -------
91
- The datetime in seconds
55
+ The observe time in seconds
92
56
  """
93
- task = self.header_parsing_function(fits_obj)
94
-
95
- if task.casefold() in self.ip_task_types:
57
+ if fits_obj.ip_task_type.casefold() == TaskName.observe.value.casefold():
96
58
  return (
97
59
  datetime.fromisoformat(getattr(fits_obj, self.metadata_key))
98
60
  .replace(tzinfo=timezone.utc)
99
61
  .timestamp()
100
62
  )
101
-
102
63
  return SpilledDirt
103
64
 
104
- def getter(self, key: Hashable) -> tuple[float, ...]:
105
- """
106
- Return a tuple of sorted times in unix seconds.
107
-
108
- Parameters
109
- ----------
110
- key
111
- The input key
112
-
113
- Returns
114
- -------
115
- A tuple that is sorted times in unix seconds
116
- """
117
- return tuple(sorted(list(self.key_to_petal_dict.values())))
118
-
119
-
120
- class CadenceBudBase(TaskDatetimeBudBase):
121
- """Base class for all Cadence Buds."""
122
-
123
- def __init__(self, constant_name: str):
124
- super().__init__(
125
- stem_name=constant_name,
126
- metadata_key=MetadataKey.time_obs,
127
- ip_task_types=TaskName.observe,
128
- )
129
-
130
65
 
131
66
  class AverageCadenceBud(CadenceBudBase):
132
67
  """Class for the average cadence Bud."""
@@ -147,7 +82,7 @@ class AverageCadenceBud(CadenceBudBase):
147
82
  -------
148
83
  The mean value of the cadences of the input frames
149
84
  """
150
- return np.mean(np.diff(super().getter(key)))
85
+ return np.mean(np.diff(sorted(list(self.key_to_petal_dict.values()))))
151
86
 
152
87
 
153
88
  class MaximumCadenceBud(CadenceBudBase):
@@ -169,7 +104,7 @@ class MaximumCadenceBud(CadenceBudBase):
169
104
  -------
170
105
  The maximum cadence between frames
171
106
  """
172
- return np.max(np.diff(super().getter(key)))
107
+ return np.max(np.diff(sorted(list(self.key_to_petal_dict.values()))))
173
108
 
174
109
 
175
110
  class MinimumCadenceBud(CadenceBudBase):
@@ -191,7 +126,7 @@ class MinimumCadenceBud(CadenceBudBase):
191
126
  -------
192
127
  The minimum cadence between frames
193
128
  """
194
- return np.min(np.diff(super().getter(key)))
129
+ return np.min(np.diff(sorted(list(self.key_to_petal_dict.values()))))
195
130
 
196
131
 
197
132
  class VarianceCadenceBud(CadenceBudBase):
@@ -212,44 +147,11 @@ class VarianceCadenceBud(CadenceBudBase):
212
147
  -------
213
148
  Return the variance of the cadences over the input frames
214
149
  """
215
- return np.var(np.diff(super().getter(key)))
216
-
217
-
218
- class TaskDateBeginBud(TaskDatetimeBudBase):
219
- """Class for the date begin task Bud."""
220
-
221
- def __init__(
222
- self,
223
- constant_name: str,
224
- ip_task_types: str | list[str],
225
- task_type_parsing_function: Callable = passthrough_header_ip_task,
226
- ):
227
- super().__init__(
228
- stem_name=constant_name,
229
- metadata_key=MetadataKey.time_obs,
230
- ip_task_types=ip_task_types,
231
- task_type_parsing_function=task_type_parsing_function,
232
- )
233
-
234
- def getter(self, key) -> str:
235
- """
236
- Return the earliest date begin for the ip task type converted from unix seconds to datetime string.
237
-
238
- Parameters
239
- ----------
240
- key
241
- The input key
242
- Returns
243
- -------
244
- Return the minimum date begin as a datetime string
245
- """
246
- min_time = super().getter(key)[0]
247
- min_time_dt = datetime.fromtimestamp(min_time, tz=timezone.utc)
248
- return min_time_dt.strftime("%Y-%m-%dT%H:%M:%S.%f")
150
+ return np.var(np.diff(sorted(list(self.key_to_petal_dict.values()))))
249
151
 
250
152
 
251
- class RoundTimeFlowerBase(SingleValueSingleKeyFlower):
252
- """Base flower for SingleValueSingleKeyFlowers that need to round their values to avoid value jitter."""
153
+ class TimeFlowerBase(SingleValueSingleKeyFlower):
154
+ """Base task for SingleValueSingleKeyFlowers that need to round their values to avoid value jitter."""
253
155
 
254
156
  def setter(self, fits_obj: L0FitsAccess):
255
157
  """
@@ -267,7 +169,7 @@ class RoundTimeFlowerBase(SingleValueSingleKeyFlower):
267
169
  return round(raw_value, EXP_TIME_ROUND_DIGITS)
268
170
 
269
171
 
270
- class ExposureTimeFlower(RoundTimeFlowerBase):
172
+ class ExposureTimeFlower(TimeFlowerBase):
271
173
  """For tagging the frame FPA exposure time."""
272
174
 
273
175
  def __init__(self):
@@ -276,7 +178,7 @@ class ExposureTimeFlower(RoundTimeFlowerBase):
276
178
  )
277
179
 
278
180
 
279
- class ReadoutExpTimeFlower(RoundTimeFlowerBase):
181
+ class ReadoutExpTimeFlower(TimeFlowerBase):
280
182
  """For tagging the exposure time of each readout that contributes to an FPA."""
281
183
 
282
184
  def __init__(self):
@@ -286,18 +188,18 @@ class ReadoutExpTimeFlower(RoundTimeFlowerBase):
286
188
  )
287
189
 
288
190
 
289
- class TaskRoundTimeBudBase(Stem):
191
+ class TaskTimeBudBase(Stem):
290
192
  """
291
- Base class for making buds that need a set of rounded times for computing for specific task types.
193
+ Base class for making time-related buds that are computed for specific task types.
292
194
 
293
- Metadata key values are already floats. Returns tuple of sorted unique rounded values.
195
+ By "time-related" we mean values that generally need rounding when ingested into the database.
294
196
 
295
197
  Complicated parsing of the header into a task type can be achieved by passing in a different
296
198
  header task parsing function.
297
199
 
298
200
  Parameters
299
201
  ----------
300
- stem_name
202
+ constant_name
301
203
  The name for the constant to be defined
302
204
 
303
205
  metadata_key
@@ -310,8 +212,6 @@ class TaskRoundTimeBudBase(Stem):
310
212
  The function used to convert a header into an IP task type
311
213
  """
312
214
 
313
- key_to_petal_dict: dict[str, float]
314
-
315
215
  def __init__(
316
216
  self,
317
217
  stem_name: str,
@@ -329,18 +229,8 @@ class TaskRoundTimeBudBase(Stem):
329
229
  self.ip_task_types = [task.casefold() for task in ip_task_types]
330
230
  self.header_parsing_function = header_task_parsing_func
331
231
 
332
- def setter(self, fits_obj: L0FitsAccess) -> float | Type[SpilledDirt]:
333
- """
334
- Store the metadata key value if the parsed task type is in the desired types.
335
-
336
- Parameters
337
- ----------
338
- fits_obj
339
- The input fits object
340
- Returns
341
- -------
342
- The rounded time
343
- """
232
+ def setter(self, fits_obj: L0FitsAccess):
233
+ """Return the desired metadata key only if the parsed task type matches the Bud's task type."""
344
234
  task = self.header_parsing_function(fits_obj)
345
235
 
346
236
  if task.casefold() in self.ip_task_types:
@@ -350,22 +240,12 @@ class TaskRoundTimeBudBase(Stem):
350
240
  return SpilledDirt
351
241
 
352
242
  def getter(self, key: Hashable) -> tuple[float, ...]:
353
- """
354
- Return a tuple of the sorted unique values found.
355
-
356
- Parameters
357
- ----------
358
- key
359
- The input key
360
-
361
- Returns
362
- -------
363
- A tuple that is the sorted set of unique times
364
- """
365
- return tuple(sorted(set(self.key_to_petal_dict.values())))
243
+ """Return a tuple of all the unique values found."""
244
+ value_tuple = tuple(sorted(set(self.key_to_petal_dict.values())))
245
+ return value_tuple
366
246
 
367
247
 
368
- class TaskExposureTimesBud(TaskRoundTimeBudBase):
248
+ class TaskExposureTimesBud(TaskTimeBudBase):
369
249
  """Produce a tuple of all FPA exposure times present in the dataset for a specific ip task type."""
370
250
 
371
251
  def __init__(
@@ -382,7 +262,7 @@ class TaskExposureTimesBud(TaskRoundTimeBudBase):
382
262
  )
383
263
 
384
264
 
385
- class TaskReadoutExpTimesBud(TaskRoundTimeBudBase):
265
+ class TaskReadoutExpTimesBud(TaskTimeBudBase):
386
266
  """Produce a tuple of all sensor readout exposure times present in the dataset for a specific task type."""
387
267
 
388
268
  def __init__(
@@ -1356,15 +1356,13 @@ class _WavecalQualityMixin:
1356
1356
  Note that the residuals are the *unweighed* residuals.
1357
1357
  """
1358
1358
  weight_data = np.ones(input_wavelength.size) if weights is None else weights
1359
- prepared_weights = np.sqrt(weight_data / np.sum(weight_data))
1359
+ prepared_weights = fit_result.prepared_weights
1360
1360
  residuals = fit_result.minimizer_result.residual / prepared_weights
1361
1361
  residuals[~np.isfinite(residuals)] = 0.0
1362
- best_fit_atlas = input_spectrum - residuals
1363
1362
  normalized_residuals = residuals / input_spectrum
1364
1363
 
1365
- best_fit_header = fit_result.wavelength_parameters.to_header(axis_num=1)
1366
- wcs = WCS(best_fit_header)
1367
- best_fit_wavelength = wcs.spectral.pixel_to_world(np.arange(input_spectrum.size))
1364
+ best_fit_atlas = fit_result.best_fit_atlas
1365
+ best_fit_wavelength = fit_result.best_fit_wavelength_vector
1368
1366
 
1369
1367
  finite_idx = (
1370
1368
  np.isfinite(input_wavelength)
@@ -1378,7 +1376,7 @@ class _WavecalQualityMixin:
1378
1376
  data = {
1379
1377
  "input_wavelength_nm": input_wavelength.to_value(u.nm)[finite_idx].tolist(),
1380
1378
  "input_spectrum": input_spectrum[finite_idx].tolist(),
1381
- "best_fit_wavelength_nm": best_fit_wavelength.to_value(u.nm)[finite_idx].tolist(),
1379
+ "best_fit_wavelength_nm": best_fit_wavelength[finite_idx].tolist(),
1382
1380
  "best_fit_atlas": best_fit_atlas[finite_idx].tolist(),
1383
1381
  "normalized_residuals": normalized_residuals[finite_idx].tolist(),
1384
1382
  "weights": None if weights is None else weight_data[finite_idx].tolist(),