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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ readme = "README.md"
version = "0.3.1"

dependencies = [
"aind-behavior-dynamic-foraging[data] @ git+https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git@ac5ddbf909c9375b9e8875d6a5f90796cfa98653",
"aind-behavior-dynamic-foraging[data] @ git+https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git@v0.0.2rc36",
"ipykernel",
]

Expand Down
30 changes: 25 additions & 5 deletions src/dynamic_foraging_processing/qc/_core/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
#: timezone-aware datetimes).
SEATTLE_TZ = ZoneInfo("America/Los_Angeles")

#: Decimal places metric values are rounded to when converted for serialization.
VALUE_DECIMALS = 3

#: Map ``contraqctor`` test statuses onto schema statuses. Warnings become
#: ``PENDING`` (needs review); skips count as passing.
STATUS_CONVERTER: t.Dict[qc.Status, Status] = {
Expand Down Expand Up @@ -81,21 +84,38 @@ def bool_to_status(
def to_builtin(value: t.Any) -> t.Any:
"""Convert numpy scalars/arrays to JSON-serializable Python builtins.

Recurses into containers: a numpy scalar nested inside a dict, list, tuple,
or set is converted too. Test results carry whole dicts of numpy values, and
pydantic refuses to serialize any numpy type that survives.

Floats are rounded to ``VALUE_DECIMALS`` decimal places — a metric reporting
``float32`` sensor readings otherwise writes out its full binary expansion
(``39.563472747802734``), which is noise rather than precision.

Parameters
----------
value : Any
A value that may be a numpy scalar or array.
A value that may be a numpy scalar or array, or a container holding
them at any depth.

Returns
-------
Any
The equivalent Python builtin (``list`` for arrays, ``item()`` for
scalars), or ``value`` unchanged when it is not a numpy type.
The equivalent Python builtin (``list`` for arrays and other sequences,
rounded ``float`` for floating-point values, dicts rebuilt with
converted keys and values), or ``value`` unchanged when it is neither a
numpy type, a float, nor a container.
"""
if isinstance(value, np.ndarray):
return value.tolist()
return to_builtin(value.tolist())
if isinstance(value, np.generic):
return value.item()
return to_builtin(value.item())
if isinstance(value, float):
return round(value, VALUE_DECIMALS)
if isinstance(value, t.Mapping):
return {to_builtin(key): to_builtin(item) for key, item in value.items()}
if isinstance(value, (list, tuple, set, frozenset)):
return [to_builtin(item) for item in value]
return value


Expand Down
26 changes: 26 additions & 0 deletions tests/test_qc/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,32 @@ def test_to_builtin_converts_numpy_types():
assert _schema.to_builtin("text") == "text"


def test_to_builtin_recurses_into_containers():
"""Numpy values nested in dicts/sequences are converted too."""
nested = {
"scalar": np.float32(1.5),
"array": np.array([1, 2]),
"rows": [(np.int64(3), {"deep": np.float32(0.25)})],
}
converted = _schema.to_builtin(nested)
assert converted == {"scalar": 1.5, "array": [1, 2], "rows": [[3, {"deep": 0.25}]]}
assert isinstance(converted["scalar"], float)
assert isinstance(converted["rows"][0][0], int)
assert isinstance(converted["rows"][0][1]["deep"], float)
assert _schema.to_builtin({np.int64(1)}) == [1]
assert _schema.to_builtin(frozenset({np.int64(2)})) == [2]
assert list(_schema.to_builtin({np.int64(4): "v"})) == [4]


def test_to_builtin_rounds_floats():
"""Floats are rounded to three decimals, at any depth; ints are untouched."""
assert _schema.to_builtin(np.float32(39.563472747802734)) == 39.563
assert _schema.to_builtin(1.23456) == 1.235
assert _schema.to_builtin(np.array([1.23456, 2.0])) == [1.235, 2.0]
assert _schema.to_builtin({"mean": np.float32(26.0916690826416)}) == {"mean": 26.092}
assert _schema.to_builtin(np.int64(123456)) == 123456


def test_make_metric_defaults_and_overrides():
"""``make_metric`` stamps modality/stage and defaults tags to ``{}``."""
status = _schema.bool_to_status(True)
Expand Down
Loading