dkist-processing-cryonirsp 1.3.4__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.
Potentially problematic release.
This version of dkist-processing-cryonirsp might be problematic. Click here for more details.
- changelog/.gitempty +0 -0
- dkist_processing_cryonirsp/__init__.py +11 -0
- dkist_processing_cryonirsp/config.py +12 -0
- dkist_processing_cryonirsp/models/__init__.py +1 -0
- dkist_processing_cryonirsp/models/constants.py +248 -0
- dkist_processing_cryonirsp/models/exposure_conditions.py +26 -0
- dkist_processing_cryonirsp/models/parameters.py +296 -0
- dkist_processing_cryonirsp/models/tags.py +168 -0
- dkist_processing_cryonirsp/models/task_name.py +14 -0
- dkist_processing_cryonirsp/parsers/__init__.py +1 -0
- dkist_processing_cryonirsp/parsers/cryonirsp_l0_fits_access.py +111 -0
- dkist_processing_cryonirsp/parsers/cryonirsp_l1_fits_access.py +30 -0
- dkist_processing_cryonirsp/parsers/exposure_conditions.py +163 -0
- dkist_processing_cryonirsp/parsers/map_repeats.py +40 -0
- dkist_processing_cryonirsp/parsers/measurements.py +55 -0
- dkist_processing_cryonirsp/parsers/modstates.py +31 -0
- dkist_processing_cryonirsp/parsers/optical_density_filters.py +40 -0
- dkist_processing_cryonirsp/parsers/polarimetric_check.py +120 -0
- dkist_processing_cryonirsp/parsers/scan_step.py +412 -0
- dkist_processing_cryonirsp/parsers/time.py +80 -0
- dkist_processing_cryonirsp/parsers/wavelength.py +26 -0
- dkist_processing_cryonirsp/tasks/__init__.py +19 -0
- dkist_processing_cryonirsp/tasks/assemble_movie.py +202 -0
- dkist_processing_cryonirsp/tasks/bad_pixel_map.py +96 -0
- dkist_processing_cryonirsp/tasks/beam_boundaries_base.py +279 -0
- dkist_processing_cryonirsp/tasks/ci_beam_boundaries.py +55 -0
- dkist_processing_cryonirsp/tasks/ci_science.py +169 -0
- dkist_processing_cryonirsp/tasks/cryonirsp_base.py +67 -0
- dkist_processing_cryonirsp/tasks/dark.py +98 -0
- dkist_processing_cryonirsp/tasks/gain.py +251 -0
- dkist_processing_cryonirsp/tasks/instrument_polarization.py +447 -0
- dkist_processing_cryonirsp/tasks/l1_output_data.py +44 -0
- dkist_processing_cryonirsp/tasks/linearity_correction.py +582 -0
- dkist_processing_cryonirsp/tasks/make_movie_frames.py +302 -0
- dkist_processing_cryonirsp/tasks/mixin/__init__.py +1 -0
- dkist_processing_cryonirsp/tasks/mixin/beam_access.py +52 -0
- dkist_processing_cryonirsp/tasks/mixin/corrections.py +177 -0
- dkist_processing_cryonirsp/tasks/mixin/intermediate_frame.py +193 -0
- dkist_processing_cryonirsp/tasks/mixin/linearized_frame.py +309 -0
- dkist_processing_cryonirsp/tasks/mixin/shift_measurements.py +297 -0
- dkist_processing_cryonirsp/tasks/parse.py +281 -0
- dkist_processing_cryonirsp/tasks/quality_metrics.py +271 -0
- dkist_processing_cryonirsp/tasks/science_base.py +511 -0
- dkist_processing_cryonirsp/tasks/sp_beam_boundaries.py +270 -0
- dkist_processing_cryonirsp/tasks/sp_dispersion_axis_correction.py +484 -0
- dkist_processing_cryonirsp/tasks/sp_geometric.py +585 -0
- dkist_processing_cryonirsp/tasks/sp_science.py +299 -0
- dkist_processing_cryonirsp/tasks/sp_solar_gain.py +475 -0
- dkist_processing_cryonirsp/tasks/trial_output_data.py +61 -0
- dkist_processing_cryonirsp/tasks/write_l1.py +1033 -0
- dkist_processing_cryonirsp/tests/__init__.py +1 -0
- dkist_processing_cryonirsp/tests/conftest.py +456 -0
- dkist_processing_cryonirsp/tests/header_models.py +592 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/__init__.py +0 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/l0_cals_only.py +541 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/l0_to_l1.py +615 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/linearize_only.py +96 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/local_trial_helpers.py +592 -0
- dkist_processing_cryonirsp/tests/test_assemble_movie.py +144 -0
- dkist_processing_cryonirsp/tests/test_assemble_qualilty.py +517 -0
- dkist_processing_cryonirsp/tests/test_bad_pixel_maps.py +115 -0
- dkist_processing_cryonirsp/tests/test_ci_beam_boundaries.py +106 -0
- dkist_processing_cryonirsp/tests/test_ci_science.py +355 -0
- dkist_processing_cryonirsp/tests/test_corrections.py +126 -0
- dkist_processing_cryonirsp/tests/test_cryo_base.py +202 -0
- dkist_processing_cryonirsp/tests/test_cryo_constants.py +76 -0
- dkist_processing_cryonirsp/tests/test_dark.py +287 -0
- dkist_processing_cryonirsp/tests/test_gain.py +278 -0
- dkist_processing_cryonirsp/tests/test_instrument_polarization.py +531 -0
- dkist_processing_cryonirsp/tests/test_linearity_correction.py +245 -0
- dkist_processing_cryonirsp/tests/test_make_movie_frames.py +111 -0
- dkist_processing_cryonirsp/tests/test_parameters.py +266 -0
- dkist_processing_cryonirsp/tests/test_parse.py +1439 -0
- dkist_processing_cryonirsp/tests/test_quality.py +203 -0
- dkist_processing_cryonirsp/tests/test_sp_beam_boundaries.py +112 -0
- dkist_processing_cryonirsp/tests/test_sp_dispersion_axis_correction.py +155 -0
- dkist_processing_cryonirsp/tests/test_sp_geometric.py +319 -0
- dkist_processing_cryonirsp/tests/test_sp_make_movie_frames.py +121 -0
- dkist_processing_cryonirsp/tests/test_sp_science.py +483 -0
- dkist_processing_cryonirsp/tests/test_sp_solar.py +198 -0
- dkist_processing_cryonirsp/tests/test_trial_create_quality_report.py +79 -0
- dkist_processing_cryonirsp/tests/test_trial_output_data.py +251 -0
- dkist_processing_cryonirsp/tests/test_workflows.py +9 -0
- dkist_processing_cryonirsp/tests/test_write_l1.py +436 -0
- dkist_processing_cryonirsp/workflows/__init__.py +2 -0
- dkist_processing_cryonirsp/workflows/ci_l0_processing.py +77 -0
- dkist_processing_cryonirsp/workflows/sp_l0_processing.py +84 -0
- dkist_processing_cryonirsp/workflows/trial_workflows.py +190 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/METADATA +194 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/RECORD +111 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/WHEEL +5 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/top_level.txt +4 -0
- docs/Makefile +134 -0
- docs/bad_pixel_calibration.rst +47 -0
- docs/beam_angle_calculation.rst +53 -0
- docs/beam_boundary_computation.rst +88 -0
- docs/changelog.rst +7 -0
- docs/ci_science_calibration.rst +33 -0
- docs/conf.py +52 -0
- docs/index.rst +21 -0
- docs/l0_to_l1_cryonirsp_ci-full-trial.rst +10 -0
- docs/l0_to_l1_cryonirsp_ci.rst +10 -0
- docs/l0_to_l1_cryonirsp_sp-full-trial.rst +10 -0
- docs/l0_to_l1_cryonirsp_sp.rst +10 -0
- docs/linearization.rst +43 -0
- docs/make.bat +170 -0
- docs/requirements.txt +1 -0
- docs/requirements_table.rst +8 -0
- docs/scientific_changelog.rst +10 -0
- docs/sp_science_calibration.rst +59 -0
- licenses/LICENSE.rst +11 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Cryonirsp quality metrics task."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import field
|
|
4
|
+
from typing import Generator
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from astropy.time import Time
|
|
9
|
+
from dkist_processing_common.codecs.fits import fits_access_decoder
|
|
10
|
+
from dkist_processing_common.parsers.quality import L1QualityFitsAccess
|
|
11
|
+
from dkist_processing_common.tasks import QualityL0Metrics
|
|
12
|
+
from dkist_processing_common.tasks.mixin.quality import QualityMixin
|
|
13
|
+
from dkist_service_configuration.logging import logger
|
|
14
|
+
|
|
15
|
+
from dkist_processing_cryonirsp.models.constants import CryonirspConstants
|
|
16
|
+
from dkist_processing_cryonirsp.models.tags import CryonirspTag
|
|
17
|
+
from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
|
|
18
|
+
|
|
19
|
+
__all__ = ["CryonirspL0QualityMetrics", "CryonirspL1QualityMetrics"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class _QualityDataPoint:
|
|
24
|
+
"""Class for storage of a single Cryonirsp quality data point in a time series."""
|
|
25
|
+
|
|
26
|
+
datetime: str | int # isot | mjd
|
|
27
|
+
value: float
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class _QualityData:
|
|
32
|
+
"""Class for storage of Cryonirsp time series quality data."""
|
|
33
|
+
|
|
34
|
+
data_points: list[_QualityDataPoint] = field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def datetimes(self) -> list[str | int]:
|
|
38
|
+
"""Parse datetimes from list of data points."""
|
|
39
|
+
return [dp.datetime for dp in self.data_points]
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def values(self) -> list[float]:
|
|
43
|
+
"""Parse values from list of data points."""
|
|
44
|
+
return [dp.value for dp in self.data_points]
|
|
45
|
+
|
|
46
|
+
def __len__(self):
|
|
47
|
+
return len(self.data_points)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CryonirspL0QualityMetrics(QualityL0Metrics):
|
|
51
|
+
"""
|
|
52
|
+
Task class for collection of Cryonirsp L0 specific quality metrics.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
recipe_run_id : int
|
|
57
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
58
|
+
workflow_name : str
|
|
59
|
+
name of the workflow to which this instance of the task belongs
|
|
60
|
+
workflow_version : str
|
|
61
|
+
version of the workflow to which this instance of the task belongs
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def constants_model_class(self):
|
|
67
|
+
"""Class for Cryonirsp constants."""
|
|
68
|
+
return CryonirspConstants
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def raw_frame_tag(self) -> str:
|
|
72
|
+
"""
|
|
73
|
+
Define tag corresponding to L0 data.
|
|
74
|
+
|
|
75
|
+
For Cryo it's LINEARIZED.
|
|
76
|
+
"""
|
|
77
|
+
return CryonirspTag.linearized()
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def modstate_list(self) -> Iterable[int] | None:
|
|
81
|
+
"""
|
|
82
|
+
Define the list of modstates over which to compute L0 quality metrics.
|
|
83
|
+
|
|
84
|
+
If the dataset is non-polarimetric then we just compute all metrics over all modstates at once.
|
|
85
|
+
"""
|
|
86
|
+
if self.constants.correct_for_polarization:
|
|
87
|
+
return range(1, self.constants.num_modstates + 1)
|
|
88
|
+
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class CryonirspL1QualityMetrics(CryonirspTaskBase, QualityMixin):
|
|
93
|
+
"""
|
|
94
|
+
Task class for collection of Cryonirsp L1 specific quality metrics.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
recipe_run_id : int
|
|
99
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
100
|
+
workflow_name : str
|
|
101
|
+
name of the workflow to which this instance of the task belongs
|
|
102
|
+
workflow_version : str
|
|
103
|
+
version of the workflow to which this instance of the task belongs
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def run(self) -> None:
|
|
108
|
+
"""Calculate sensitivity and noise quality metrics."""
|
|
109
|
+
if self.constants.correct_for_polarization:
|
|
110
|
+
with self.apm_processing_step(
|
|
111
|
+
"Calculating L1 Sensitivity metrics for all stokes states"
|
|
112
|
+
):
|
|
113
|
+
self.compute_full_stokes_sensitivity()
|
|
114
|
+
with self.apm_task_step("Calculating L1 Cryonirsp noise metrics for all stokes states"):
|
|
115
|
+
self.compute_full_stokes_noise()
|
|
116
|
+
else:
|
|
117
|
+
with self.apm_processing_step("Calculating L1 Sensitivity metrics for intensity only"):
|
|
118
|
+
self.compute_intensity_only_sensitivity()
|
|
119
|
+
with self.apm_task_step("Calculating L1 Cryonirsp noise metrics for intensity only"):
|
|
120
|
+
self.compute_intensity_only_noise()
|
|
121
|
+
|
|
122
|
+
def compute_full_stokes_sensitivity(self):
|
|
123
|
+
"""Compute the sensitivities of each map scan for each stokes state."""
|
|
124
|
+
for stokes_state in self.constants.stokes_params:
|
|
125
|
+
with self.apm_processing_step(f"Calculating sensitivity for stokes = {stokes_state}"):
|
|
126
|
+
quality_data = self.calculate_sensitivity_for_stokes_state(
|
|
127
|
+
stokes_state=stokes_state
|
|
128
|
+
)
|
|
129
|
+
with self.apm_writing_step(f"Writing sensitivity data for stokes = {stokes_state}"):
|
|
130
|
+
self.quality_store_sensitivity(
|
|
131
|
+
stokes=stokes_state,
|
|
132
|
+
datetimes=quality_data.datetimes,
|
|
133
|
+
values=quality_data.values,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def compute_intensity_only_sensitivity(self):
|
|
137
|
+
"""Compute the sensitivities of each map scan for the intensity stokes state only."""
|
|
138
|
+
with self.apm_processing_step(f"Calculating sensitivity for intensity only"):
|
|
139
|
+
quality_data = self.calculate_sensitivity_for_stokes_state(stokes_state="I")
|
|
140
|
+
with self.apm_writing_step("Writing sensitivity data for intensity only"):
|
|
141
|
+
self.quality_store_sensitivity(
|
|
142
|
+
stokes="I", datetimes=quality_data.datetimes, values=quality_data.values
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def compute_full_stokes_noise(self):
|
|
146
|
+
"""Compute noise in data broken down by each stokes state."""
|
|
147
|
+
for stokes in self.constants.stokes_params:
|
|
148
|
+
with self.apm_processing_step(f"Compile noise values for {stokes=}"):
|
|
149
|
+
noise_data = self.compile_noise_data(stokes=stokes)
|
|
150
|
+
with self.apm_writing_step(f"Write noise values for {stokes=}"):
|
|
151
|
+
self.quality_store_noise(
|
|
152
|
+
datetimes=noise_data.datetimes, values=noise_data.values, stokes=stokes
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def compute_intensity_only_noise(self):
|
|
156
|
+
"""Compute noise in data for the intensity stokes state only."""
|
|
157
|
+
stokes = "I"
|
|
158
|
+
with self.apm_processing_step(f"Compile noise values for {stokes=}"):
|
|
159
|
+
noise_data = self.compile_noise_data(stokes=stokes)
|
|
160
|
+
with self.apm_writing_step(f"Write noise values for {stokes=}"):
|
|
161
|
+
self.quality_store_noise(
|
|
162
|
+
datetimes=noise_data.datetimes, values=noise_data.values, stokes=stokes
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def calculate_sensitivity_for_stokes_state(self, stokes_state: str) -> _QualityData:
|
|
166
|
+
"""Calculate the sensitivities of each map scan for a given stokes state."""
|
|
167
|
+
stokes_sensitivities = _QualityData()
|
|
168
|
+
for map_scan in range(1, self.constants.num_map_scans + 1):
|
|
169
|
+
map_scan_sensitivity_data_point = self.calculate_sensitivity_for_map_scan(
|
|
170
|
+
map_scan=map_scan,
|
|
171
|
+
meas_num=1,
|
|
172
|
+
stokes_state=stokes_state,
|
|
173
|
+
)
|
|
174
|
+
stokes_sensitivities.data_points.append(map_scan_sensitivity_data_point)
|
|
175
|
+
logger.info(
|
|
176
|
+
f"Calculated {len(stokes_sensitivities)} stokes state sensitivities for {stokes_state=}"
|
|
177
|
+
)
|
|
178
|
+
return stokes_sensitivities
|
|
179
|
+
|
|
180
|
+
def calculate_sensitivity_for_map_scan(
|
|
181
|
+
self, map_scan: int, meas_num: int, stokes_state: str
|
|
182
|
+
) -> _QualityDataPoint:
|
|
183
|
+
"""Calculate the sensitivity as the standard deviation of a frame divided by median intensity frame averaged over the scan steps for a single map scan."""
|
|
184
|
+
scan_step_sensitivities = self.calculate_sensitivities_per_scan_step(
|
|
185
|
+
map_scan=map_scan, meas_num=meas_num, stokes_state=stokes_state
|
|
186
|
+
)
|
|
187
|
+
map_scan_sensitivity = np.mean(scan_step_sensitivities.values)
|
|
188
|
+
map_scan_average_date = Time(np.mean(scan_step_sensitivities.datetimes), format="mjd").isot
|
|
189
|
+
result = _QualityDataPoint(value=map_scan_sensitivity, datetime=map_scan_average_date)
|
|
190
|
+
logger.info(
|
|
191
|
+
f"Calculated map scan sensitivity for {map_scan=}, {meas_num=}, {stokes_state=}"
|
|
192
|
+
f" as {result} "
|
|
193
|
+
)
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
def calculate_sensitivities_per_scan_step(
|
|
197
|
+
self, map_scan: int, meas_num: int, stokes_state: str
|
|
198
|
+
) -> _QualityData:
|
|
199
|
+
"""Calculate the sensitivities for each scan step in a map scan."""
|
|
200
|
+
scan_step_sensitivities = _QualityData()
|
|
201
|
+
for step in range(1, self.constants.num_scan_steps + 1):
|
|
202
|
+
avg_stokes_i_data_point = self.get_intensity_frame_average(
|
|
203
|
+
map_scan=map_scan, step=step, meas_num=1
|
|
204
|
+
)
|
|
205
|
+
frame = next(
|
|
206
|
+
self.read(
|
|
207
|
+
tags=[
|
|
208
|
+
CryonirspTag.calibrated(),
|
|
209
|
+
CryonirspTag.frame(),
|
|
210
|
+
CryonirspTag.scan_step(step),
|
|
211
|
+
CryonirspTag.map_scan(map_scan),
|
|
212
|
+
CryonirspTag.stokes(stokes_state),
|
|
213
|
+
CryonirspTag.meas_num(meas_num),
|
|
214
|
+
],
|
|
215
|
+
decoder=fits_access_decoder,
|
|
216
|
+
fits_access_class=L1QualityFitsAccess,
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
scan_step_sensitivity = np.nanstd(frame.data) / avg_stokes_i_data_point.value
|
|
220
|
+
scan_step_sensitivity_data_point = _QualityDataPoint(
|
|
221
|
+
value=scan_step_sensitivity, datetime=avg_stokes_i_data_point.datetime
|
|
222
|
+
)
|
|
223
|
+
scan_step_sensitivities.data_points.append(scan_step_sensitivity_data_point)
|
|
224
|
+
logger.info(
|
|
225
|
+
f"Calculated {len(scan_step_sensitivities)} scan step sensitivities for {map_scan=}, "
|
|
226
|
+
f"{meas_num=}, and {stokes_state=}"
|
|
227
|
+
)
|
|
228
|
+
return scan_step_sensitivities
|
|
229
|
+
|
|
230
|
+
def get_intensity_frame_average(
|
|
231
|
+
self, map_scan: int, step: int, meas_num: int
|
|
232
|
+
) -> _QualityDataPoint:
|
|
233
|
+
"""Calculate the average of an intensity frame."""
|
|
234
|
+
frame: L1QualityFitsAccess = next(
|
|
235
|
+
self.read(
|
|
236
|
+
tags=[
|
|
237
|
+
CryonirspTag.calibrated(),
|
|
238
|
+
CryonirspTag.frame(),
|
|
239
|
+
CryonirspTag.scan_step(step),
|
|
240
|
+
CryonirspTag.map_scan(map_scan),
|
|
241
|
+
CryonirspTag.stokes("I"),
|
|
242
|
+
CryonirspTag.meas_num(meas_num),
|
|
243
|
+
],
|
|
244
|
+
decoder=fits_access_decoder,
|
|
245
|
+
fits_access_class=L1QualityFitsAccess,
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
median = np.nanmedian(frame.data)
|
|
249
|
+
time_obs_mjd = Time(frame.time_obs).mjd
|
|
250
|
+
mean = np.nanmean(frame.data)
|
|
251
|
+
average = median or mean
|
|
252
|
+
result = _QualityDataPoint(value=average, datetime=time_obs_mjd)
|
|
253
|
+
logger.info(f"Calculated intensity frame average as {result}")
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
def compile_noise_data(self, stokes: str) -> _QualityData:
|
|
257
|
+
"""Compile lists of noise values and their observation times."""
|
|
258
|
+
tags = [CryonirspTag.calibrated(), CryonirspTag.frame(), CryonirspTag.stokes(stokes)]
|
|
259
|
+
frames: Generator[L1QualityFitsAccess, None, None] = self.read(
|
|
260
|
+
tags=tags,
|
|
261
|
+
decoder=fits_access_decoder,
|
|
262
|
+
fits_access_class=L1QualityFitsAccess,
|
|
263
|
+
)
|
|
264
|
+
result = _QualityData()
|
|
265
|
+
logger.info(f"Compiling noise data for {tags = }")
|
|
266
|
+
for frame in frames:
|
|
267
|
+
data_point = _QualityDataPoint(
|
|
268
|
+
value=self.avg_noise(frame.data), datetime=frame.time_obs
|
|
269
|
+
)
|
|
270
|
+
result.data_points.append(data_point)
|
|
271
|
+
return result
|