Skip to content
Open
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: 2 additions & 0 deletions docs/release-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +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. 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
Expand Down
1 change: 0 additions & 1 deletion examples/jig_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@


class MuxOne(VirtualMux):
pin_list = ("x0", "x1", "x2")
map_list = (
("sig1", "x0"),
("sig2", "x1"),
Expand Down
53 changes: 44 additions & 9 deletions src/fixate/_switching.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from dataclasses import dataclass
from functools import reduce
from operator import or_
from collections import defaultdict

Signal = str
Pin = str
Expand Down Expand Up @@ -89,7 +90,6 @@ def __or__(self, other: PinUpdate) -> PinUpdate:


class VirtualMux:
pin_list: PinList = ()
clearing_time: float = 0.0

###########################################################################
Expand All @@ -108,11 +108,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
Expand Down Expand Up @@ -218,7 +216,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

Expand All @@ -231,9 +229,32 @@ 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("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"):
return {sig: frozenset(pins) for sig, *pins in self.map_list}
pin_set = set()
signal_map = {}
# 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"
Expand Down Expand Up @@ -477,7 +498,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)

Expand Down Expand Up @@ -752,6 +773,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.
Expand All @@ -760,16 +784,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")

Expand Down
55 changes: 54 additions & 1 deletion test/test_switching.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -592,3 +592,56 @@ 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])


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()