Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 28 additions & 12 deletions src/pyrecest/_backend/_shared_numpy/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
from ._dispatch import numpy as _np

_DTYPES = {
_np.dtype("int32"): 0,
_np.dtype("int64"): 1,
_np.dtype("float32"): 2,
_np.dtype("float64"): 3,
_np.dtype("complex64"): 4,
_np.dtype("complex128"): 5,
_np.dtype("bool"): 0,
_np.dtype("int32"): 1,
_np.dtype("int64"): 2,
_np.dtype("float32"): 3,
_np.dtype("float64"): 4,
_np.dtype("complex64"): 5,
_np.dtype("complex128"): 6,
}

_COMPLEX_DTYPES = [
Expand Down Expand Up @@ -99,12 +100,27 @@ def to_ndarray(x, to_ndim, axis=0, dtype=None):


def _get_wider_dtype(tensor_list):
dtype_list = [_DTYPES.get(x.dtype, -1) for x in tensor_list]
if len(dtype_list) == 1:
return dtype_list[0], True

wider_dtype_index = max(dtype_list)
wider_dtype = list(_DTYPES.keys())[wider_dtype_index]
if len(tensor_list) == 0:
return None, True

dtypes = [_np.dtype(x.dtype) for x in tensor_list]
if all(dtype == dtypes[0] for dtype in dtypes[1:]):
return dtypes[0], True

dtype_ranks = [_DTYPES.get(dtype) for dtype in dtypes]
if any(rank is None for rank in dtype_ranks):
try:
return _np.result_type(*dtypes), False
except AttributeError as exc:
raise TypeError(
"Cannot determine a common dtype for unsupported dtype(s): "
f"{', '.join(str(dtype) for dtype in dtypes)}"
) from exc

wider_dtype_rank = max(dtype_ranks)
wider_dtype = next(
dtype for dtype, rank in _DTYPES.items() if rank == wider_dtype_rank
)

return wider_dtype, False

Expand Down
23 changes: 11 additions & 12 deletions src/pyrecest/calibration/bias.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ def make_bias_training_examples(
raise ValueError(
"measurement_values and reference_values must have the same target dimension"
)
if feature_values is None:
features = np.empty((measurements.shape[0], 0), dtype=float)
else:
features = _as_2d(feature_values, "feature_values")
if features.shape[0] != measurements.shape[0]:
raise ValueError("feature_values rows must match measurement_values rows")

if reference_times.size == 0:
return BiasTrainingExamples(
measured=np.empty((0, measurements.shape[1])),
Expand All @@ -168,11 +175,7 @@ def make_bias_training_examples(
features=np.empty(
(
0,
(
0
if feature_values is None
else _as_2d(feature_values, "feature_values").shape[1]
),
features.shape[1],
)
),
time_delta_s=np.empty(0),
Expand All @@ -189,13 +192,7 @@ def make_bias_training_examples(
& np.isfinite(references[nearest]).all(axis=1)
& (delta_s <= float(max_time_delta_s))
)
if feature_values is None:
features = np.empty((measurements.shape[0], 0), dtype=float)
else:
features = _as_2d(feature_values, "feature_values")
if features.shape[0] != measurements.shape[0]:
raise ValueError("feature_values rows must match measurement_values rows")
valid &= np.isfinite(features).all(axis=1)
valid &= np.isfinite(features).all(axis=1)
measured = measurements[valid]
reference = references[nearest[valid]]
return BiasTrainingExamples(
Expand All @@ -222,6 +219,8 @@ def fit_sensor_bias_correction_from_examples(
raise ValueError("min_samples must be positive")
y = _as_2d(examples.residual, "examples.residual")
x = _as_2d(examples.features, "examples.features")
if x.shape[0] != y.shape[0]:
raise ValueError("examples.features rows must match examples.residual rows")
valid = np.isfinite(y).all(axis=1) & np.isfinite(x).all(axis=1)
y = y[valid]
x = x[valid]
Expand Down
32 changes: 32 additions & 0 deletions tests/calibration/test_time_offset_bias.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import numpy as np
import numpy.testing as npt
from pyrecest.calibration.bias import (
BiasTrainingExamples,
fit_sensor_bias_correction,
fit_sensor_bias_correction_from_examples,
make_bias_training_examples,
)
from pyrecest.calibration.time_offset import (
Expand Down Expand Up @@ -140,6 +142,36 @@ def test_make_bias_training_examples_uses_nearest_reference(self):

npt.assert_allclose(examples.residual, np.array([[1.0], [1.0]]))

def test_make_bias_training_examples_validates_feature_rows_without_references(
self,
):
with self.assertRaisesRegex(
ValueError,
"feature_values rows must match measurement_values rows",
):
make_bias_training_examples(
np.array([0.0, 1.0]),
np.array([[1.0], [2.0]]),
np.array([]),
np.empty((0, 1)),
feature_values=np.array([[0.0]]),
)

def test_fit_sensor_bias_correction_from_examples_rejects_mismatched_rows(self):
examples = BiasTrainingExamples(
measured=np.zeros((2, 1)),
reference=np.zeros((2, 1)),
residual=np.zeros((2, 1)),
features=np.zeros((1, 1)),
time_delta_s=np.zeros(2),
)

with self.assertRaisesRegex(
ValueError,
"examples.features rows must match examples.residual rows",
):
fit_sensor_bias_correction_from_examples(examples, min_samples=1)

def test_fit_sensor_bias_correction_subtracts_predicted_bias(self):
times = np.arange(8.0)
reference = np.column_stack([times, -times])
Expand Down
12 changes: 12 additions & 0 deletions tests/test_backend_contract.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import unittest

import numpy as np
import numpy.testing as npt
import pyrecest.backend as backend
from pyrecest._backend import BACKEND_ATTRIBUTES
Expand All @@ -15,6 +16,17 @@ def test_backend_attribute_lists_do_not_contain_duplicates(self):
)
self.assertEqual(duplicates, [])

def test_convert_to_wider_dtype_preserves_matching_boolean_dtype(self):
if backend.__backend_name__ not in {"autograd", "numpy"}:
self.skipTest("shared NumPy dtype promotion regression test")

first, second = backend.convert_to_wider_dtype(
[array([True, False]), array([False, True])]
)

self.assertEqual(to_numpy(first).dtype, np.dtype("bool"))
self.assertEqual(to_numpy(second).dtype, np.dtype("bool"))

def test_choice_supports_numpy_like_size_replace_and_probabilities(self):
values = array([0, 1, 2, 3])
weights = array([0.1, 0.2, 0.3, 0.4])
Expand Down
Loading