acspype 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.
- acspype/__init__.py +5 -0
- acspype/core.py +28 -0
- acspype/dev.py +229 -0
- acspype/discontinuity.py +142 -0
- acspype/experimental.py +64 -0
- acspype/processing.py +343 -0
- acspype/qaqc.py +221 -0
- acspype/stream.py +147 -0
- acspype/structures.py +57 -0
- acspype/tscor.py +79 -0
- acspype/utils/__init__.py +1 -0
- acspype/utils/core.py +42 -0
- acspype/utils/ooi.py +187 -0
- acspype-0.1.0.dist-info/METADATA +120 -0
- acspype-0.1.0.dist-info/RECORD +18 -0
- acspype-0.1.0.dist-info/WHEEL +5 -0
- acspype-0.1.0.dist-info/licenses/LICENSE +21 -0
- acspype-0.1.0.dist-info/top_level.txt +1 -0
acspype/__init__.py
ADDED
acspype/core.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
NUM_PAT = "[+-]?[0-9]*[.]?[0-9]+" # REGEX for any number, float or int, positive or negative.
|
|
4
|
+
|
|
5
|
+
PACKET_REGISTRATION = b'\xff\x00\xff\x00' # Start of every ACS packet.
|
|
6
|
+
PAD_BYTE = b'\x00' # End of every ACS packet.
|
|
7
|
+
WVL_BYTE_OFFSET = 4 + 2 + 1 + 1 + 1 + 3 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 4 + 1 # See Process Data section in ACS manual.
|
|
8
|
+
NUM_CHECKSUM_BYTES = 2
|
|
9
|
+
PACKET_HEAD = '!4cHBBl7HIBB' # struct descriptor for the static header of a packet.
|
|
10
|
+
PACKET_TAIL = 'Hx' # struct descriptor for the static tail of a packet.
|
|
11
|
+
LPR = len(PACKET_REGISTRATION)
|
|
12
|
+
|
|
13
|
+
class DefaultSerial:
|
|
14
|
+
BAUDRATE: int = 115200
|
|
15
|
+
BYTESIZE: int = 8
|
|
16
|
+
PARITY: str = 'N'
|
|
17
|
+
STOPBITS: int = 1
|
|
18
|
+
FLOWCONTROL: int = 0
|
|
19
|
+
TIMEOUT: int = 3
|
|
20
|
+
|
|
21
|
+
# Raw pressure counts are no longer output by an ACS and can be safely ignored. The reserved_1 and reserved_2 variables are single byte variables that are not used by the ACS and can be ignored.
|
|
22
|
+
ACS_VARS_TO_IGNORE = ['raw_pressure', 'reserved_1', 'reserved_2']
|
|
23
|
+
|
|
24
|
+
#---------- File Creation ----------#
|
|
25
|
+
ENCODING = {'time': {'units': 'nanoseconds since 1900-01-01'}} # xr.Dataset to netcdf encoding for time
|
|
26
|
+
|
|
27
|
+
#---------- PHYSICAL QUANTITIES ----------#
|
|
28
|
+
EST_FLOW_CELL_VOLUME = 30 # in mL, from the ACS Protocol Document, Rev Q.
|
acspype/dev.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import numpy as np
|
|
3
|
+
import re
|
|
4
|
+
from scipy import interpolate
|
|
5
|
+
import xarray as xr
|
|
6
|
+
|
|
7
|
+
from acspype.core import NUM_PAT
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ACSDev:
|
|
11
|
+
"""
|
|
12
|
+
A class for parsing ACS .dev files and putting them into a format that is easier to work with for larger or
|
|
13
|
+
multiple file datasets.
|
|
14
|
+
|
|
15
|
+
Generally, users will not call individual functions, but rather use the class to obtain attributes, which are
|
|
16
|
+
created at class instantiation or convert the data to an xarray dataset using the to_xarray function.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, filepath: str) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Run the following functions at instantiation to parse the .dev file and store the info as class attributes.
|
|
22
|
+
|
|
23
|
+
:param filepath: The filepath to the .dev file.
|
|
24
|
+
:return: None
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
self._filepath = filepath
|
|
28
|
+
self.__read_dev()
|
|
29
|
+
self.__parse_metadata()
|
|
30
|
+
self.__parse_tbins()
|
|
31
|
+
self.__parse_offsets()
|
|
32
|
+
self.__build_interp_funcs()
|
|
33
|
+
self.__check_parse()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def __read_dev(self) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Import the .dev file as a text file.
|
|
39
|
+
The file contents are stored as a list of strings in the class attribute self._content.
|
|
40
|
+
|
|
41
|
+
:return: None
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
with open(self._filepath, 'r') as _file:
|
|
45
|
+
self._content = _file.readlines()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def __parse_metadata(self) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Parse the .dev file for individual sensor metadata.
|
|
51
|
+
Sensor specific metadata are stored as class attributes.
|
|
52
|
+
|
|
53
|
+
:return: None
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
metadata_lines = [line for line in self._content if 'C and A offset' not in line]
|
|
57
|
+
for line in metadata_lines:
|
|
58
|
+
if 'ACS Meter' in line:
|
|
59
|
+
self.sensor_type = re.findall('(.*?)\n', line)[0]
|
|
60
|
+
elif 'Serial' in line:
|
|
61
|
+
self.sn_hexdec = re.findall('(.*?)\t', line)[0]
|
|
62
|
+
self.sn = 'ACS-' + str(int(self.sn_hexdec[-6:], 16)).zfill(5) # Convert to sn shown on product sticker.
|
|
63
|
+
elif 'structure version' in line:
|
|
64
|
+
self.structure_version = int(re.findall(f'({NUM_PAT})\t', line)[0])
|
|
65
|
+
elif 'tcal' in line or 'Tcal' in line:
|
|
66
|
+
self.tcal, self.ical = [float(v) for v in re.findall(f': ({NUM_PAT}) C', line)]
|
|
67
|
+
cal_date_str = re.findall('file on (.*?)[.]', line)[0].replace(' ', '')
|
|
68
|
+
try: # Sometimes the file date is entered as yyyy or yy. This should handle both cases.
|
|
69
|
+
self.cal_date = datetime.strptime(cal_date_str, '%m/%d/%Y').strftime('%Y-%m-%d')
|
|
70
|
+
except:
|
|
71
|
+
self.cal_date = datetime.strptime(cal_date_str, '%m/%d/%y').strftime('%Y-%m-%d')
|
|
72
|
+
elif 'Depth calibration' in line:
|
|
73
|
+
(self.depth_cal_1,
|
|
74
|
+
self.depth_cal_2) = [float(v) for v in re.findall(f'({NUM_PAT})', line)]
|
|
75
|
+
elif 'Baud' in line:
|
|
76
|
+
self.baudrate = int(re.findall(f'({NUM_PAT})\t', line)[0])
|
|
77
|
+
elif 'Path' in line:
|
|
78
|
+
self.path_length = float(re.findall(f'({NUM_PAT})\t', line)[0])
|
|
79
|
+
elif 'wavelengths' in line:
|
|
80
|
+
self.num_wavelength = int(re.findall(f'({NUM_PAT})\t', line)[0])
|
|
81
|
+
elif 'number of temperature bins' in line:
|
|
82
|
+
self.num_tbin = int(re.findall(f'({NUM_PAT})\t', line)[0])
|
|
83
|
+
elif 'maxANoise' in line:
|
|
84
|
+
(self.max_a_noise, self.max_c_noise, self.max_a_nonconform, self.max_c_nonconform,
|
|
85
|
+
self.max_a_difference, self.max_c_difference, self.min_a_counts,
|
|
86
|
+
self.min_c_counts, self.min_r_counts, self.max_temp_sd,
|
|
87
|
+
self.max_depth_sd) = [float(v) for v in re.findall(f'({NUM_PAT})\t', line)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def __parse_tbins(self) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Parse the .dev file for temperature bin information.
|
|
93
|
+
|
|
94
|
+
:return: None
|
|
95
|
+
"""
|
|
96
|
+
tbin_line = [line for line in self._content if '; temperature bins' in line][0]
|
|
97
|
+
tbins = tbin_line.split('\t')
|
|
98
|
+
tbins = [v for v in tbins if v] # Toss empty strings.
|
|
99
|
+
tbins = [v for v in tbins if v != '\n'] # Toss newline characters.
|
|
100
|
+
self.tbin = np.array([float(v) for v in tbins if 'temperature bins' not in v]) # Convert to float and toss comment.
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def __parse_offsets(self) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Parse the .dev file for a and c offsets. Data are saved as class attributes for access at a later time.
|
|
106
|
+
|
|
107
|
+
:return: None
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
offset_lines = [line for line in self._content if 'C and A offset' in line]
|
|
111
|
+
|
|
112
|
+
# Create holder arrays to loop over and append data to.
|
|
113
|
+
c_wvls = []
|
|
114
|
+
a_wvls = []
|
|
115
|
+
c_offs = []
|
|
116
|
+
a_offs = []
|
|
117
|
+
c_deltas = []
|
|
118
|
+
a_deltas = []
|
|
119
|
+
wavelength_color_schemes = []
|
|
120
|
+
|
|
121
|
+
for line in offset_lines:
|
|
122
|
+
offsets, c_delta, a_delta = line.split('\t\t')[:-1]
|
|
123
|
+
c_wvl, a_wvl, wvl_color, c_off, a_off = offsets.split('\t')
|
|
124
|
+
|
|
125
|
+
# Convert strings to proper pythonic datatypes.
|
|
126
|
+
c_wvl = float(c_wvl.replace('C', ''))
|
|
127
|
+
a_wvl = float(a_wvl.replace('A', ''))
|
|
128
|
+
c_off = float(c_off)
|
|
129
|
+
a_off = float(a_off)
|
|
130
|
+
c_delta = [float(v) for v in c_delta.split('\t')]
|
|
131
|
+
a_delta = [float(v) for v in a_delta.split('\t')]
|
|
132
|
+
|
|
133
|
+
# Append files to holder arrays.
|
|
134
|
+
c_wvls.append(c_wvl)
|
|
135
|
+
a_wvls.append(a_wvl)
|
|
136
|
+
c_offs.append(c_off)
|
|
137
|
+
a_offs.append(a_off)
|
|
138
|
+
c_deltas.append(c_delta)
|
|
139
|
+
a_deltas.append(a_delta)
|
|
140
|
+
wavelength_color_schemes.append(wvl_color)
|
|
141
|
+
|
|
142
|
+
# Convert holder arrays to numpy arrays.
|
|
143
|
+
self.c_wavelength = np.array(c_wvls)
|
|
144
|
+
self.a_wavelength = np.array(a_wvls)
|
|
145
|
+
self.c_offset = np.array(c_offs)
|
|
146
|
+
self.a_offset = np.array(a_offs)
|
|
147
|
+
self.c_delta_t = np.array(c_deltas)
|
|
148
|
+
self.a_delta_t = np.array(a_deltas)
|
|
149
|
+
self.wavelength_color_schemes = wavelength_color_schemes
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def __build_interp_funcs(self) -> None:
|
|
153
|
+
"""
|
|
154
|
+
Build interpolation functions for the a and c delta_t values and store as class attributes.
|
|
155
|
+
|
|
156
|
+
:return: None
|
|
157
|
+
"""
|
|
158
|
+
self.func_a_delta_t = interpolate.interp1d(self.tbin, self.a_delta_t, axis=1)
|
|
159
|
+
self.func_c_delta_t = interpolate.interp1d(self.tbin, self.c_delta_t, axis=1)
|
|
160
|
+
self.delta_t_interp_method = 'scipy.interpolate.interp1d'
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def __check_parse(self) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Verify that the shape of the data is as expected.
|
|
166
|
+
|
|
167
|
+
:return: None
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
if len(self.a_wavelength) != self.num_wavelength:
|
|
171
|
+
raise ValueError('Mismatch between number of wavelengths extracted for A and expected from file.'
|
|
172
|
+
'Please verify the .dev file integrity.')
|
|
173
|
+
if len(self.c_wavelength) != self.num_wavelength:
|
|
174
|
+
raise ValueError('Mismatch between number of wavelengths extracted for C and expected from file.'
|
|
175
|
+
'Please verify the .dev file integrity.')
|
|
176
|
+
if len(self.c_wavelength) != len(self.a_wavelength):
|
|
177
|
+
raise ValueError('Mismatch between number of wavelengths extracted for A and C.'
|
|
178
|
+
'Please verify the .dev file integrity.')
|
|
179
|
+
if np.array(self.a_delta_t).shape != (len(self.a_wavelength), self.num_tbin):
|
|
180
|
+
raise ValueError('Mismatch between length of A wavelengths and number of temperature bins.'
|
|
181
|
+
'Please verify the .dev file integrity.')
|
|
182
|
+
if np.array(self.c_delta_t).shape != (len(self.a_wavelength), self.num_tbin):
|
|
183
|
+
raise ValueError('Mismatch between length of C wavelengths and number of temperature bins.'
|
|
184
|
+
'Please verify the .dev file integrity.')
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def to_xarray(self) -> xr.Dataset:
|
|
188
|
+
"""
|
|
189
|
+
Convert the parsed .dev file files to an xarray dataset
|
|
190
|
+
|
|
191
|
+
Returns: An appropriately dimensioned xarray dataset containing device file files.
|
|
192
|
+
"""
|
|
193
|
+
ds = xr.Dataset()
|
|
194
|
+
ds = ds.assign_coords({'a_wavelength': self.a_wavelength,
|
|
195
|
+
'c_wavelength': self.c_wavelength,
|
|
196
|
+
'temperature_bin': self.tbin})
|
|
197
|
+
|
|
198
|
+
ds['a_offset'] = (['a_wavelength'], self.a_offset)
|
|
199
|
+
ds['a_delta_t'] = (['a_wavelength', 'temperature_bin'], self.a_delta_t)
|
|
200
|
+
|
|
201
|
+
ds['c_offset'] = (['c_wavelength'], np.array(self.c_offset))
|
|
202
|
+
ds['c_delta_t'] = (['c_wavelength', 'temperature_bin'], self.c_delta_t)
|
|
203
|
+
|
|
204
|
+
ds.attrs['device_filepath'] = self._filepath
|
|
205
|
+
ds.attrs['sensor_type'] = self.sensor_type
|
|
206
|
+
ds.attrs['serial_number_hexdec'] = self.sn_hexdec
|
|
207
|
+
ds.attrs['serial_number'] = self.sn
|
|
208
|
+
ds.attrs['device_file_structure_version'] = self.structure_version
|
|
209
|
+
ds.attrs['tcal'] = self.tcal
|
|
210
|
+
ds.attrs['ical'] = self.ical
|
|
211
|
+
ds.attrs['calibration_date'] = self.cal_date
|
|
212
|
+
ds.attrs['depth_cal_1'] = self.depth_cal_1
|
|
213
|
+
ds.attrs['depth_cal_2'] = self.depth_cal_2
|
|
214
|
+
ds.attrs['baudrate'] = self.baudrate
|
|
215
|
+
ds.attrs['path_length'] = self.path_length
|
|
216
|
+
ds.attrs['number_of_wavelength_bins'] = self.num_wavelength
|
|
217
|
+
ds.attrs['number_of_temperature_bins'] = self.num_tbin
|
|
218
|
+
ds.attrs['max_a_noise'] = self.max_a_noise
|
|
219
|
+
ds.attrs['max_c_noise'] = self.max_c_noise
|
|
220
|
+
ds.attrs['max_a_nonconform'] = self.max_a_nonconform
|
|
221
|
+
ds.attrs['max_c_nonconform'] = self.max_c_nonconform
|
|
222
|
+
ds.attrs['max_a_difference'] = self.max_a_difference
|
|
223
|
+
ds.attrs['max_c_difference'] = self.max_c_difference
|
|
224
|
+
ds.attrs['min_a_counts'] = self.min_a_counts
|
|
225
|
+
ds.attrs['min_c_counts'] = self.min_c_counts
|
|
226
|
+
ds.attrs['min_r_counts'] = self.min_r_counts
|
|
227
|
+
ds.attrs['max_temp_sd'] = self.max_temp_sd
|
|
228
|
+
ds.attrs['max_depth_sd'] = self.max_depth_sd
|
|
229
|
+
return ds
|
acspype/discontinuity.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.interpolate import CubicSpline
|
|
3
|
+
from typing import Union
|
|
4
|
+
import xarray as xr
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def find_discontinuity_index(a_wavelengths: Union[list,tuple, np.array, xr.DataArray],
|
|
8
|
+
c_wavelengths: Union[list,tuple, np.array, xr.DataArray],
|
|
9
|
+
min_band: int = 535, max_band: int = 600) -> int:
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
This code is modified from the OPTAA processing utilities in the ooi-data-explorations repo.
|
|
13
|
+
https://github.com/IanTBlack/ooi-data-explorations/blob/master/python/ooi_data_explorations/uncabled/utilities/utilities_optaa.py#L104
|
|
14
|
+
|
|
15
|
+
Find the last wavelength index of the first filter based on wavelength differences.
|
|
16
|
+
This function assumes that the discontinuity occurs between 535 nm and 600 nm, which is buffered from the values in the ACS Protocol Document, Rev Q.
|
|
17
|
+
|
|
18
|
+
:param a_wavelengths: Absorption wavelengths
|
|
19
|
+
:param c_wavelengths: Attenuation wavelengths
|
|
20
|
+
:return: The last wavelength index at the discontinuity.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Copy and convert to numpy arrays because we are paranoid about global variables.
|
|
24
|
+
a_wavelengths = np.array(a_wavelengths).copy()
|
|
25
|
+
c_wavelengths = np.array(c_wavelengths).copy()
|
|
26
|
+
|
|
27
|
+
# Set values outside the range to NaN
|
|
28
|
+
a_wavelengths[(a_wavelengths < min_band) | (a_wavelengths > max_band)] = np.nan
|
|
29
|
+
c_wavelengths[(c_wavelengths < min_band) | (c_wavelengths > max_band)] = np.nan
|
|
30
|
+
|
|
31
|
+
# Find the index of the discontinuity
|
|
32
|
+
didx = int(np.nanargmin(np.diff(a_wavelengths) + np.diff(c_wavelengths)))
|
|
33
|
+
return didx
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _compute_discontinuity_offset(values: Union[list, tuple, np.array],
|
|
38
|
+
wavelength: Union[list, tuple, np.array],
|
|
39
|
+
didx: int) -> float:
|
|
40
|
+
"""
|
|
41
|
+
This code is modified from the OPTAA processing utilities in the ooi-data-explorations repo.
|
|
42
|
+
https://github.com/IanTBlack/ooi-data-explorations/blob/master/python/ooi_data_explorations/uncabled/utilities/utilities_optaa.py#L212
|
|
43
|
+
|
|
44
|
+
Compute the scalar discontinuity offset to be applied to the second half of an ACS spectra.
|
|
45
|
+
NOTE: If the input values contain an inf value, the function will return -999. This is to prevent math errors associated with creating a cubic spline on infinite values.
|
|
46
|
+
Spectra with infinite values should be removed at some point in the processing pipeline.
|
|
47
|
+
|
|
48
|
+
:param values: The incoming absorption or attenuation values. It is highly recommended that these values be
|
|
49
|
+
representative of ACS data that have been converted to 'geophysical' units (1/m) and corrected for the effects
|
|
50
|
+
of internal temperature on output. That is to say, the recommended input is the measured (a_m and c_m) in the
|
|
51
|
+
ACS protocol documents and manual. acspype inputs would be a_m_discontinuity and c_m_discontinuity.
|
|
52
|
+
:param wavelength: The wavelength bins of the values.
|
|
53
|
+
:param didx: The index of discontinuity.
|
|
54
|
+
:return: The discontinuity offset for the second half of the spectrum.
|
|
55
|
+
"""
|
|
56
|
+
_wavelength = np.copy(wavelength)
|
|
57
|
+
_values = np.copy(values)
|
|
58
|
+
_didx = int(np.copy(didx))
|
|
59
|
+
|
|
60
|
+
x = _wavelength[_didx - 2:_didx + 1]
|
|
61
|
+
y = _values[_didx - 2:_didx + 1]
|
|
62
|
+
|
|
63
|
+
if True in np.isinf(y):
|
|
64
|
+
return -999
|
|
65
|
+
else:
|
|
66
|
+
cubic_spline = CubicSpline(x, y)
|
|
67
|
+
interp = cubic_spline(_wavelength[_didx + 1], extrapolate=True)
|
|
68
|
+
offset = interp - _values[_didx + 1]
|
|
69
|
+
return offset
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _apply_discontinuity_offset(values: Union[list, tuple, np.array],
|
|
73
|
+
offset: float,
|
|
74
|
+
didx: int) -> np.array:
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
This code is modified from the OPTAA processing utilities in the ooi-data-explorations repo.
|
|
78
|
+
https://github.com/IanTBlack/ooi-data-explorations/blob/master/python/ooi_data_explorations/uncabled/utilities/utilities_optaa.py#L212
|
|
79
|
+
|
|
80
|
+
Apply a pre-determined discontinuity offset to values after the discontinuity index.
|
|
81
|
+
|
|
82
|
+
:param values: The measured values to apply the discontinuity offset to.
|
|
83
|
+
:param offset: The scalar offset to apply to the values after the discontinuity index.
|
|
84
|
+
:param didx: The discontinuity index computed from find_discontinuity_index.
|
|
85
|
+
:return: A discontinuity-corrected spectra.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
_values = np.copy(values)
|
|
89
|
+
_offset = np.copy(offset)
|
|
90
|
+
_didx = int(np.copy(didx))
|
|
91
|
+
_values[_didx + 1:] = _values[_didx + 1:] + _offset
|
|
92
|
+
return _values
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def compute_discontinuity_offset(measured, wavelength_dim, discontinuity_index):
|
|
96
|
+
"""
|
|
97
|
+
This is a wrapper function for _compute_discontinuity_offset that is vectorized for Xarray.
|
|
98
|
+
|
|
99
|
+
:param measured: The measured values
|
|
100
|
+
:param wavelength_dim: The dimension to calculate the offset on.
|
|
101
|
+
:param discontinuity_index
|
|
102
|
+
:return: The scalar offset for a given spectrum.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
offset = xr.apply_ufunc(_compute_discontinuity_offset, measured,
|
|
106
|
+
kwargs = {'wavelength': measured[wavelength_dim].values, 'didx': discontinuity_index},
|
|
107
|
+
input_core_dims = [[wavelength_dim]],
|
|
108
|
+
output_core_dims = [[]],
|
|
109
|
+
vectorize = True)
|
|
110
|
+
return offset
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def apply_discontinuity_offset(measured, offset, wavelength_dim, discontinuity_index):
|
|
114
|
+
"""
|
|
115
|
+
This is a wrapper function for _apply_discontinuity_offset that is vectorized for Xarray.
|
|
116
|
+
:param measured: The measured values.
|
|
117
|
+
:param offset: The pre-determined discontinuity offset.
|
|
118
|
+
:param wavelength_dim: The wavelength dimension to apply the offset to.
|
|
119
|
+
:param discontinuity_index: The index of discontinuity.
|
|
120
|
+
:return: Spectra with the discontinuity offset applied.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
dc = xr.apply_ufunc(_apply_discontinuity_offset, measured, offset,
|
|
124
|
+
kwargs = {'didx': discontinuity_index},
|
|
125
|
+
input_core_dims=[[wavelength_dim],[]],
|
|
126
|
+
output_core_dims = [[wavelength_dim]],
|
|
127
|
+
vectorize = True)
|
|
128
|
+
return dc
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def discontinuity_correction(measured, wavelength_dim, discontinuity_index):
|
|
132
|
+
"""
|
|
133
|
+
This is a convenience function for computing the discontinuity offset and applying it to the measured values in Xarray.
|
|
134
|
+
:param measured: The measured values.
|
|
135
|
+
:param wavelength_dim: The wavelength dimension to apply the correction to.
|
|
136
|
+
:param discontinuity_index: The index of discontinuity.
|
|
137
|
+
:return:
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
offset = compute_discontinuity_offset(measured, wavelength_dim, discontinuity_index)
|
|
141
|
+
dc = apply_discontinuity_offset(measured, offset, wavelength_dim, discontinuity_index)
|
|
142
|
+
return dc, offset
|
acspype/experimental.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import xarray as xr
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def compute_chl_alh(absorption: xr.DataArray, alh_star: float = 0.0104) -> xr.DataArray:
|
|
5
|
+
"""
|
|
6
|
+
Compute chlorophyll-a from absorption line height via Roesler and Barnard 2013.
|
|
7
|
+
https://www.sciencedirect.com/science/article/pii/S2211122013000509
|
|
8
|
+
|
|
9
|
+
:param absorption: Absorption data with wavelength as a coordinate.
|
|
10
|
+
:param alh_star: Absorption line height coefficient value, default is 0.0104,
|
|
11
|
+
which is the average from Table 1 in Roesler and Barnard 2013.
|
|
12
|
+
:return: Chlorophyll-a concentration in mg/m^3.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
a650 = absorption.sel(wavelength=650, method='nearest')
|
|
16
|
+
a676 = absorption.sel(wavelength=676, method='nearest')
|
|
17
|
+
a715 = absorption.sel(wavelength=715, method='nearest')
|
|
18
|
+
|
|
19
|
+
abl = ((a715 - a650) / (715 - 650)) * (676 - 650) + a650 #EQ 1 in Roesler and Barnard 2013
|
|
20
|
+
alh = a676 - abl #EQ 2 in Roesler and Barnard 2013
|
|
21
|
+
chl_alh = alh / alh_star #EQ 3 in Roesler and Barnard 2013
|
|
22
|
+
|
|
23
|
+
chl_alh.attrs['alh_star'] = alh_star
|
|
24
|
+
chl_alh.attrs['method'] = 'Roesler and Barnard, 2013'
|
|
25
|
+
chl_alh.attrs['ancillary_variables'] = str(absorption.name)
|
|
26
|
+
return chl_alh
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# def compute_poc(attenuation: xr.DataArray, method = 'gardner_et_al_2006'):
|
|
30
|
+
# """
|
|
31
|
+
# Compute particulate organic carbon (POC) from attenuation based on linear models defined by the issued method.
|
|
32
|
+
#
|
|
33
|
+
# :param attenuation: The particulate attenuation coefficient (c_p).
|
|
34
|
+
# :param method: The method to use for POC calculation. Options are:
|
|
35
|
+
# gardner_et_al_2006
|
|
36
|
+
# behrenfeld_and_boss_2006
|
|
37
|
+
# stramski_et_al_2008
|
|
38
|
+
# cetenic_et_al_2012
|
|
39
|
+
# goni_et_al_2021 -> Oregon Coast, August, 2011, Sigma 22-23
|
|
40
|
+
# :return: POC in mg/m3
|
|
41
|
+
# """
|
|
42
|
+
#
|
|
43
|
+
# if method == 'gardner_et_al_2006':
|
|
44
|
+
# m = 381 #POC to c_p slope in units of mgC/m2
|
|
45
|
+
# c_p = attenuation.sel(wavelength=660, method='nearest')
|
|
46
|
+
# b = 9.4
|
|
47
|
+
# elif method == 'behrenfeld_and_boss_2006':
|
|
48
|
+
# m = 585
|
|
49
|
+
# c_p = attenuation.sel(wavelength=660, method='nearest')
|
|
50
|
+
# b = 7.6
|
|
51
|
+
# elif method == 'stramski_et_al_2008':
|
|
52
|
+
# m = 458
|
|
53
|
+
# c_p = attenuation.sel(wavelength=660, method='nearest')
|
|
54
|
+
# b = 0
|
|
55
|
+
# elif method == 'cetenic_et_al_2012':
|
|
56
|
+
# m = 391
|
|
57
|
+
# c_p = attenuation.sel(wavelength=660, method='nearest')
|
|
58
|
+
# b = -5.8
|
|
59
|
+
# elif method == 'goni_et_al_2021':
|
|
60
|
+
# m = 38.9* 12.01
|
|
61
|
+
# c_p = attenuation.sel(wavelength=650, method='nearest')
|
|
62
|
+
# b = 0
|
|
63
|
+
# poc = m * c_p + b
|
|
64
|
+
# return poc
|