oceanarray 0.2.0__py3-none-any.whl → 0.4.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.
- oceanarray/__init__.py +13 -0
- oceanarray/_version.py +1 -1
- oceanarray/analysis/clock.py +11 -12
- oceanarray/analysis/hydrographic.py +7 -5
- oceanarray/analysis/science.py +15 -15
- oceanarray/analysis/spectral.py +11 -11
- oceanarray/analysis/temporal.py +2 -2
- oceanarray/cli.py +336 -649
- oceanarray/config/parameters.py +359 -7
- oceanarray/config/report.mplstyle +26 -0
- oceanarray/config/report_tokens.py +242 -0
- oceanarray/config/validation.py +36 -36
- oceanarray/inspect.py +7 -7
- oceanarray/logger.py +2 -2
- oceanarray/paths.py +83 -3
- oceanarray/plotters/__init__.py +50 -56
- oceanarray/plotters/_cli_legacy.py +5 -5
- oceanarray/plotters/animation.py +3 -4
- oceanarray/plotters/current.py +285 -174
- oceanarray/plotters/diagnostic.py +482 -332
- oceanarray/plotters/helpers.py +174 -33
- oceanarray/plotters/hydrography.py +58 -100
- oceanarray/plotters/primitives.py +452 -61
- oceanarray/plotters/spectrum.py +184 -84
- oceanarray/plotters/timeseries.py +160 -58
- oceanarray/plotters/ts.py +221 -79
- oceanarray/processors/__init__.py +39 -17
- oceanarray/processors/caldip.py +309 -1
- oceanarray/processors/coordinate.py +60 -49
- oceanarray/processors/grid.py +57 -62
- oceanarray/processors/helpers.py +34 -22
- oceanarray/processors/pressure.py +18 -14
- oceanarray/processors/qc.py +36 -83
- oceanarray/processors/stack.py +35 -34
- oceanarray/processors/stage1.py +107 -77
- oceanarray/processors/stage2.py +22 -21
- oceanarray/processors/stage3.py +115 -48
- oceanarray/reports/__init__.py +6 -0
- oceanarray/{report → reports}/_array.py +44 -235
- oceanarray/reports/_css.py +267 -0
- oceanarray/reports/_encode.py +136 -0
- oceanarray/reports/_env.py +101 -0
- oceanarray/reports/_figdebug.py +142 -0
- oceanarray/reports/_grid.py +650 -0
- oceanarray/{report → reports}/_html_helpers.py +140 -53
- oceanarray/reports/_instrument.py +656 -0
- oceanarray/reports/_manifest.py +449 -0
- oceanarray/reports/_mooring.py +862 -0
- oceanarray/reports/_pdf.py +415 -0
- oceanarray/reports/_plots.py +940 -0
- oceanarray/{report → reports}/_recovery_table.py +22 -177
- oceanarray/reports/_report_css.py +23 -0
- oceanarray/reports/_slots.py +62 -0
- oceanarray/reports/_stack.py +1098 -0
- oceanarray/reports/templates/_grid_history.html +10 -0
- oceanarray/reports/templates/_grid_nc_globals.html +14 -0
- oceanarray/reports/templates/_grid_nc_scalars.html +18 -0
- oceanarray/reports/templates/_grid_nc_variables.html +30 -0
- oceanarray/reports/templates/_instrument_analog_yaml.html +12 -0
- oceanarray/reports/templates/_instrument_files.html +39 -0
- oceanarray/reports/templates/_instrument_nc_globals.html +14 -0
- oceanarray/reports/templates/_instrument_nc_scalars.html +19 -0
- oceanarray/reports/templates/_instrument_nc_variables.html +30 -0
- oceanarray/reports/templates/_instrument_qc.html +65 -0
- oceanarray/reports/templates/_instrument_rose_note.html +6 -0
- oceanarray/reports/templates/_instrument_windows_note.html +36 -0
- oceanarray/reports/templates/_macros.html +87 -0
- oceanarray/reports/templates/_mooring_calibration.html +51 -0
- oceanarray/reports/templates/_mooring_clock_table.html +59 -0
- oceanarray/reports/templates/_mooring_diagram.html +5 -0
- oceanarray/reports/templates/_mooring_instruments.html +101 -0
- oceanarray/reports/templates/_mooring_issues.html +28 -0
- oceanarray/reports/templates/_mooring_pipeline.html +103 -0
- oceanarray/reports/templates/_mooring_qc.html +72 -0
- oceanarray/reports/templates/_mooring_timing.html +156 -0
- oceanarray/reports/templates/_stack_instr_table.html +16 -0
- oceanarray/reports/templates/_stack_nc_dims.html +11 -0
- oceanarray/reports/templates/_stack_roses_note.html +13 -0
- oceanarray/reports/templates/array.html +150 -0
- oceanarray/reports/templates/base.html +103 -0
- oceanarray/reports/templates/grid.html +43 -0
- oceanarray/reports/templates/instrument.html +36 -0
- oceanarray/reports/templates/mooring.html +91 -0
- oceanarray/reports/templates/recovery_table.html +129 -0
- oceanarray/reports/templates/stack.html +28 -0
- oceanarray/tools/rapid_interp.py +72 -78
- oceanarray/tools/readers.py +16 -18
- oceanarray/tools/writers.py +1 -1
- oceanarray/utilities.py +115 -36
- {oceanarray-0.2.0.dist-info → oceanarray-0.4.0.dist-info}/METADATA +41 -10
- oceanarray-0.4.0.dist-info/RECORD +114 -0
- {oceanarray-0.2.0.dist-info → oceanarray-0.4.0.dist-info}/WHEEL +1 -1
- oceanarray/logsheet/__init__.py +0 -60
- oceanarray/logsheet/_columns.py +0 -97
- oceanarray/logsheet/_config.py +0 -245
- oceanarray/logsheet/_firmware.py +0 -43
- oceanarray/logsheet/_latex.py +0 -128
- oceanarray/logsheet/_render.py +0 -126
- oceanarray/logsheet/builders/__init__.py +0 -13
- oceanarray/logsheet/builders/caldip_download.py +0 -195
- oceanarray/logsheet/builders/caldip_setup.py +0 -188
- oceanarray/logsheet/builders/deployment_setup.py +0 -218
- oceanarray/logsheet/builders/mooring_download.py +0 -243
- oceanarray/logsheet/builders/recovery.py +0 -157
- oceanarray/logsheet/data/column_defs.yaml +0 -686
- oceanarray/logsheet/data/latex_templates/_base_instrument.tex.j2 +0 -42
- oceanarray/logsheet/data/latex_templates/caldip_download.tex.j2 +0 -47
- oceanarray/logsheet/data/latex_templates/caldip_setup.tex.j2 +0 -70
- oceanarray/logsheet/data/latex_templates/deployment_setup.tex.j2 +0 -97
- oceanarray/logsheet/data/latex_templates/mooring_download.tex.j2 +0 -54
- oceanarray/logsheet/data/latex_templates/recovery.tex.j2 +0 -103
- oceanarray/oceanarray.mplstyle +0 -18
- oceanarray/report/__init__.py +0 -5
- oceanarray/report/_grid.py +0 -566
- oceanarray/report/_instrument.py +0 -866
- oceanarray/report/_mooring.py +0 -1514
- oceanarray/report/_plots.py +0 -979
- oceanarray/report/_stack.py +0 -947
- oceanarray-0.2.0.dist-info/RECORD +0 -93
- /oceanarray/{config → legacy}/OS1_vocab_attrs.yaml +0 -0
- {oceanarray-0.2.0.dist-info → oceanarray-0.4.0.dist-info}/entry_points.txt +0 -0
- {oceanarray-0.2.0.dist-info → oceanarray-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {oceanarray-0.2.0.dist-info → oceanarray-0.4.0.dist-info}/top_level.txt +0 -0
oceanarray/__init__.py
CHANGED
|
@@ -4,10 +4,23 @@ This package provides functions for processing, quality control, and analysis
|
|
|
4
4
|
of oceanographic time series data from moorings and instruments.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
try:
|
|
8
|
+
# Written by setuptools-scm at build time (gitignored); the live version.
|
|
9
|
+
from oceanarray._version import __version__
|
|
10
|
+
except ImportError: # pragma: no cover - source tree with no build artefact
|
|
11
|
+
try:
|
|
12
|
+
from importlib.metadata import PackageNotFoundError as _PNF
|
|
13
|
+
from importlib.metadata import version as _v
|
|
14
|
+
|
|
15
|
+
__version__ = _v("oceanarray")
|
|
16
|
+
except _PNF: # pragma: no cover
|
|
17
|
+
__version__ = "0.0.0"
|
|
18
|
+
|
|
7
19
|
from oceanarray.config import parameters
|
|
8
20
|
from oceanarray.processors import process
|
|
9
21
|
|
|
10
22
|
__all__ = [
|
|
23
|
+
"__version__",
|
|
11
24
|
# Subpackages (canonical locations)
|
|
12
25
|
"analysis",
|
|
13
26
|
"config",
|
oceanarray/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '0.4.0'
|
oceanarray/analysis/clock.py
CHANGED
|
@@ -12,7 +12,7 @@ Naming conventions updated from the original module:
|
|
|
12
12
|
- file suffix ``_raw`` → ``_stage2``
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
import
|
|
15
|
+
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
import numpy as np
|
|
18
18
|
import pandas as pd
|
|
@@ -53,13 +53,13 @@ def load_mooring_instruments(
|
|
|
53
53
|
proc_dir = output_path + mooring_name
|
|
54
54
|
moor_yaml = proc_dir + "/" + mooring_name + ".mooring.yaml"
|
|
55
55
|
|
|
56
|
-
with
|
|
56
|
+
with Path(moor_yaml).open() as f:
|
|
57
57
|
moor_yaml_data = yaml.safe_load(f)
|
|
58
58
|
|
|
59
59
|
# Support both new key 'clamp' and legacy key 'instruments'
|
|
60
60
|
instrument_list = moor_yaml_data.get("clamp", moor_yaml_data.get("instruments", []))
|
|
61
61
|
if not instrument_list:
|
|
62
|
-
raise KeyError(
|
|
62
|
+
raise KeyError(
|
|
63
63
|
f"Mooring YAML {moor_yaml} has neither 'clamp' nor 'instruments' key."
|
|
64
64
|
)
|
|
65
65
|
|
|
@@ -68,7 +68,7 @@ def load_mooring_instruments(
|
|
|
68
68
|
fname = mooring_name + "_" + str(i["serial"]) + file_suffix + ".nc"
|
|
69
69
|
rawfile = proc_dir + "/" + i["instrument"] + "/" + fname
|
|
70
70
|
|
|
71
|
-
if
|
|
71
|
+
if Path(rawfile).exists():
|
|
72
72
|
print(rawfile)
|
|
73
73
|
ds1 = xr.open_dataset(rawfile)
|
|
74
74
|
|
|
@@ -123,9 +123,7 @@ def create_common_time_grid(datasets):
|
|
|
123
123
|
latest_end = np.datetime64("NaT", "ns")
|
|
124
124
|
|
|
125
125
|
dt_sec = int(np.nanmedian(intervals_min) * 60)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return time_grid
|
|
126
|
+
return np.arange(earliest_start, latest_end, np.timedelta64(dt_sec, "s"))
|
|
129
127
|
|
|
130
128
|
|
|
131
129
|
def interpolate_datasets_to_grid(datasets, time_grid):
|
|
@@ -251,7 +249,7 @@ def combine_interpolated_datasets(datasets_interp):
|
|
|
251
249
|
co = 0
|
|
252
250
|
clock_offsets.append(int(np.rint(co)) if np.isfinite(co) else 0)
|
|
253
251
|
|
|
254
|
-
|
|
252
|
+
return xr.Dataset(
|
|
255
253
|
data_vars=combined_data,
|
|
256
254
|
coords={
|
|
257
255
|
"time": time_coord,
|
|
@@ -263,8 +261,6 @@ def combine_interpolated_datasets(datasets_interp):
|
|
|
263
261
|
},
|
|
264
262
|
)
|
|
265
263
|
|
|
266
|
-
return combined_ds
|
|
267
|
-
|
|
268
264
|
|
|
269
265
|
def analyze_deployment_timing(combined_ds):
|
|
270
266
|
"""Analyze deployment timing using temperature-based detection of deployment periods.
|
|
@@ -319,7 +315,7 @@ def calculate_timing_offsets(combined_ds, bin_width_sec=60):
|
|
|
319
315
|
# Find consensus group
|
|
320
316
|
vals = start_off0[np.isfinite(start_off0)]
|
|
321
317
|
if vals.size == 0:
|
|
322
|
-
raise RuntimeError("No finite start offsets to form consensus.")
|
|
318
|
+
raise RuntimeError("No finite start offsets to form consensus.")
|
|
323
319
|
|
|
324
320
|
vmin, vmax = vals.min(), vals.max()
|
|
325
321
|
bins = np.arange(vmin - bin_width_sec, vmax + 2 * bin_width_sec, bin_width_sec)
|
|
@@ -539,6 +535,7 @@ def print_correlation_summary(combined_ds, correlation_results):
|
|
|
539
535
|
correlation_results["lags"],
|
|
540
536
|
correlation_results["max_correlations"],
|
|
541
537
|
correlation_results["clock_offsets"],
|
|
538
|
+
strict=False,
|
|
542
539
|
)
|
|
543
540
|
):
|
|
544
541
|
print(
|
|
@@ -626,7 +623,9 @@ def plot_deployment_boundaries(
|
|
|
626
623
|
continue
|
|
627
624
|
|
|
628
625
|
# Plot each boundary
|
|
629
|
-
for ax, title, idx, time_ref in zip(
|
|
626
|
+
for ax, title, idx, time_ref in zip(
|
|
627
|
+
axes, titles, indices, times_ref, strict=False
|
|
628
|
+
):
|
|
630
629
|
# Define range around the boundary
|
|
631
630
|
start_range = max(0, idx - n_samples)
|
|
632
631
|
end_range = min(len(time_orig), idx + n_samples + 1)
|
|
@@ -9,10 +9,10 @@ Pairs with :mod:`oceanarray.plotters.hydrography` for figure output.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import gsw
|
|
12
13
|
import numpy as np
|
|
13
14
|
import pandas as pd
|
|
14
15
|
import xarray as xr
|
|
15
|
-
import gsw
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def calc_psal(ds: xr.Dataset) -> xr.Dataset:
|
|
@@ -99,7 +99,9 @@ def find_cold_entry_exit(
|
|
|
99
99
|
starts = np.r_[idx[0], idx[1:][gaps]]
|
|
100
100
|
ends = np.r_[idx[:-1][gaps], idx[-1]]
|
|
101
101
|
|
|
102
|
-
runs = [
|
|
102
|
+
runs = [
|
|
103
|
+
(s, e) for s, e in zip(starts, ends, strict=False) if (e - s + 1) >= min_len
|
|
104
|
+
]
|
|
103
105
|
if not runs:
|
|
104
106
|
return None, None, thr
|
|
105
107
|
|
|
@@ -111,7 +113,7 @@ def find_cold_entry_exit(
|
|
|
111
113
|
def calc_ds_difference(ds1: xr.Dataset, ds2: xr.Dataset) -> xr.Dataset:
|
|
112
114
|
"""Compute the variable-by-variable difference between two time-matched datasets."""
|
|
113
115
|
if not np.array_equal(ds1["TIME"].values, ds2["TIME"].values):
|
|
114
|
-
raise ValueError("TIME grids do not match between datasets.")
|
|
116
|
+
raise ValueError("TIME grids do not match between datasets.")
|
|
115
117
|
|
|
116
118
|
# Variables to exclude from differencing
|
|
117
119
|
exclude_vars = {"YY", "MM", "DD", "HH"}
|
|
@@ -261,11 +263,11 @@ def isopycnal_dataset(
|
|
|
261
263
|
|
|
262
264
|
"""
|
|
263
265
|
if sigma_var not in ds:
|
|
264
|
-
raise ValueError(f"sigma variable '{sigma_var}' not found in dataset")
|
|
266
|
+
raise ValueError(f"sigma variable '{sigma_var}' not found in dataset")
|
|
265
267
|
|
|
266
268
|
da = ds[sigma_var]
|
|
267
269
|
if "time" not in da.dims or "pressure" not in da.dims:
|
|
268
|
-
raise ValueError(
|
|
270
|
+
raise ValueError(
|
|
269
271
|
f"'{sigma_var}' must have both 'time' and 'pressure' dimensions"
|
|
270
272
|
)
|
|
271
273
|
da_tp = da.transpose("time", "pressure")
|
oceanarray/analysis/science.py
CHANGED
|
@@ -57,8 +57,7 @@ def flag_salinity_outliers(ds: xr.Dataset, n_std: float = 4) -> xr.DataArray:
|
|
|
57
57
|
lower = mean_sal - n_std * std_sal
|
|
58
58
|
upper = mean_sal + n_std * std_sal
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
return flag
|
|
60
|
+
return (ds["PSAL"] < lower) | (ds["PSAL"] > upper)
|
|
62
61
|
|
|
63
62
|
|
|
64
63
|
def flag_temporal_spikes(
|
|
@@ -87,8 +86,7 @@ def flag_vertical_inconsistencies(
|
|
|
87
86
|
vert_diff = np.abs(ds[var].diff("DEPTH"))
|
|
88
87
|
# Pad to match original dimensions
|
|
89
88
|
vert_diff = vert_diff.reindex(DEPTH=ds["DEPTH"], method="ffill")
|
|
90
|
-
|
|
91
|
-
return flag
|
|
89
|
+
return vert_diff > threshold
|
|
92
90
|
|
|
93
91
|
|
|
94
92
|
def run_qc(ds: xr.Dataset) -> xr.Dataset:
|
|
@@ -172,10 +170,10 @@ def process_dataset(
|
|
|
172
170
|
|
|
173
171
|
# Apply region of interest mask
|
|
174
172
|
mask = (
|
|
175
|
-
(
|
|
176
|
-
& (
|
|
177
|
-
& (
|
|
178
|
-
& (
|
|
173
|
+
(latlim[0] <= LAT)
|
|
174
|
+
& (latlim[1] >= LAT)
|
|
175
|
+
& (lonlim[0] <= LON)
|
|
176
|
+
& (lonlim[1] >= LON)
|
|
179
177
|
)
|
|
180
178
|
TEMP_profiles = TEMP_profiles[mask]
|
|
181
179
|
PSAL_profiles = PSAL_profiles[mask]
|
|
@@ -194,7 +192,7 @@ def process_dataset(
|
|
|
194
192
|
standard_pressures = pgrid.flatten()
|
|
195
193
|
|
|
196
194
|
# Create ds_standard
|
|
197
|
-
|
|
195
|
+
return xr.Dataset(
|
|
198
196
|
{
|
|
199
197
|
"CT": ((time_dim, pres_dim), CT_standard),
|
|
200
198
|
"SA": ((time_dim, pres_dim), SA_standard),
|
|
@@ -209,18 +207,20 @@ def process_dataset(
|
|
|
209
207
|
},
|
|
210
208
|
)
|
|
211
209
|
|
|
212
|
-
return ds_standard
|
|
213
|
-
|
|
214
210
|
|
|
215
211
|
# ---------------------------------------------------------------------------
|
|
216
212
|
# Backward-compat re-exports — import directly from the new modules in new code
|
|
217
213
|
# ---------------------------------------------------------------------------
|
|
218
|
-
from .temporal import lag_correlation, split_value, downsample_to_sparse # noqa: F401, E402
|
|
219
|
-
from .spectral import compute_cwt, welch_psd, welch_psd_gapaware # noqa: F401, E402
|
|
220
214
|
from .hydrographic import ( # noqa: F401, E402
|
|
215
|
+
calc_ds_difference,
|
|
221
216
|
calc_psal,
|
|
222
217
|
find_cold_entry_exit,
|
|
223
|
-
calc_ds_difference,
|
|
224
|
-
isopycnal_pressure_series,
|
|
225
218
|
isopycnal_dataset,
|
|
219
|
+
isopycnal_pressure_series,
|
|
220
|
+
)
|
|
221
|
+
from .spectral import compute_cwt, welch_psd, welch_psd_gapaware # noqa: F401, E402
|
|
222
|
+
from .temporal import ( # noqa: F401, E402
|
|
223
|
+
downsample_to_sparse,
|
|
224
|
+
lag_correlation,
|
|
225
|
+
split_value,
|
|
226
226
|
)
|
oceanarray/analysis/spectral.py
CHANGED
|
@@ -61,14 +61,14 @@ def gonella_rotary_spectrum(
|
|
|
61
61
|
"""
|
|
62
62
|
from scipy.signal import csd as _scipy_csd
|
|
63
63
|
|
|
64
|
-
_kw: dict =
|
|
65
|
-
fs
|
|
66
|
-
window
|
|
67
|
-
nperseg
|
|
68
|
-
noverlap
|
|
69
|
-
detrend
|
|
70
|
-
scaling
|
|
71
|
-
|
|
64
|
+
_kw: dict = {
|
|
65
|
+
"fs": fs,
|
|
66
|
+
"window": "hann",
|
|
67
|
+
"nperseg": nperseg,
|
|
68
|
+
"noverlap": noverlap,
|
|
69
|
+
"detrend": "linear",
|
|
70
|
+
"scaling": "density",
|
|
71
|
+
}
|
|
72
72
|
f_uu, p_uu = _scipy_welch(u_col, **_kw)
|
|
73
73
|
_, p_vv = _scipy_welch(v_col, **_kw)
|
|
74
74
|
_, c_uv = _scipy_csd(u_col, v_col, **_kw)
|
|
@@ -139,7 +139,7 @@ def compute_cwt(
|
|
|
139
139
|
try:
|
|
140
140
|
import pycwt
|
|
141
141
|
except ImportError as exc:
|
|
142
|
-
raise ImportError(
|
|
142
|
+
raise ImportError(
|
|
143
143
|
"pycwt is required for wavelet analysis: pip install pycwt"
|
|
144
144
|
) from exc
|
|
145
145
|
|
|
@@ -205,7 +205,7 @@ def compute_cwt(
|
|
|
205
205
|
gap_starts = np.where(edge_diff == 1)[0]
|
|
206
206
|
gap_ends = np.where(edge_diff == -1)[0]
|
|
207
207
|
t_all = np.arange(n, dtype=float)
|
|
208
|
-
for s, e in zip(gap_starts, gap_ends):
|
|
208
|
+
for s, e in zip(gap_starts, gap_ends, strict=False):
|
|
209
209
|
eff_coi[s:e] = 0.0 # entire gap column is unreliable
|
|
210
210
|
# Distance (samples) to the nearer gap boundary, outside the gap
|
|
211
211
|
dist = np.minimum(
|
|
@@ -322,7 +322,7 @@ def welch_psd_gapaware(
|
|
|
322
322
|
freq_out: np.ndarray | None = None
|
|
323
323
|
psd_sum: np.ndarray | None = None
|
|
324
324
|
total_wins = 0
|
|
325
|
-
for s, e in zip(starts, ends):
|
|
325
|
+
for s, e in zip(starts, ends, strict=False):
|
|
326
326
|
seg = x[s:e]
|
|
327
327
|
if len(seg) < segment_length:
|
|
328
328
|
continue
|
oceanarray/analysis/temporal.py
CHANGED
|
@@ -97,7 +97,7 @@ def lag_correlation(
|
|
|
97
97
|
x = np.asarray(x, float)
|
|
98
98
|
y = np.asarray(y, float)
|
|
99
99
|
if x.shape != y.shape:
|
|
100
|
-
raise ValueError("x and y must have same length (subsample both).")
|
|
100
|
+
raise ValueError("x and y must have same length (subsample both).")
|
|
101
101
|
corrs = np.full(2 * max_lag + 1, np.nan)
|
|
102
102
|
for k, lag in enumerate(range(-max_lag, max_lag + 1)):
|
|
103
103
|
if lag < 0:
|
|
@@ -154,7 +154,7 @@ def split_value(data: np.ndarray, nbins: int = 30) -> float:
|
|
|
154
154
|
|
|
155
155
|
# Step 3: Find minimum between first two major peaks
|
|
156
156
|
if len(peaks) < 2:
|
|
157
|
-
raise ValueError(
|
|
157
|
+
raise ValueError(
|
|
158
158
|
f"split_value: fewer than 2 histogram peaks found in data "
|
|
159
159
|
f"(found {len(peaks)}). Data may be unimodal or nbins={nbins} too coarse."
|
|
160
160
|
)
|