From 73b2e0547cb44e55818587e133645e5ffa28d06f Mon Sep 17 00:00:00 2001 From: Daniel Gaskins Date: Thu, 20 Aug 2026 13:24:06 -0700 Subject: [PATCH] fix: make NumPy object arrays JSON-safe Signed-off-by: Daniel Gaskins --- aisteer360/evaluation/utils/data_utils.py | 6 ++--- tests/core/test_benchmark.py | 30 +++++++++++++++++++++-- tests/core/test_evaluation_utils.py | 19 ++++++++++++-- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/aisteer360/evaluation/utils/data_utils.py b/aisteer360/evaluation/utils/data_utils.py index 5b329cfb..0f1277c5 100644 --- a/aisteer360/evaluation/utils/data_utils.py +++ b/aisteer360/evaluation/utils/data_utils.py @@ -13,7 +13,7 @@ def to_jsonable(obj: Any) -> Any: - Path: str(path) - mappings: recurse, stringify keys - sequences: recurse on elements - - numpy scalars/arrays: convert to Python / list + - numpy scalars/arrays: convert to Python / list, then recurse - everything else: repr(obj) """ from pathlib import Path as _Path @@ -28,7 +28,7 @@ def to_jsonable(obj: Any) -> Any: return obj.item() if isinstance(obj, np.ndarray): - return obj.tolist() + return to_jsonable(obj.tolist()) if isinstance(obj, Mapping): return {str(k): to_jsonable(v) for k, v in obj.items()} @@ -38,7 +38,7 @@ def to_jsonable(obj: Any) -> Any: if callable(obj): return f"callable:{getattr(obj, '__qualname__', type(obj).__name__)}" - + return repr(obj) diff --git a/tests/core/test_benchmark.py b/tests/core/test_benchmark.py index e94ce82c..9d876b8b 100644 --- a/tests/core/test_benchmark.py +++ b/tests/core/test_benchmark.py @@ -16,8 +16,10 @@ test are the package implementations. """ import json +from pathlib import Path from unittest.mock import MagicMock +import numpy as np import pytest from aisteer360.algorithms.core.specs import ControlSpec @@ -118,8 +120,6 @@ def test_num_trials_coerced_to_int(self, sample_evaluation_data): assert isinstance(benchmark.num_trials, int) def test_save_dir_coerced_to_path(self, sample_evaluation_data, tmp_path): - from pathlib import Path - benchmark = Benchmark( use_case=_make_use_case(sample_evaluation_data), base_model_name_or_path="test-model", @@ -439,6 +439,32 @@ def test_mixed_spec_and_concrete_raises(self, sample_evaluation_data, mock_base_ class TestBenchmarkCheckpointing: """Tests for incremental checkpointing and resume.""" + def test_checkpoint_converts_nested_numpy_objects(self, sample_evaluation_data, tmp_path): + benchmark = Benchmark( + use_case=_make_use_case(sample_evaluation_data), + base_model_name_or_path="test-model", + steering_pipelines={"baseline": []}, + save_dir=tmp_path, + ) + profiles = { + "baseline": [ + { + "params": np.array( + [Path("models/base"), {"layers": {1, 2}}], + dtype=object, + ) + } + ] + } + + benchmark._save_checkpoint(profiles) + + with (tmp_path / "checkpoint.json").open(encoding="utf-8") as handle: + saved = json.load(handle) + saved_params = saved["baseline"][0]["params"] + assert saved_params[0] == "models/base" + assert sorted(saved_params[1]["layers"]) == [1, 2] + def test_checkpoint_written(self, sample_evaluation_data, mock_base_model, tmp_path): use_case = _make_use_case(sample_evaluation_data) benchmark = Benchmark( diff --git a/tests/core/test_evaluation_utils.py b/tests/core/test_evaluation_utils.py index c83954c5..ee64a97a 100644 --- a/tests/core/test_evaluation_utils.py +++ b/tests/core/test_evaluation_utils.py @@ -7,6 +7,9 @@ - Visualization utilities (plot functions) """ +import json +from pathlib import Path + import numpy as np import pandas as pd import pytest @@ -149,8 +152,6 @@ def test_primitive_passthrough(self): def test_path_to_string(self): """Test that Path objects become strings.""" - from pathlib import Path - result = to_jsonable(Path("/some/path")) assert result == "/some/path" assert isinstance(result, str) @@ -168,6 +169,20 @@ def test_numpy_array(self): assert result == [1, 2, 3] assert isinstance(result, list) + def test_numpy_object_array_recurses(self): + """Test nested objects in numpy arrays become JSON-safe.""" + arr = np.array( + [Path("models/base"), {"layers": {1, 2}}], + dtype=object, + ) + + result = to_jsonable(arr) + encoded = json.dumps(result) + + assert result[0] == "models/base" + assert sorted(result[1]["layers"]) == [1, 2] + assert json.loads(encoded) == result + def test_nested_dict(self): """Test nested dict conversion.""" data = {"a": np.float64(1.0), "b": {"c": np.array([1, 2])}}