chromparser 0.1.0__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.
- chromparser/__init__.py +183 -0
- chromparser/__main__.py +7 -0
- chromparser/_netcdf3.py +548 -0
- chromparser/_util.py +98 -0
- chromparser/cli.py +192 -0
- chromparser/errors.py +49 -0
- chromparser/hdf5.py +428 -0
- chromparser/model.py +254 -0
- chromparser/py.typed +0 -0
- chromparser/sniff.py +80 -0
- chromparser/vendors/__init__.py +55 -0
- chromparser/vendors/agilent/__init__.py +17 -0
- chromparser/vendors/agilent/ch.py +344 -0
- chromparser/vendors/agilent/dot_d.py +69 -0
- chromparser/vendors/agilent/dx.py +147 -0
- chromparser/vendors/andi/__init__.py +15 -0
- chromparser/vendors/andi/chrom.py +181 -0
- chromparser/vendors/andi/common.py +232 -0
- chromparser/vendors/andi/ms.py +358 -0
- chromparser/vendors/andi/write.py +320 -0
- chromparser/vendors/chromeleon/__init__.py +19 -0
- chromparser/vendors/chromeleon/archive.py +310 -0
- chromparser/vendors/chromeleon/raw.py +407 -0
- chromparser/vendors/chromeleon/sequence.py +177 -0
- chromparser/vendors/chromeleon/txt.py +319 -0
- chromparser/vendors/shimadzu/__init__.py +8 -0
- chromparser/vendors/shimadzu/gcd.py +146 -0
- chromparser/vendors/shimadzu/ole.py +84 -0
- chromparser/vendors/shimadzu/qgd.py +196 -0
- chromparser/vendors/thermo/__init__.py +7 -0
- chromparser/vendors/thermo/raw.py +377 -0
- chromparser-0.1.0.dist-info/METADATA +162 -0
- chromparser-0.1.0.dist-info/RECORD +36 -0
- chromparser-0.1.0.dist-info/WHEEL +4 -0
- chromparser-0.1.0.dist-info/entry_points.txt +3 -0
- chromparser-0.1.0.dist-info/licenses/LICENSE +51 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""ANDI chromatography (AIA) netCDF files: one detector trace.
|
|
2
|
+
|
|
3
|
+
The trace is `ordinate_values`, sampled every `actual_sampling_interval`
|
|
4
|
+
seconds from `actual_delay_time` on. A peak table (variables on the peak
|
|
5
|
+
dimension), if present, is kept in the signal metadata. See
|
|
6
|
+
`docs/formats/andi.md`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from ... import _netcdf3 as nc
|
|
16
|
+
from ...errors import CorruptFile, UnsupportedVariant, warn
|
|
17
|
+
from ...model import Injection, Signal
|
|
18
|
+
from .common import (
|
|
19
|
+
apply_scale,
|
|
20
|
+
check_variables,
|
|
21
|
+
has_variable,
|
|
22
|
+
injection_of,
|
|
23
|
+
json_value,
|
|
24
|
+
open_andi,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
KEY = "ordinate_values"
|
|
28
|
+
SCALARS = (
|
|
29
|
+
"actual_sampling_interval",
|
|
30
|
+
"actual_delay_time",
|
|
31
|
+
"actual_run_time_length",
|
|
32
|
+
"detector_maximum_value",
|
|
33
|
+
"detector_minimum_value",
|
|
34
|
+
)
|
|
35
|
+
#: Global attributes that describe the signal.
|
|
36
|
+
SIGNAL_ATTRIBUTES = {"detector_unit", "detector_name", "retention_unit"}
|
|
37
|
+
SECONDS = ("Seconds", "seconds", "s")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _scalar(f: nc.File, name: str) -> float | None:
|
|
41
|
+
if name not in f.variables:
|
|
42
|
+
return None
|
|
43
|
+
data = f.read(name).ravel()
|
|
44
|
+
if data.size != 1:
|
|
45
|
+
raise UnsupportedVariant(f"{f.path.name}: {name} holds {data.size} values.")
|
|
46
|
+
return float(data[0])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _peak_table(f: nc.File, dimension: str) -> dict[str, list]:
|
|
50
|
+
table = {}
|
|
51
|
+
for variable in f.variables.values():
|
|
52
|
+
if not variable.dimensions or variable.dimensions[0] != dimension:
|
|
53
|
+
continue
|
|
54
|
+
data = f.read(variable.name)
|
|
55
|
+
if variable.nc_type == 2:
|
|
56
|
+
values = nc.strings(data) if data.size else []
|
|
57
|
+
else:
|
|
58
|
+
values = [json_value(v) for v in data.tolist()]
|
|
59
|
+
table[variable.name] = values
|
|
60
|
+
return table
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def decode_andi_chrom(f: nc.File, file: str = "andi") -> tuple[Signal, Injection]:
|
|
64
|
+
"""Decode an open ANDI chromatography file.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
f : chromparser._netcdf3.File
|
|
69
|
+
The open file.
|
|
70
|
+
file : str
|
|
71
|
+
File name for messages.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
Signal, Injection
|
|
76
|
+
The signal (channel ``detector_name``, or ``"detector"``) and its
|
|
77
|
+
injection (without the signal).
|
|
78
|
+
|
|
79
|
+
Raises
|
|
80
|
+
------
|
|
81
|
+
UnsupportedVariant
|
|
82
|
+
For irregular sampling, times in other units than seconds, or
|
|
83
|
+
variables on the point dimension that the reader does not know.
|
|
84
|
+
CorruptFile
|
|
85
|
+
For unfinished files.
|
|
86
|
+
"""
|
|
87
|
+
attributes = f.attributes
|
|
88
|
+
metadata: dict = {}
|
|
89
|
+
variable = f.variables[KEY]
|
|
90
|
+
if len(variable.dimensions) != 1:
|
|
91
|
+
raise UnsupportedVariant(f"{file}: {KEY} is not 1D.")
|
|
92
|
+
peak_dim = "peak_number"
|
|
93
|
+
known = {KEY, *SCALARS}
|
|
94
|
+
known |= {v.name for v in f.variables.values() if v.dimensions[:1] == (peak_dim,)}
|
|
95
|
+
check_variables(f, known, {variable.dimensions[0]}, metadata, file)
|
|
96
|
+
|
|
97
|
+
uniform = json_value(variable.attributes.get("uniform_sampling_flag"))
|
|
98
|
+
if uniform not in (None, "Y"):
|
|
99
|
+
raise UnsupportedVariant(
|
|
100
|
+
f"{file}: uniform_sampling_flag {uniform!r}; irregularly sampled "
|
|
101
|
+
"signals are not supported."
|
|
102
|
+
)
|
|
103
|
+
unit = json_value(attributes.get("retention_unit"))
|
|
104
|
+
if unit is None:
|
|
105
|
+
warn(metadata, f"{file}: no retention_unit; seconds assumed.")
|
|
106
|
+
elif unit not in SECONDS:
|
|
107
|
+
raise UnsupportedVariant(f"{file}: retention_unit {unit!r} (only seconds).")
|
|
108
|
+
interval = _scalar(f, "actual_sampling_interval")
|
|
109
|
+
if interval is None or not interval > 0:
|
|
110
|
+
raise UnsupportedVariant(f"{file}: no positive actual_sampling_interval.")
|
|
111
|
+
delay = _scalar(f, "actual_delay_time")
|
|
112
|
+
if delay is None:
|
|
113
|
+
warn(metadata, f"{file}: no actual_delay_time; 0 assumed.")
|
|
114
|
+
delay = 0.0
|
|
115
|
+
if variable.is_record and f.numrecs == 0 and f.records_in_file():
|
|
116
|
+
raise CorruptFile(
|
|
117
|
+
f"{file}: the header says 0 records but the file holds "
|
|
118
|
+
f"{f.records_in_file()}; the export did not finish."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
values = apply_scale(f, KEY, f.read(KEY), metadata, "ordinate")
|
|
122
|
+
for name in SCALARS[2:]:
|
|
123
|
+
value = _scalar(f, name)
|
|
124
|
+
if value is not None:
|
|
125
|
+
metadata[f"andi_{name}"] = value
|
|
126
|
+
for key in ("uniform_sampling_flag", "autosampler_position"):
|
|
127
|
+
value = json_value(variable.attributes.get(key))
|
|
128
|
+
if value is not None:
|
|
129
|
+
metadata[f"andi_{key}"] = value
|
|
130
|
+
if peak_dim in f.dimensions:
|
|
131
|
+
table = _peak_table(f, peak_dim)
|
|
132
|
+
if table:
|
|
133
|
+
metadata["andi_peak_table"] = table
|
|
134
|
+
channel = json_value(attributes.get("detector_name")) or "detector"
|
|
135
|
+
signal = Signal(
|
|
136
|
+
channel=channel,
|
|
137
|
+
time=(delay + interval * np.arange(len(values))) / 60.0,
|
|
138
|
+
values=values,
|
|
139
|
+
unit=json_value(attributes.get("detector_unit")),
|
|
140
|
+
metadata=metadata,
|
|
141
|
+
sampling_interval=interval,
|
|
142
|
+
)
|
|
143
|
+
injection = injection_of(
|
|
144
|
+
f, file, "injection_date_time_stamp", SIGNAL_ATTRIBUTES.__contains__
|
|
145
|
+
)
|
|
146
|
+
return signal, injection
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def read_andi_chrom(path: str | Path) -> list[Injection]:
|
|
150
|
+
"""Read an ANDI chromatography (AIA) netCDF file.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
path : str or Path
|
|
155
|
+
The `.cdf` file.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
list of Injection
|
|
160
|
+
One injection with one signal. `inject_time` is the
|
|
161
|
+
``injection_date_time_stamp`` (aware when it has an offset).
|
|
162
|
+
|
|
163
|
+
Raises
|
|
164
|
+
------
|
|
165
|
+
UnsupportedFormat
|
|
166
|
+
If the file is not netCDF or has no ``ordinate_values``.
|
|
167
|
+
UnsupportedVariant
|
|
168
|
+
For netCDF-4 files and the variants `decode_andi_chrom` refuses.
|
|
169
|
+
CorruptFile
|
|
170
|
+
For unfinished files.
|
|
171
|
+
"""
|
|
172
|
+
path = Path(path)
|
|
173
|
+
with open_andi(path, KEY, "ANDI chromatography") as f:
|
|
174
|
+
signal, injection = decode_andi_chrom(f, path.name)
|
|
175
|
+
injection.signals[signal.channel] = signal
|
|
176
|
+
return [injection]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def sniff_andi_chrom(path: str | Path) -> bool:
|
|
180
|
+
"""Whether `path` is a netCDF file with an ``ordinate_values`` variable."""
|
|
181
|
+
return has_variable(path, KEY)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Helpers shared by the ANDI-MS and ANDI chromatography (AIA) readers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from ... import _netcdf3 as nc
|
|
15
|
+
from ..._util import existing_file, head
|
|
16
|
+
from ...errors import UnsupportedFormat, UnsupportedVariant, warn
|
|
17
|
+
from ...hdf5 import HDF5_MAGIC
|
|
18
|
+
from ...model import Injection
|
|
19
|
+
|
|
20
|
+
_TIMESTAMP = re.compile(r"^(\d{14})(?:([+-])(\d{2})(\d{2}))?$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_timestamp(text: str) -> datetime | None:
|
|
24
|
+
"""Parse an ANDI timestamp, ``YYYYMMDDHHMMSS`` with optional ``±ZZZZ``.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
datetime or None
|
|
29
|
+
Timezone-aware with an offset, naive without; `None` if `text` is not
|
|
30
|
+
such a timestamp.
|
|
31
|
+
"""
|
|
32
|
+
match = _TIMESTAMP.match(text.strip())
|
|
33
|
+
if not match:
|
|
34
|
+
return None
|
|
35
|
+
digits, sign, hours, minutes = match.groups()
|
|
36
|
+
try:
|
|
37
|
+
value = datetime.strptime(digits, "%Y%m%d%H%M%S")
|
|
38
|
+
except ValueError:
|
|
39
|
+
return None
|
|
40
|
+
if sign is None:
|
|
41
|
+
return value
|
|
42
|
+
offset = timedelta(hours=int(hours), minutes=int(minutes))
|
|
43
|
+
return value.replace(tzinfo=timezone(-offset if sign == "-" else offset))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def format_timestamp(value: datetime) -> str:
|
|
47
|
+
"""Format a datetime as an ANDI timestamp (offset only if it is aware)."""
|
|
48
|
+
text = value.strftime("%Y%m%d%H%M%S")
|
|
49
|
+
offset = value.utcoffset()
|
|
50
|
+
if offset is None:
|
|
51
|
+
return text
|
|
52
|
+
minutes = int(offset.total_seconds()) // 60
|
|
53
|
+
sign = "-" if minutes < 0 else "+"
|
|
54
|
+
return f"{text}{sign}{abs(minutes) // 60:02d}{abs(minutes) % 60:02d}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def json_value(value: Any) -> Any:
|
|
58
|
+
"""Return an attribute value as a JSON type (`None` for empty values)."""
|
|
59
|
+
if isinstance(value, str):
|
|
60
|
+
return value.strip() or None
|
|
61
|
+
if isinstance(value, np.ndarray):
|
|
62
|
+
return [json_value(v) for v in value.tolist()] or None
|
|
63
|
+
if isinstance(value, np.generic):
|
|
64
|
+
value = value.item()
|
|
65
|
+
if isinstance(value, float) and not np.isfinite(value):
|
|
66
|
+
return None
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def attribute_metadata(attributes: dict[str, Any]) -> dict[str, Any]:
|
|
71
|
+
"""Global attributes as ``andi_<name>`` metadata.
|
|
72
|
+
|
|
73
|
+
Timestamps become ISO 8601 strings; empty values are left out.
|
|
74
|
+
"""
|
|
75
|
+
metadata = {}
|
|
76
|
+
for key, value in attributes.items():
|
|
77
|
+
value = json_value(value)
|
|
78
|
+
if value is None:
|
|
79
|
+
continue
|
|
80
|
+
if key.endswith("date_time_stamp") and isinstance(value, str):
|
|
81
|
+
parsed = parse_timestamp(value)
|
|
82
|
+
value = parsed.isoformat() if parsed else value
|
|
83
|
+
metadata[f"andi_{key}"] = value
|
|
84
|
+
return metadata
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_fill(data: np.ndarray, attributes: dict[str, Any], nc_type: int) -> np.ndarray:
|
|
88
|
+
"""Mask of values equal to the variable's ``_FillValue`` or the default fill."""
|
|
89
|
+
fills = [nc.FILL[nc_type]]
|
|
90
|
+
if "_FillValue" in attributes:
|
|
91
|
+
fills.append(attributes["_FillValue"])
|
|
92
|
+
mask = np.zeros(data.shape, bool)
|
|
93
|
+
for fill in fills:
|
|
94
|
+
mask |= data == np.asarray(fill).astype(data.dtype)
|
|
95
|
+
return mask
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _netcdf4_variables(path: Path) -> set[str]:
|
|
99
|
+
"""Root dataset names of an HDF5 (netCDF-4) file; empty for other files."""
|
|
100
|
+
if head(path, 8) != HDF5_MAGIC:
|
|
101
|
+
return set()
|
|
102
|
+
import h5py
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
with h5py.File(path, "r") as hdf:
|
|
106
|
+
return set(hdf.keys())
|
|
107
|
+
except (OSError, ValueError):
|
|
108
|
+
return set()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def has_variable(path: str | Path, name: str) -> bool:
|
|
112
|
+
"""Whether `path` is a netCDF file (3 or 4) with the variable `name`."""
|
|
113
|
+
path = Path(path)
|
|
114
|
+
if not path.is_file():
|
|
115
|
+
return False
|
|
116
|
+
try:
|
|
117
|
+
if nc.is_netcdf3(head(path, 4)):
|
|
118
|
+
with nc.File(path) as f:
|
|
119
|
+
return name in f.variables
|
|
120
|
+
return name in _netcdf4_variables(path)
|
|
121
|
+
except (ValueError, OSError):
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@contextmanager
|
|
126
|
+
def open_andi(path: str | Path, key: str, what: str):
|
|
127
|
+
"""Open an ANDI file as a netCDF-3 `File`.
|
|
128
|
+
|
|
129
|
+
Raises
|
|
130
|
+
------
|
|
131
|
+
UnsupportedVariant
|
|
132
|
+
For a netCDF-4 (HDF5) file with `key`, or a CDF-5 file.
|
|
133
|
+
UnsupportedFormat
|
|
134
|
+
If the file is not netCDF-3 or has no variable `key`.
|
|
135
|
+
"""
|
|
136
|
+
path = existing_file(path)
|
|
137
|
+
if key in _netcdf4_variables(path):
|
|
138
|
+
raise UnsupportedVariant(
|
|
139
|
+
f"{path.name} is a netCDF-4 (HDF5) {what} file; only netCDF-3 "
|
|
140
|
+
"(classic and 64-bit offset) files are read."
|
|
141
|
+
)
|
|
142
|
+
with nc.File(path) as f:
|
|
143
|
+
if key not in f.variables:
|
|
144
|
+
raise UnsupportedFormat(f"{path.name} is not an {what} file (no {key}).")
|
|
145
|
+
yield f
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def check_variables(
|
|
149
|
+
f: nc.File, known: set[str], data_dims: set[str], metadata: dict, name: str
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Refuse unknown variables on the data dimensions, warn about others.
|
|
152
|
+
|
|
153
|
+
Raises
|
|
154
|
+
------
|
|
155
|
+
UnsupportedVariant
|
|
156
|
+
If a variable outside `known` uses one of `data_dims`.
|
|
157
|
+
"""
|
|
158
|
+
unknown = [v for v in f.variables.values() if v.name not in known]
|
|
159
|
+
on_data = [v.name for v in unknown if data_dims & set(v.dimensions)]
|
|
160
|
+
if on_data:
|
|
161
|
+
raise UnsupportedVariant(
|
|
162
|
+
f"{name}: variable(s) {', '.join(on_data)} on the data dimensions "
|
|
163
|
+
"are not understood by this reader."
|
|
164
|
+
)
|
|
165
|
+
if unknown:
|
|
166
|
+
warn(
|
|
167
|
+
metadata,
|
|
168
|
+
f"{name}: variable(s) not read: {', '.join(v.name for v in unknown)}.",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def injection_of(
|
|
173
|
+
f: nc.File, file: str, stamp_key: str, elsewhere: Callable[[str], bool]
|
|
174
|
+
) -> Injection:
|
|
175
|
+
"""Build the injection (without data) from the global attributes.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
f : chromparser._netcdf3.File
|
|
180
|
+
The open file.
|
|
181
|
+
file : str
|
|
182
|
+
File name for messages.
|
|
183
|
+
stamp_key : str
|
|
184
|
+
Attribute holding the injection time.
|
|
185
|
+
elsewhere : callable
|
|
186
|
+
Whether an attribute belongs to the signal or MS metadata instead.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
Injection
|
|
191
|
+
`name` from ``experiment_title`` or ``sample_name``; ``sample_name``
|
|
192
|
+
and ``operator`` (``operator_name``) metadata; the other attributes
|
|
193
|
+
as ``andi_<name>``.
|
|
194
|
+
"""
|
|
195
|
+
attributes = f.attributes
|
|
196
|
+
metadata: dict = {}
|
|
197
|
+
sample = json_value(attributes.get("sample_name"))
|
|
198
|
+
title = json_value(attributes.get("experiment_title"))
|
|
199
|
+
operator = json_value(attributes.get("operator_name"))
|
|
200
|
+
if sample:
|
|
201
|
+
metadata["sample_name"] = sample
|
|
202
|
+
if operator:
|
|
203
|
+
metadata["operator"] = operator
|
|
204
|
+
stamp = json_value(attributes.get(stamp_key))
|
|
205
|
+
inject_time = parse_timestamp(stamp) if isinstance(stamp, str) else None
|
|
206
|
+
if stamp is not None and inject_time is None:
|
|
207
|
+
warn(metadata, f"{file}: {stamp_key} {stamp!r} is not an ANDI timestamp.")
|
|
208
|
+
metadata[f"andi_{stamp_key}"] = stamp
|
|
209
|
+
mapped = {"experiment_title", "sample_name", "operator_name", stamp_key}
|
|
210
|
+
rest = {k: v for k, v in attributes.items() if k not in mapped and not elsewhere(k)}
|
|
211
|
+
metadata.update(attribute_metadata(rest))
|
|
212
|
+
return Injection(
|
|
213
|
+
name=title or sample or "", inject_time=inject_time, metadata=metadata
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def apply_scale(
|
|
218
|
+
f: nc.File, name: str, data: np.ndarray, metadata: dict, key: str
|
|
219
|
+
) -> np.ndarray:
|
|
220
|
+
"""Apply `scale_factor` / `add_offset` when they change the values."""
|
|
221
|
+
attributes = f.variables[name].attributes
|
|
222
|
+
scale = attributes.get("scale_factor")
|
|
223
|
+
offset = attributes.get("add_offset")
|
|
224
|
+
if scale is not None:
|
|
225
|
+
metadata[f"andi_{key}_scale_factor"] = json_value(scale)
|
|
226
|
+
if offset is not None:
|
|
227
|
+
metadata[f"andi_{key}_add_offset"] = json_value(offset)
|
|
228
|
+
scale = 1 if scale is None else float(np.asarray(scale).ravel()[0])
|
|
229
|
+
offset = 0 if offset is None else float(np.asarray(offset).ravel()[0])
|
|
230
|
+
if scale == 1 and offset == 0:
|
|
231
|
+
return data
|
|
232
|
+
return data.astype(np.float64) * scale + offset
|