Closed EvanKirshenbaum closed 5 months ago
This issue was referenced by the following commits before migration:
So, what I settled on is a mix-in class ExternalComponent
:
class ExternalComponent:
number: int = -1
@property
def pads_for_sort(self) -> Sequence[Pad]:
return ()
def join_system(self, system: System) -> None: # @UnusedVariable
...
def reset_component(self) -> None:
me = self
if isinstance(me, BinaryComponent):
me.current_state = OnOff.OFF
EC = TypeVar('EC', bound=ExternalComponent)
which is, currently, a base class of TemperatureControl
, Magnet
, PowerSupply
, Fan
, and Pipettor
.
ExternalComponent
is hooked into Board
by
class Board(SystemComponent):
_external_components: Final[list[ExternalComponent]]
@property
def external_components(self) -> Sequence[ExternalComponent]:
return self._external_components
def find_all(self, of_type: Type[EC]) -> Sequence[EC]:
return [c for c in self.external_components if isinstance(c, of_type)]
@overload
def find_one(self, of_type: type[EC], *, if_missing: ValOrFn[str]) -> EC: ... # @UnusedVariable
@overload
def find_one(self, of_type: type[EC], *, if_missing: Missing = MISSING) -> Optional[EC]: ... # @UnusedVariable
def find_one(self, of_type: type[EC], *,
if_missing: MissingOr[ValOrFn[str]] = MISSING) -> Optional[EC]:
all_found = self.find_all(of_type)
if len(all_found) > 0:
return all_found[0]
if if_missing is MISSING:
return None
assert False, ensure_val(if_missing, str)
@cached_property
def pipettor(self) -> Pipettor:
from mpam.pipettor import Pipettor
return self.find_one(Pipettor, # type: ignore [type-abstract]
if_missing=lambda: f"{self} doesn't have a registered pipettor.")
def _add_external(self, cpt_type: type[EC], cpt: EC) -> int:
known = self.find_all(cpt_type)
n = len(known)
cpt.number = n+1
self._external_components.append(cpt)
return n
def _maybe_add_external(self, cpt_type: type[EC], cpt: Optional[EC]) -> None:
if cpt is not None:
self._add_external(cpt_type, cpt)
def _add_externals(self, cpt_type: type[EC], cpts: Sequence[EC], *,
order: Optional[RCOrder] = None) -> None:
if order is None:
order = self.component_layout
cpts = self.sorted(cpts, get_pads = lambda c: c.pads_for_sort, order=order)
for c in cpts:
self._add_external(cpt_type, c)
def join_system(self, system: System)->None:
super().join_system(system)
for cpt in self.external_components:
cpt.join_system(system)
def reset_components(self, of_type: type[ExternalComponent] = ExternalComponent) -> None:
for cpt in self.find_all(of_type):
cpt.reset_component()
def reset_all(self) -> None:
self.reset_pads()
self.reset_components()
ExternalComponent
has a number
, which is assigned by Board.add_external()
. join_system()
is called by Board.join_system()
.
Pipettor
delegates to its SystemComponent
and TemperatureControl
starts polling.reset_component()
is called by Board.reset_components()
.
BinaryComponent
, otherwise it doesn't do anything.pads_for_sort()
is called by add_components()
to ensure that multiple components are sorted correctly for numbering.Other things to note:
find_one()
is declared to return the correct component type if an if_missing
argument is given. Otherwise, it might return None
. If there is no such component and if_missing
is given, an assertion will fail.type[EC]
where EC
is an abstract type, Mypy (version 1.2.0) will complain.
isinstance()
, but you have to add a # type: ignore [type-abstract]
comment to shut it up.I figured out a way around the spurious errors, although it's perhaps a bit of overkill. What I did was to add parallel class methods to ExternalComponent
that turn around and call the methods on Board
:
@classmethod
def find_all_in(cls: type[EC], board: Board) -> Sequence[EC]:
return board.find_all(cls)
@overload
@classmethod
def find_one_in(cls: type[EC], board: Board, *, if_missing: ValOrFn[str]) -> EC: ... # @UnusedVariable
@overload
@classmethod
def find_one_in(cls: type[EC], board: Board, *, if_missing: Missing = MISSING) -> Optional[EC]: ... # @UnusedVariable
@classmethod
def find_one_in(cls: type[EC], board: Board, *,
if_missing: MissingOr[ValOrFn[str]] = MISSING) -> Optional[EC]:
return board.find_one(cls, if_missing=if_missing)
@classmethod
def _add_external_to(cls: type[EC], board: Board, cpt: EC) -> int:
return board._add_external(cls, cpt)
@classmethod
def _maybe_add_external_to(cls: type[EC], board: Board, cpt: Optional[EC]) -> None:
board._maybe_add_external(cls, cpt)
@classmethod
def _add_externals_to(cls: type[EC], board: Board, cpts: Sequence[EC], *,
order: Optional[RCOrder] = None) -> None:
board._add_externals(cls, cpts, order=order)
@classmethod
def component_number_in(cls: type[EC], board: Board, n: int) -> EC:
return cls.find_all_in(board)[n-1]
@classmethod
def reset_in(cls: type[EC], board: Board) -> None:
board.reset_components(cls)
So, while
board.find_one(Pipettor)
gets a Mypy error that needs to be suppressed,
Pipettor.find_one_in(board)
works just fine.
When I first created the
Board
class, it hadPad
s andWell
s, and I soon addedExtractionPoint
s. These are all, arguably, intrinsic to the board. Then cameHeater
s andMagnet
s,Pipettor
s,PowerSupply
s,Chiller
s (and the generalization ofTemperatureControl
s, andFan
s. Now I'm thinking about addingSensor
s. Most of these areBoardComponent
s, some (e.g.,Pipettor
s) areSystemComponent
s.Pipettor
isn't aSystemComponent
. It contains one. I don't remember why I did it that way, but I'm sure I had a good reason. Oh, now I see. APipettor
is anOpScheduler[Pipettor]
, andOpScheduler
andSystemComponent
both haveschedule()
methods. Sigh. I should fix that at some point.Each of these needs an attribute/property (either an
Optional
or a list), and each needs to be able to be set in the__init__()
method or added.What I'd like to do is to simplify
Board
by creating a tag classExternalComponent
(with a type varEC
) and then do something likeetc. Or maybe just let the caller use
find_external()
andfind_externals()
(or maybeall_components()
andone_component()
or something like that.)I'd also like to simplify
Board.__init__()
to say that it doesn't take any components as parameters, and it's up to the subclass to add everything. (Which is what they already do, but some of the parameters aren't optional.)Migrated from internal repository. Originally created by @EvanKirshenbaum on May 20, 2023 at 12:53 PM PDT. Closed on May 22, 2023 at 4:32 PM PDT.