-
Notifications
You must be signed in to change notification settings - Fork 40
Merge master into dev #815
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2426bbc
38f43ed
357968b
052cc29
2d58caa
6cee97c
9a0d9b6
265e51d
5cdf793
f541961
26008f1
9db5f37
37a0345
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| """Benchmarks for importing dascore using pytest-codspeed.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import importlib | ||
| import sys | ||
| import warnings | ||
|
|
||
| import pint | ||
| import pytest | ||
|
|
||
|
|
||
| def _dascore_module_names(): | ||
| """Get the names of all currently imported dascore modules.""" | ||
| return [x for x in sys.modules if x == "dascore" or x.startswith("dascore.")] | ||
|
|
||
|
|
||
| class TestImportBenchmarks: | ||
| """ | ||
| Benchmarks for re-executing dascore's modules. | ||
|
|
||
| Only dascore's own modules are removed from the module cache, so these | ||
| measure the cost of executing dascore's module bodies, not the one-time | ||
| cost of importing third party dependencies. A fresh interpreter (eg a | ||
| CLI call) also pays the latter, but it cannot be measured here; a | ||
| subprocess falls outside the region CodSpeed instruments. Instead, see | ||
| tests/test_imports.py for the guards which keep slow dependencies out of | ||
| the import chain entirely. | ||
| """ | ||
|
|
||
| @pytest.fixture() | ||
| def restore_dascore_modules(self): | ||
| """Put the original dascore modules back after re-importing them.""" | ||
| # Import here so third party dependencies are warm before timing, | ||
| # even when this file is the only one collected. | ||
| importlib.import_module("dascore") | ||
| saved = {x: sys.modules[x] for x in _dascore_module_names()} | ||
| # Re-importing dascore makes (and installs) a new pint registry. | ||
| registry = pint.get_application_registry().get() | ||
| # dascore adds a warning filter on import; catch_warnings undoes that. | ||
| with warnings.catch_warnings(): | ||
| yield | ||
| for name in _dascore_module_names(): | ||
| del sys.modules[name] | ||
| sys.modules.update(saved) | ||
| pint.set_application_registry(registry) | ||
|
|
||
| @pytest.mark.benchmark | ||
| def test_reimport_dascore(self, restore_dascore_modules): | ||
| """Time re-importing the top-level dascore module.""" | ||
| for name in _dascore_module_names(): | ||
| del sys.modules[name] | ||
| importlib.import_module("dascore") |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,6 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import h5py | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from h5py import h5r | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from h5py.h5r import Reference, dereference | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import dascore as dc | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -77,15 +76,18 @@ def _raise_legacy_ref_error(h5, field_name: str) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _dereference(h5, value, field_name: str): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Resolve an HDF5 reference, rejecting legacy anonymous DASVader refs.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Resolve an HDF5 reference or raise a clear compatibility error.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(value, Reference): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if h5r.get_name(value, h5.id) is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _raise_legacy_ref_error(h5, field_name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return h5[value] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except KeyError: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return h5py.Dataset(dereference(value, h5.id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # The high-level lookup fails for some references HDF5 can still | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # resolve directly, so try that before giving up. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return h5py.Dataset(dereference(value, h5.id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _raise_legacy_ref_error(h5, field_name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
78
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: Yes, h5py Group.getitem raises a ValueError when provided with an unresolvable or "null" object reference [1][2]. In the implementation of Group.getitem, h5py checks if the provided key is an instance of h5r.Reference [1]. If it is, the library attempts to dereference it using h5r.dereference [1]. If the dereferencing process returns None (which occurs for zero-filled or null references), h5py explicitly raises a ValueError with the message "Invalid HDF5 object reference" [1][3][2]. This behavior is specific to object references. In contrast, when accessing group members by string path, h5py typically raises a KeyError if the object does not exist [4][5]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'Files matching utils.py:\n'
fd 'utils.py$' . || true
printf '\nTarget file outline:\n'
ast-grep outline dascore/io/dasvader/utils.py --view expanded || true
printf '\nRelevant portion of dascore/io/dasvader/utils.py:\n'
cat -n dascore/io/dasvader/utils.py | sed -n '1,140p'
printf '\nUsages of _dereference and DASVaderCompatibilityError:\n'
rg -n "_dereference|DASVaderCompatibilityError|h5yp|Reference" dascore/io/dasvader/utils.py dascore io 2>/dev/null | head -200 || true
printf '\nPython module availability and h5py reference resolution behavior probe:\n'
python3 - <<'PY'
import inspect, struct, tempfile, os
try:
import h5py, h5py.h5r as h5r
except Exception as exc:
print(f"h5py import error: {type(exc).__name__}: {exc}")
raise SystemExit(0)
print("h5py version:", getattr(h5py, "__version__", "unknown"))
src = inspect.getsource(h5py.h5r.Reference)
print(f"\nh5py.h5r.Reference.source:\n{src[:3000]}")
src = inspect.getsource(h5py.Group.__getitem__)
print(f"\nh5py.Group.__getitem__.source:\n{src}")
def make_zero_ref():
return b'0'*48
for dtype in ("u8", "i8", "<u8", ">u8", "<i8", ">i8", None):
arr = make_zero_ref()
buf = arr[:]
if dtype is None or dtype.startswith("<"):
buf = buf.swapbytes()
print(dtype, repr(buf), "starts with zero bytes:", buf[:4] == b'\x00\0\0\0')
with tempfile.NamedTemporaryFile(suffix=".h5", delete=False) as f:
path = f.name
try:
with h5py.File(path, "w") as f:
f.attrs["zero_ref"] = make_zero_ref()
f.attrs["one_ref"] = make_zero_ref()
f.attrs["one_ref"][0] = b"\x0F"[0] if isinstance(make_zero_ref(), bytes) else 15
with h5py.File(path, "r") as f:
zero = f.attrs["zero_ref"]
one = f.attrs["one_ref"]
print("\nReading attrs:", zero, one)
print("is Reference?", isinstance(zero, h5r.Reference), isinstance(one, h5r.Reference))
for label, ref in [("zero_ref", zero), ("one_ref", one)]:
try:
val = f[ref]
print("getitem succeed", label, type(val))
except Exception as exc:
print("getitem", label, type(exc).__name__, str(exc))
try:
res = h5r.dereference(ref, f.id)
print("h5r.dereference raw", label, res, type(res))
except Exception as exc:
print("h5r.dereference", label, type(exc).__name__, str(exc))
finally:
os.unlink(path)
PYRepository: DASDAE/dascore Length of output: 9980 🌐 Web query:
💡 Result: The ValueError "Invalid HDF5 object reference" in h5py is raised within the Citations:
Catch
🐛 Proposed fix try:
return h5[value]
- except KeyError:
+ except (KeyError, ValueError):
# The high-level lookup fails for some references HDF5 can still
# resolve directly, so try that before giving up.
try:
return h5py.Dataset(dereference(value, h5.id))
except Exception:
_raise_legacy_ref_error(h5, field_name)Keep the narrow 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.16.0)[warning] 89-89: Do not catch blind exception: (BLE001) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --- Metadata parsing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -318,6 +318,10 @@ def normalize( | |
| """ | ||
| Normalize a patch along a specified dimension. | ||
|
|
||
| NaN values are ignored when computing the norm. They remain NaN in the | ||
| output but do not affect any other sample. Slices with a norm of zero, | ||
| meaning they contain nothing but zeros and NaN, are returned unscaled. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| dim | ||
|
|
@@ -348,26 +352,24 @@ def normalize( | |
| data = self.data | ||
| if norm in {"l1", "l2"}: | ||
| order = int(norm[-1]) | ||
| norm_values = np.linalg.norm(self.data, axis=axis, ord=order) | ||
| # Equivalent to np.linalg.norm, but skips NaN rather than letting a | ||
| # single null blank every sample sharing its slice. The float exponent | ||
| # promotes ints so the powers cannot overflow a narrow dtype. | ||
| norm_values = np.nansum(np.abs(data) ** float(order), axis=axis) ** (1 / order) | ||
| divisor = np.expand_dims(norm_values, axis=axis) | ||
| elif norm == "max": | ||
| norm_values = np.max(np.abs(data), axis=axis) | ||
| divisor = np.expand_dims(np.nanmax(np.abs(data), axis=axis), axis=axis) | ||
| elif norm == "bit": | ||
| pass | ||
| divisor = np.abs(data) | ||
| else: | ||
| msg = ( | ||
| f"Norm value of {norm} is not supported. " | ||
| f"Supported values are {('l1', 'l2', 'max', 'bit')}" | ||
| ) | ||
| raise ValueError(msg) | ||
| if norm == "bit": | ||
| new_data = np.divide( | ||
| data, np.abs(data), out=np.zeros_like(data), where=np.abs(data) != 0 | ||
| ) | ||
| else: | ||
| expanded_norm = np.expand_dims(norm_values, axis=axis) | ||
| new_data = np.divide( | ||
| data, expanded_norm, out=np.zeros_like(data), where=expanded_norm != 0 | ||
| ) | ||
| # A zero divisor means there is nothing but zeros and nulls to scale, so | ||
| # divide those by one; the zeros stay zero and the nulls stay null. | ||
| new_data = data / np.where(divisor == 0, 1, divisor) | ||
|
Comment on lines
362
to
+372
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
python - <<'PY'
import numpy as np
value = np.array([np.iinfo(np.int8).min], dtype=np.int8)
assert (value / np.abs(value))[0] == 1.0
PYRepository: DASDAE/dascore Length of output: 152 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate relevant files =="
git ls-files | rg '(^dascore/proc/basic\.py$|^tests/test_proc/test_basic\.py$)' || true
echo
echo "== basic.py normalize implementation around bit norm =="
sed -n '320,400p' dascore/proc/basic.py
echo
echo "== test_basic.py integer normalization tests =="
sed -n '220,275p' tests/test_proc/test_basic.py
echo
echo "== search for normalize signature and usage =="
rg -n "def normalize|normalize\\(|norm =|\"bit\"|'bit'" dascore tests/test_proc/test_basic.py -SRepository: DASDAE/dascore Length of output: 9655 Preserve the sign of signed-integer minimum values in
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| return self.new(data=new_data) | ||
|
|
||
|
|
||
|
|
@@ -385,6 +387,9 @@ def standardize( | |
| where u is the mean of the training samples or zero if with_mean=False, | ||
| and s is the standard deviation of the training samples or one if with_std=False. | ||
|
|
||
| NaN values are ignored when computing the mean and standard deviation. They | ||
| remain NaN in the output but do not affect any other sample. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| dim | ||
|
|
@@ -406,8 +411,8 @@ def standardize( | |
| """ | ||
| axis = self.get_axis(dim) | ||
| data = self.data | ||
| mean = np.mean(data, axis=axis, keepdims=True) | ||
| std = np.std(data, axis=axis, keepdims=True) | ||
| mean = np.nanmean(data, axis=axis, keepdims=True) | ||
| std = np.nanstd(data, axis=axis, keepdims=True) | ||
| new_data = (data - mean) / std | ||
| return self.new(data=new_data) | ||
|
|
||
|
|
@@ -810,6 +815,10 @@ def demedian(patch, dim: str = "time"): | |
| """ | ||
| Remove the median along a given dimension of a DASCore patch. | ||
|
|
||
| NaN values are ignored when computing the median, consistent with | ||
| [Patch.median](`dascore.proc.aggregate.median`). They remain NaN in the | ||
| output but do not affect any other sample. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| patch : | ||
|
|
@@ -856,7 +865,7 @@ def demedian(patch, dim: str = "time"): | |
| data = patch.data | ||
|
|
||
| # Compute median along axis, keep dims for broadcasting | ||
| med = np.median(data, axis=axis, keepdims=True) | ||
| med = np.nanmedian(data, axis=axis, keepdims=True) | ||
|
|
||
| new_data = data - med | ||
|
|
||
|
|
@@ -869,6 +878,10 @@ def demean(patch, dim: str = "time"): | |
| """ | ||
| Remove the mean along a given dimension of a DASCore patch. | ||
|
|
||
| NaN values are ignored when computing the mean, consistent with | ||
| [Patch.mean](`dascore.proc.aggregate.mean`). They remain NaN in the output | ||
| but do not affect any other sample. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| patch : | ||
|
|
@@ -915,7 +928,7 @@ def demean(patch, dim: str = "time"): | |
| data = patch.data | ||
|
|
||
| # Compute mean along axis, keep dims for broadcasting | ||
| mea = np.mean(data, axis=axis, keepdims=True) | ||
| mea = np.nanmean(data, axis=axis, keepdims=True) | ||
|
|
||
| new_data = data - mea | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Hyphenate the compound modifier.
Change
third party dependenciestothird-party dependencies.🧰 Tools
🪛 LanguageTool
[grammar] ~31-~31: Use a hyphen to join words.
Context: ...benchmarks (dascore's own modules; third party dependencies stay warm) Each benc...
(QB_NEW_EN_HYPHEN)
🤖 Prompt for AI Agents
Source: Linters/SAST tools