From 5ab175fd0478e8c9ef0f06f99d928967eaefee2f Mon Sep 17 00:00:00 2001 From: Daniel Kohler <11864045+ddkohler@users.noreply.github.com> Date: Fri, 16 Jan 2026 08:38:02 -0600 Subject: [PATCH 1/6] np.ndarray output types --- attune/_discrete_tune.py | 25 ++++++++++--------------- attune/_tune.py | 2 +- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/attune/_discrete_tune.py b/attune/_discrete_tune.py index 52ae447..a0c53a2 100644 --- a/attune/_discrete_tune.py +++ b/attune/_discrete_tune.py @@ -35,7 +35,7 @@ def __init__( def __repr__(self): return f"DiscreteTune({repr(self.ranges)}, {repr(self.default)})" - def __call__(self, ind_value, *, ind_units=None): + def __call__(self, ind_value, *, ind_units=None) -> np.ndarray: """Evaluate the DiscreteTune at specific independent value(s). Paramters @@ -54,20 +54,15 @@ def __call__(self, ind_value, *, ind_units=None): """ if ind_units is not None and self._ind_units is not None: ind_value = wt.units.convert(ind_value, ind_units, self._ind_units) - if isinstance(ind_value, np.ndarray): - out = np.full( - ind_value.shape, - self.default, - dtype=f"U{max([len(s) for s in self.ranges.keys()])}", - ) - for key, (imin, imax) in self.ranges.items(): - out[(ind_value >= imin) & (ind_value <= imax)] = key - return out - else: - for key, (imin, imax) in self.ranges.items(): - if imin <= ind_value <= imax: - return key - return self.default + ind_value = np.asarray(ind_value) + out = np.full( + ind_value.shape, + self.default, + dtype=f"U{max([len(s) for s in self.ranges.keys()])}", + ) + for key, (imin, imax) in self.ranges.items(): + out[(ind_value >= imin) & (ind_value <= imax)] = key + return out def __eq__(self, other): return self.ranges == other.ranges and self.default == other.default diff --git a/attune/_tune.py b/attune/_tune.py index 6b4128e..4b2a5ad 100644 --- a/attune/_tune.py +++ b/attune/_tune.py @@ -55,7 +55,7 @@ def __repr__(self): return f"Tune({repr(self.independent)}, {repr(self.dependent)})" return f"Tune({repr(self.independent)}, {repr(self.dependent)}, dep_units={repr(self.dep_units)})" - def __call__(self, ind_value, *, ind_units=None, dep_units=None): + def __call__(self, ind_value, *, ind_units=None, dep_units=None) -> np.ndarray: if ind_units is not None and self._ind_units is not None: ind_value = wt.units.convert(ind_value, ind_units, self._ind_units) ret = self._interp(ind_value) From 86da6ed49cf7a8d8d73a4239ef65e70eda0891cf Mon Sep 17 00:00:00 2001 From: Daniel Kohler <11864045+ddkohler@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:34:08 -0600 Subject: [PATCH 2/6] tests --- tests/discrete_tune.py | 13 ++++++------- tests/instrument/test_update.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/discrete_tune.py b/tests/discrete_tune.py index 042d3e0..99fda7f 100644 --- a/tests/discrete_tune.py +++ b/tests/discrete_tune.py @@ -1,12 +1,11 @@ import attune +import numpy as np def test_discrete(): dt = attune.DiscreteTune({"hi": (100, 200), "lo": (10, 20), "med": (20, 100)}, default="def") - assert dt(150) == "hi" - assert dt(20) == "lo" - assert dt(15) == "lo" - assert dt(100) == "hi" - assert dt(70) == "med" - assert dt(5) == "def" - assert dt(500) == "def" + x = [150, 20, 15, 100, 70, 5, 500] + y = ["hi", "lo", "lo", "hi", "med", "def", "def"] + assert dt(x) == np.asarray(y) + for xi, yi in zip(x,y): + assert dt(xi).item() == yi diff --git a/tests/instrument/test_update.py b/tests/instrument/test_update.py index 06ce880..106c8af 100644 --- a/tests/instrument/test_update.py +++ b/tests/instrument/test_update.py @@ -46,7 +46,7 @@ def test_update_existing(): inst2 = attune.Instrument({"arr": arr_new}, {"tune": attune.Setable("tune")}) inst_new = attune.update_merge(inst, inst2) assert math.isclose(inst_new(0.5)["tune"], 1.5) - assert inst_new(0.5)["discrete"] == "med" + assert inst_new(0.5)["discrete"] == inst(0.5)["discrete"] assert inst_new.transition.type == "update_merge" From ceb729ee95700a69470b4b26294c4efc2681fe06 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:34:18 +0000 Subject: [PATCH 3/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/discrete_tune.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/discrete_tune.py b/tests/discrete_tune.py index 99fda7f..729fa45 100644 --- a/tests/discrete_tune.py +++ b/tests/discrete_tune.py @@ -7,5 +7,5 @@ def test_discrete(): x = [150, 20, 15, 100, 70, 5, 500] y = ["hi", "lo", "lo", "hi", "med", "def", "def"] assert dt(x) == np.asarray(y) - for xi, yi in zip(x,y): + for xi, yi in zip(x, y): assert dt(xi).item() == yi From 082b05d72337a1a5867ec5574c30b77f3f99579e Mon Sep 17 00:00:00 2001 From: Daniel Kohler <11864045+ddkohler@users.noreply.github.com> Date: Fri, 16 Jan 2026 09:51:58 -0600 Subject: [PATCH 4/6] fix numpy order bug --- attune/_discrete_tune.py | 3 ++- tests/discrete_tune.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/attune/_discrete_tune.py b/attune/_discrete_tune.py index a0c53a2..74cb650 100644 --- a/attune/_discrete_tune.py +++ b/attune/_discrete_tune.py @@ -60,7 +60,8 @@ def __call__(self, ind_value, *, ind_units=None) -> np.ndarray: self.default, dtype=f"U{max([len(s) for s in self.ranges.keys()])}", ) - for key, (imin, imax) in self.ranges.items(): + # work in reverse so that the first valid entry persists + for key, (imin, imax) in reversed(self.ranges.items()): out[(ind_value >= imin) & (ind_value <= imax)] = key return out diff --git a/tests/discrete_tune.py b/tests/discrete_tune.py index 729fa45..1d6bff7 100644 --- a/tests/discrete_tune.py +++ b/tests/discrete_tune.py @@ -6,6 +6,6 @@ def test_discrete(): dt = attune.DiscreteTune({"hi": (100, 200), "lo": (10, 20), "med": (20, 100)}, default="def") x = [150, 20, 15, 100, 70, 5, 500] y = ["hi", "lo", "lo", "hi", "med", "def", "def"] - assert dt(x) == np.asarray(y) - for xi, yi in zip(x, y): - assert dt(xi).item() == yi + assert np.all(dt(x) == np.asarray(y)) + assert dt(20) == dt(np.array(20)) == "lo" # should choose first valid range + assert all([dt(xi).item() == yi for xi, yi in zip(x, y)]) From 46efb5da4c625d62503ba6aa748f7077d0badd03 Mon Sep 17 00:00:00 2001 From: Daniel Kohler <11864045+ddkohler@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:03:30 -0600 Subject: [PATCH 5/6] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8ee4e7..755c23b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] +## Changed +- DiscreteTune.__call__ will now always return a numpy.ndarray object, regardless of argument type + +## Fixed +- fixed bug where DiscreteTune did not respect order of identifiers when called with an array + ## [0.5.1] ### Added From 0ed802e87258c55240ec77251f91dd562156ead5 Mon Sep 17 00:00:00 2001 From: Daniel Kohler <11864045+ddkohler@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:03:39 -0600 Subject: [PATCH 6/6] Update structure.rst --- docs/structure.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/structure.rst b/docs/structure.rst index 3b43428..c4cdd7a 100644 --- a/docs/structure.rst +++ b/docs/structure.rst @@ -78,15 +78,15 @@ You can, however, place higher priority (earlier) ranges inside of other ranges .. code-block:: python dt = attune.DiscreteTune({"hi": (100, 200), "lo": (10, 20), "inner": (50, 60), "med": (20, 100)}, default="def") - dt(5) == "def" - dt(15) == "lo" - dt(20) == "lo" - dt(30) == "med" - dt(55) == "inner" - dt(70) == "med" - dt(100) == "hi" - dt(150) == "hi" - dt(500) == "def" + dt(5) == np.array("def") + dt(15) == np.array("lo") + dt(20) == np.array("lo") + dt(30) == np.array("med") + dt(55) == np.array("inner") + dt(70) == np.array("med") + dt(100) == np.array("hi") + dt(150) == np.array("hi") + dt(500) == np.array("def")