From d40447df51821856b966d774a1db9bd53a63f868 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Tue, 24 Feb 2026 14:31:34 +1100 Subject: [PATCH 1/8] remove pinlist requirement when not needed --- src/fixate/_switching.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index f9209d7e..94d06fae 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -89,7 +89,6 @@ def __or__(self, other: PinUpdate) -> PinUpdate: class VirtualMux: - pin_list: PinList = () clearing_time: float = 0.0 ########################################################################### @@ -108,11 +107,9 @@ def __init__(self, update_pins: Optional[PinUpdateCallback] = None): # mux is defined with a map_tree, we need the ordering. But after # initialisation, we only need set operations on the pin list, so # we convert here and keep a reference to the set for future use. - self._pin_set = frozenset(self.pin_list) - self._state = "" - self._signal_map: SignalMap = self._map_signals() + self._signal_map, self._pin_set = self._map_signals() # Define the implicit signal "" which can be used to turn off all pins. # If the signal map already has this defined, raise an error. In the old @@ -218,7 +215,7 @@ def _calculate_pins( # The following methods are intended as implementation detail and # subclasses should avoid overriding. - def _map_signals(self) -> SignalMap: + def _map_signals(self) -> tuple[SignalMap, PinSet]: """ Default implementation of the signal mapping @@ -231,9 +228,18 @@ def _map_signals(self) -> SignalMap: map_tree or map_list. """ if hasattr(self, "map_tree"): - return self._map_tree(self.map_tree, self.pin_list, fixed_pins=frozenset()) + if not hasattr(self, "pin_list"): + raise ValueError("pin_list must not be None if defining map_tree") + return self._map_tree( + self.map_tree, self.pin_list, fixed_pins=frozenset() + ), frozenset(self.pin_list) elif hasattr(self, "map_list"): - return {sig: frozenset(pins) for sig, *pins in self.map_list} + pin_set = set() + signal_map = {} + for sig, *pins in self.map_list: + pin_set.update(pins) + signal_map[sig] = frozenset(pins) + return signal_map, frozenset(pin_set) else: raise ValueError( "VirtualMux subclass must define either map_tree or map_list" @@ -477,7 +483,7 @@ def __init__( self, update_pins: Optional[PinUpdateCallback] = None, ): - if not self.pin_list: + if not hasattr(self, "pin_list"): self.pin_list = [self.pin_name] super().__init__(update_pins) From 87320abaa43f3a157024fb1ef3761641d8009473 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Tue, 24 Feb 2026 14:46:19 +1100 Subject: [PATCH 2/8] add duplicate pin detection to jigs --- src/fixate/_switching.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index 94d06fae..1e9287f0 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -47,6 +47,7 @@ from dataclasses import dataclass from functools import reduce from operator import or_ +from collections import defaultdict Signal = str Pin = str @@ -758,6 +759,9 @@ def _validate(self) -> None: - Ensure all pins that are used in muxes are defined by some address handler. + - Ensure that mux pins are unique so that muxes do not affect + eachother + Note: It is O.K. for there to be AddressHandler pins that are not used anywhere. Eventually we might choose to warn about them. This it is necessary to define some jigs. @@ -766,16 +770,27 @@ def _validate(self) -> None: or_, (set(handler.pin_list) for handler in self._handlers), set() ) mux_missing_pins = [] - + all_mux_pins: defaultdict[Pin, list[str]] = defaultdict(list) for mux in self.mux.get_multiplexers(): if unknown_pins := mux.pins() - all_handler_pins: mux_missing_pins.append((mux, unknown_pins)) + for pin in mux.pins(): + # record what we have seen to find duplicates + all_mux_pins[pin].append(str(mux)) if mux_missing_pins: raise ValueError( f"One or more VirtualMux uses unknown pins:\n{mux_missing_pins}" ) + duplicates = [ + f"Pin {pin} found in {', '.join(muxes)}" + for pin, muxes in all_mux_pins.items() + if len(muxes) > 1 + ] + if duplicates: + raise ValueError(duplicates) + _T = TypeVar("_T") From bc0d0dcb65883fc46a5ba0d00a25a7bcd3fca3c4 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Tue, 24 Feb 2026 15:08:34 +1100 Subject: [PATCH 3/8] update example --- examples/jig_driver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/jig_driver.py b/examples/jig_driver.py index 6d56cdf6..f6a7cbc0 100644 --- a/examples/jig_driver.py +++ b/examples/jig_driver.py @@ -14,7 +14,6 @@ class MuxOne(VirtualMux): - pin_list = ("x0", "x1", "x2") map_list = ( ("sig1", "x0"), ("sig2", "x1"), From 515687abf81d63fdc00e84fd1055a049b6f4cf87 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Tue, 24 Feb 2026 15:09:41 +1100 Subject: [PATCH 4/8] bump release notes --- docs/release-notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 1d4a4ac4..cdd4633c 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -20,6 +20,7 @@ Improvements - Sequencer logic now handles exceptions raised on sequence abort. GUI will no longer hang when a test raises an exception during a test abort. - Fix bug where DSOX1202G appeared to hang both the program and scope - LCR Driver now supports instruments reporting as Keysight or Agilent. Newer models of the LCR meter report as Keysight, whereas older models report as Agilent. +- Jig switching will now raise an error when pins are not unique across muxes ************* Version 0.6.4 From aa177cc2e591650e9697e9e91e18285627687768 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Tue, 24 Feb 2026 15:18:41 +1100 Subject: [PATCH 5/8] bump release notes again --- docs/release-notes.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index cdd4633c..86602503 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -20,7 +20,8 @@ Improvements - Sequencer logic now handles exceptions raised on sequence abort. GUI will no longer hang when a test raises an exception during a test abort. - Fix bug where DSOX1202G appeared to hang both the program and scope - LCR Driver now supports instruments reporting as Keysight or Agilent. Newer models of the LCR meter report as Keysight, whereas older models report as Agilent. -- Jig switching will now raise an error when pins are not unique across muxes +- Jig switching will now raise an error when pins are not unique across muxes. Requirement for pin_list has been dropped when not using map_tree due to issue + where pins would be set but never cleared when a pin was defined in the mux signals but not in the mux pins. ************* Version 0.6.4 From f56b0a2124fea5b74977620e5f56d06ddbd8b9d6 Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Tue, 24 Feb 2026 16:30:08 +1100 Subject: [PATCH 6/8] fix broken test for special case --- src/fixate/_switching.py | 21 +++++++++++++++++---- test/test_switching.py | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index 1e9287f0..f8436ec2 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -237,10 +237,23 @@ def _map_signals(self) -> tuple[SignalMap, PinSet]: elif hasattr(self, "map_list"): pin_set = set() signal_map = {} - for sig, *pins in self.map_list: - pin_set.update(pins) - signal_map[sig] = frozenset(pins) - return signal_map, frozenset(pin_set) + # so great think about the unpack operator (*) is that it gives you a list of items + # which means that a string, or sequence of strings look the same as a sequence of sequences of strings + # when unpacked this way + # filter out these edge cases. + # if you want to have a one signal mux, then use a virtual switch instead, + # or use the correct definition of map_list + if isinstance(self.map_list, str) or all( + isinstance(item, str) for item in self.map_list + ): + raise ValueError( + "map_list should be in the form (('sig1', 'pin1', 'pin2',...)\n('sig2', 'pin3', 'pin4',...)\n...)" + ) + else: + for sig, *pins in self.map_list: + pin_set.update(pins) + signal_map[sig] = frozenset(pins) + return signal_map, frozenset(pin_set) else: raise ValueError( "VirtualMux subclass must define either map_tree or map_list" diff --git a/test/test_switching.py b/test/test_switching.py index 527f1d2c..73135253 100644 --- a/test/test_switching.py +++ b/test/test_switching.py @@ -551,7 +551,7 @@ def test_jig_driver_with_unknown_pins(): class Mux(VirtualMux): pin_list = ("x0", "x1") # "x1" isn't in either handler - map_list = ("sig1", "x1") + map_list = (("sig1", "x1"),) class Group(MuxGroup): def __init__(self): From 8df06c2900e705ae59d4e7c6cee0dd634789df9e Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Tue, 24 Feb 2026 16:45:53 +1100 Subject: [PATCH 7/8] add some tests --- test/test_switching.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/test_switching.py b/test/test_switching.py index 73135253..1555576d 100644 --- a/test/test_switching.py +++ b/test/test_switching.py @@ -592,3 +592,42 @@ def test_pin_update_or(): 2.0, ) assert expected == a | b + + +@pytest.mark.parametrize("bad_map_list", ("single_string", ("tuple", "of", "strings"))) +def test_unsupported_map_lists_raise(bad_map_list): + class Mux(VirtualMux): + map_list = bad_map_list + + with pytest.raises(ValueError): + Mux() + + +def test_duplicate_pins_raise(): + handler = AddressHandler(("x0", "x1")) + + class Mux1(VirtualMux): + map_list = (("sig1", "x0"),) + + class Mux2(VirtualMux): + map_list = (("sig2", "x1"),) + + class Mux3(VirtualMux): + map_list = (("sig3", "x0", "x1"),) + + class GoodGroup(MuxGroup): + def __init__(self): + self.mux1 = Mux1() + self.mux2 = Mux2() + + class BadGroup(MuxGroup): + def __init__(self): + self.mux1 = Mux1() + self.mux3 = Mux3() + + # this is ok + JigDriver(GoodGroup, [handler]) + + # overlap of pins is bad, mux3 can control mux1 + with pytest.raises(ValueError): + JigDriver(BadGroup, [handler]) From b6e53f26fe379d54d2d46ce1d4b5acacf5daf3fe Mon Sep 17 00:00:00 2001 From: Daniel Montanari Date: Tue, 24 Feb 2026 16:58:38 +1100 Subject: [PATCH 8/8] add one more test and make mypy happy --- src/fixate/_switching.py | 9 +++++---- test/test_switching.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/fixate/_switching.py b/src/fixate/_switching.py index f8436ec2..dc3774e2 100644 --- a/src/fixate/_switching.py +++ b/src/fixate/_switching.py @@ -230,10 +230,11 @@ def _map_signals(self) -> tuple[SignalMap, PinSet]: """ if hasattr(self, "map_tree"): if not hasattr(self, "pin_list"): - raise ValueError("pin_list must not be None if defining map_tree") - return self._map_tree( - self.map_tree, self.pin_list, fixed_pins=frozenset() - ), frozenset(self.pin_list) + raise ValueError("must include pin_list if defining map_tree") + else: + return self._map_tree( + self.map_tree, self.pin_list, fixed_pins=frozenset() + ), frozenset(self.pin_list) elif hasattr(self, "map_list"): pin_set = set() signal_map = {} diff --git a/test/test_switching.py b/test/test_switching.py index 1555576d..e9adc509 100644 --- a/test/test_switching.py +++ b/test/test_switching.py @@ -631,3 +631,17 @@ def __init__(self): # overlap of pins is bad, mux3 can control mux1 with pytest.raises(ValueError): JigDriver(BadGroup, [handler]) + + +def test_map_tree_missing_pin_list_raises(): + class GoodTree(VirtualMux): + pin_list = "1" + map_tree = ("a", "b") + + GoodTree() + + class BadTree(VirtualMux): + map_tree = ("a", "b") + + with pytest.raises(ValueError): + BadTree()