Repository: wright-group/attune

Issue:
Incorrect ind_min and ind_max on arrangement after map
```
tune = attune.Tune(np.linspace(1300, 1400, 20), np.linspace(-5, 5, 20))
arr = attune.Arrangement("map", {"test": tune})
inst0 = attune.Instrument({"map": arr}, {"test": attune.Setable("tune")})
inst1 = attune.map_ind_points(inst0, "map", "test", np.linspace(1310, 1450, 25))

print(inst0(1373))
print(inst1(1373))
print(inst0["map"].ind_min, inst0["map"].ind_max)
print(inst1["map"].ind_min, inst1["map"].ind_max)
```

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..28c3b65
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,17 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/).
+
+## [Unreleased]
+
+### Fixed
+- Writing NDarrays to `instrument.json` files
+
+## [0.4.0]
+
+### Added
+- initial release after a major rewrite
+
+[Unreleased]: https://github.com/wright-group/attune/compare/0.4.0...master
+[0.4.0]: https://github.com/wright-group/attune/releases/tag/0.4.0
diff --git a/attune/_arrangement.py b/attune/_arrangement.py
index bab449f..d4d867e 100644
--- a/attune/_arrangement.py
+++ b/attune/_arrangement.py
@@ -31,8 +31,6 @@ class Arrangement:
             k: Tune(**v) if isinstance(v, dict) else v for k, v in tunes.items()
         }
         self._ind_units: str = "nm"
-        self._ind_max: float = min([t.ind_max for t in self._tunes.values()])
-        self._ind_min: float = max([t.ind_min for t in self._tunes.values()])
 
     def __repr__(self):
         return f"Arrangement({repr(self.name)}, {repr(self.tunes)})"
@@ -81,11 +79,11 @@ class Arrangement:
 
     @property
     def ind_max(self):
-        return self._ind_max
+        return min([t.ind_max for t in self._tunes.values()])
 
     @property
     def ind_min(self):
-        return self._ind_min
+        return max([t.ind_min for t in self._tunes.values()])
 
     @property
     def name(self):
diff --git a/attune/_instrument.py b/attune/_instrument.py
index a3260eb..7bf50db 100644
--- a/attune/_instrument.py
+++ b/attune/_instrument.py
@@ -133,4 +133,11 @@ class Instrument(object):
 
     def save(self, file):
         """Save the JSON representation into an open file."""
-        json.dump(self.as_dict(), file)
+
+        class NdarrayEncoder(json.JSONEncoder):
+            def default(self, obj):
+                if hasattr(obj, "tolist"):
+                    return obj.tolist()
+                return json.JSONEncoder.default(self, obj)
+
+        json.dump(self.as_dict(), file, cls=NdarrayEncoder)
diff --git a/attune/_tune.py b/attune/_tune.py
index 259e389..9e9e50c 100644
--- a/attune/_tune.py
+++ b/attune/_tune.py
@@ -31,8 +31,6 @@ class Tune:
         dependent = np.asarray(dependent)
         assert independent.size == dependent.size
         assert independent.ndim == dependent.ndim == 1
-        self._ind_max = max(independent)
-        self._ind_min = min(independent)
         self._ind_units = "nm"
         self._dep_units = dep_units
         self._interp = scipy.interpolate.interp1d(independent, dependent, fill_value="extrapolate")
@@ -79,11 +77,11 @@ class Tune:
 
     @property
     def ind_max(self):
-        return self._ind_max
+        return self.independent.max()
 
     @property
     def ind_min(self):
-        return self._ind_min
+        return self.independent.min()
 
     @property
     def ind_units(self):


Test patch:
diff --git a/tests/map.py b/tests/map.py
index d3d3887..351ca18 100644
--- a/tests/map.py
+++ b/tests/map.py
@@ -16,6 +16,8 @@ def test_map_ind_points():
         inst1["test_map"]["test"](test_points), inst0["test_map"]["test"](test_points)
     )
     assert len(inst1["test_map"]["test"]) == 25
+    assert inst1["test_map"].ind_min == 1310
+    assert inst1["test_map"].ind_max == 1450
 
 
 def test_map_ind_limits():
diff --git a/tests/store/test_store.py b/tests/store/test_store.py
index 97c32e7..8109c30 100644
--- a/tests/store/test_store.py
+++ b/tests/store/test_store.py
@@ -4,6 +4,7 @@ import shutil
 import tempfile
 
 import attune
+import numpy as np
 import pytest
 
 here = pathlib.Path(__file__).parent
@@ -46,3 +47,12 @@ def test_load_store():
     instr = attune.load("test")
     with pytest.warns(UserWarning, match="Attempted to store instrument equivalent"):
         attune.store(instr)
+
+
+@temp_store
+def test_store_ndarray():
+    instr = attune.load("test")
+    instr = attune.map_ind_points(instr, "arr", "tune", np.linspace(0.25, 1, 5))
+    # Would raise here because it is trying to serialize the ndarray in metadata
+    # prior to bug fix
+    attune.store(instr)