diff --git a/setup.cfg b/setup.cfg index c4aacb6..a039684 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,11 +27,11 @@ package_dir = =src packages = find_namespace: install_requires = - attrs + attrs >= 22.1 numpy pandas openpyxl - pint >= 0.19.2 + pint >= 0.20 tabulate toml typing_extensions >= 4.2 diff --git a/src/alhambra_mixes/actions.py b/src/alhambra_mixes/actions.py index 1d75ba6..751088a 100644 --- a/src/alhambra_mixes/actions.py +++ b/src/alhambra_mixes/actions.py @@ -43,9 +43,9 @@ def name(self) -> str: # pragma: no cover def tx_volume( self, - mix_vol: Quantity[Decimal] = Q_(DNAN, uL), + mix_vol: DecimalQuantity = Q_(DNAN, uL), actions: Sequence[AbstractAction] = tuple(), - ) -> Quantity[Decimal]: # pragma: no cover + ) -> DecimalQuantity: # pragma: no cover """The total volume transferred by the action to the sample. May depend on the total mix volume. Parameters @@ -60,14 +60,14 @@ def tx_volume( def _mixlines( self, tablefmt: str | TableFormat, - mix_vol: Quantity[Decimal], + mix_vol: DecimalQuantity, actions: Sequence[AbstractAction] = tuple(), ) -> Sequence[MixLine]: # pragma: no cover ... @abstractmethod def all_components( - self, mix_vol: Quantity[Decimal], actions: Sequence[AbstractAction] = tuple() + self, mix_vol: DecimalQuantity, actions: Sequence[AbstractAction] = tuple() ) -> pd.DataFrame: # pragma: no cover """A dataframe containing all base components added by the action. @@ -94,8 +94,8 @@ def with_reference( ... def dest_concentration( - self, mix_vol: Quantity, actions: Sequence[AbstractAction] = tuple() - ) -> Quantity: + self, mix_vol: DecimalQuantity, actions: Sequence[AbstractAction] = tuple() + ) -> DecimalQuantity: """The destination concentration added to the mix by the action. Raises @@ -108,8 +108,8 @@ def dest_concentration( raise ValueError("Single destination concentration not defined.") def dest_concentrations( - self, mix_vol: Quantity, actions: Sequence[AbstractAction] = tuple() - ) -> Sequence[Quantity[Decimal]]: + self, mix_vol: DecimalQuantity, actions: Sequence[AbstractAction] = tuple() + ) -> Sequence[DecimalQuantity]: raise ValueError @property @@ -120,9 +120,9 @@ def components(self) -> list[AbstractComponent]: @abstractmethod def each_volumes( self, - total_volume: Quantity[Decimal], + total_volume: DecimalQuantity, actions: Sequence[AbstractAction] = tuple(), - ) -> list[Quantity[Decimal]]: + ) -> list[DecimalQuantity]: ... @classmethod @@ -161,7 +161,7 @@ def __eq__(self, other: Any) -> bool: for a in self.__attrs_attrs__: # type: Attribute v1 = getattr(self, a.name) v2 = getattr(other, a.name) - if isinstance(v1, Quantity): + if isinstance(v1, ureg.Quantity): if isnan(v1.m) and isnan(v2.m) and (v1.units == v2.units): continue if v1 != v2: @@ -198,7 +198,7 @@ def with_reference( ) @property - def source_concentrations(self) -> list[Quantity[Decimal]]: + def source_concentrations(self) -> list[DecimalQuantity]: concs = [c.concentration for c in self.components] return concs @@ -213,7 +213,7 @@ def _unstructure(self, experiment: "Experiment" | None) -> dict[str, Any]: if val is a.default: continue # FIXME: nan quantities are always default, and pint handles them poorly - if isinstance(val, Quantity) and isnan(val.m): + if isinstance(val, ureg.Quantity) and isnan(val.m): continue d[a.name] = _unstructure(val) return d @@ -240,7 +240,7 @@ def _structure( return cls(**d) def all_components( - self, mix_vol: Quantity[Decimal], actions: Sequence[AbstractAction] = tuple() + self, mix_vol: DecimalQuantity, actions: Sequence[AbstractAction] = tuple() ) -> pd.DataFrame: newdf = _empty_components() @@ -265,8 +265,8 @@ def all_components( def _compactstrs( self, tablefmt: str | TableFormat, - dconcs: Sequence[Quantity[Decimal]], - eavols: Sequence[Quantity[Decimal]], + dconcs: Sequence[DecimalQuantity], + eavols: Sequence[DecimalQuantity], ) -> Sequence[MixLine]: # locs = [(c.name,) + c.location for c in self.components] # names = [c.name for c in self.components] @@ -294,18 +294,18 @@ def _compactstrs( ) names: list[list[str]] = [] - source_concs: list[Quantity[Decimal]] = [] - dest_concs: list[Quantity[Decimal]] = [] + source_concs: list[DecimalQuantity] = [] + dest_concs: list[DecimalQuantity] = [] numbers: list[int] = [] - ea_vols: list[Quantity[Decimal]] = [] - tot_vols: list[Quantity[Decimal]] = [] + ea_vols: list[DecimalQuantity] = [] + tot_vols: list[DecimalQuantity] = [] plates: list[str] = [] wells_list: list[list[WellPos]] = [] for plate, plate_comps in locdf.groupby("plate"): # type: str, pd.DataFrame for vol, plate_vol_comps in plate_comps.groupby( "ea_vols" - ): # type: Quantity[Decimal], pd.DataFrame + ): # type: DecimalQuantity, pd.DataFrame if pd.isna(plate_vol_comps["well"].iloc[0]): if not pd.isna(plate_vol_comps["well"]).all(): raise ValueError @@ -408,7 +408,7 @@ class FixedVolume(ActionWithComponents): | c1, c2, c3 | 200.00 nM | 66.67 nM | 3 | 5.00 µl | 15.00 µl | | | """ - fixed_volume: Quantity[Decimal] = attrs.field( + fixed_volume: DecimalQuantity = attrs.field( converter=_parse_vol_required, on_setattr=attrs.setters.convert ) set_name: str | None = None @@ -429,9 +429,9 @@ def __new__(cls, *args, **kwargs): def dest_concentrations( self, - mix_vol: Quantity[Decimal] = Q_(DNAN, uL), + mix_vol: DecimalQuantity = Q_(DNAN, uL), actions: Sequence[AbstractAction] = tuple(), - ) -> list[Quantity[Decimal]]: + ) -> list[DecimalQuantity]: return [ x * y for x, y in zip( @@ -441,9 +441,9 @@ def dest_concentrations( def each_volumes( self, - mix_vol: Quantity[Decimal] = Q_(DNAN, uL), + mix_vol: DecimalQuantity = Q_(DNAN, uL), actions: Sequence[AbstractAction] = tuple(), - ) -> list[Quantity[Decimal]]: + ) -> list[DecimalQuantity]: return [self.fixed_volume.to(uL)] * len(self.components) @property @@ -456,7 +456,7 @@ def name(self) -> str: def _mixlines( self, tablefmt: str | TableFormat, - mix_vol: Quantity[Decimal], + mix_vol: DecimalQuantity, actions: Sequence[AbstractAction] = tuple(), ) -> list[MixLine]: dconcs = self.dest_concentrations(mix_vol) @@ -549,7 +549,7 @@ class EqualConcentration(FixedVolume): def __init__( self, components: Sequence[AbstractComponent | str] | AbstractComponent | str, - fixed_volume: str | Quantity, + fixed_volume: str | DecimalQuantity, set_name: str | None = None, compact_display: bool = True, method: Literal["max_volume", "min_volume", "check"] @@ -576,7 +576,7 @@ def __init__( ] = "min_volume" @property - def source_concentrations(self) -> List[Quantity[Decimal]]: + def source_concentrations(self) -> List[DecimalQuantity]: concs = super().source_concentrations if any(x != concs[0] for x in concs) and (self.method == "check"): raise ValueError("Not all components have equal concentration.") @@ -584,9 +584,9 @@ def source_concentrations(self) -> List[Quantity[Decimal]]: def each_volumes( self, - mix_vol: Quantity[Decimal] = Q_(DNAN, uL), + mix_vol: DecimalQuantity = Q_(DNAN, uL), actions: Sequence[AbstractAction] = tuple(), - ) -> list[Quantity[Decimal]]: + ) -> list[DecimalQuantity]: # match self.equal_conc: if self.method == "min_volume": sc = self.source_concentrations @@ -607,9 +607,9 @@ def each_volumes( def tx_volume( self, - mix_vol: Quantity[Decimal] = Q_(DNAN, uL), + mix_vol: DecimalQuantity = Q_(DNAN, uL), actions: Sequence[AbstractAction] = tuple(), - ) -> Quantity[Decimal]: + ) -> DecimalQuantity: if isinstance(self.method, Sequence) and (self.method[0] == "max_fill"): return self.fixed_volume * len(self.components) return sum(self.each_volumes(mix_vol), ureg("0.0 uL")) @@ -617,7 +617,7 @@ def tx_volume( def _mixlines( self, tablefmt: str | TableFormat, - mix_vol: Quantity[Decimal], + mix_vol: DecimalQuantity, actions: Sequence[AbstractAction] = tuple(), ) -> list[MixLine]: ml = super()._mixlines(tablefmt, mix_vol) @@ -682,12 +682,12 @@ class FixedConcentration(ActionWithComponents): | *Total:* | | 40.00 nM | | | 25.00 µl | | | """ - fixed_concentration: Quantity[Decimal] = attrs.field( + fixed_concentration: DecimalQuantity = attrs.field( converter=_parse_conc_required, on_setattr=attrs.setters.convert ) set_name: str | None = None compact_display: bool = True - min_volume: Quantity[Decimal] = attrs.field( + min_volume: DecimalQuantity = attrs.field( converter=_parse_vol_optional_none_zero, default=ZERO_VOL, on_setattr=attrs.setters.convert, @@ -695,16 +695,16 @@ class FixedConcentration(ActionWithComponents): def dest_concentrations( self, - mix_vol: Quantity[Decimal] = Q_(DNAN, uL), + mix_vol: DecimalQuantity = Q_(DNAN, uL), actions: Sequence[AbstractAction] = tuple(), - ) -> list[Quantity[Decimal]]: + ) -> list[DecimalQuantity]: return [self.fixed_concentration] * len(self.components) def each_volumes( self, - mix_vol: Quantity[Decimal] = Q_(DNAN, uL), + mix_vol: DecimalQuantity = Q_(DNAN, uL), actions: Sequence[AbstractAction] = tuple(), - ) -> list[Quantity[Decimal]]: + ) -> list[DecimalQuantity]: ea_vols = [ mix_vol * r for r in _ratio(self.fixed_concentration, self.source_concentrations) @@ -726,7 +726,7 @@ def each_volumes( def _mixlines( self, tablefmt: str | TableFormat, - mix_vol: Quantity[Decimal], + mix_vol: DecimalQuantity, actions: Sequence[AbstractAction] = tuple(), ) -> list[MixLine]: dconcs = self.dest_concentrations(mix_vol) @@ -769,18 +769,18 @@ class ToConcentration(ActionWithComponents): An action adding an amount of components such that the concentration of each component in the mix will be at some tapiprget concentration. Unlike FixedConcentration, which *adds* a certain concentration, this takes into account other contents of the mix, and only adds enough to reach a particular final concentration.""" - fixed_concentration: Quantity[Decimal] = attrs.field( + fixed_concentration: DecimalQuantity = attrs.field( converter=_parse_conc_required, on_setattr=attrs.setters.convert ) compact_display: bool = True - min_volume: Quantity[Decimal] = attrs.field( + min_volume: DecimalQuantity = attrs.field( converter=_parse_vol_optional, default=Q_(DNAN, uL), on_setattr=attrs.setters.convert, ) def _othercomps( - self, mix_vol: Quantity[Decimal], actions: Sequence[AbstractAction] = tuple() + self, mix_vol: DecimalQuantity, actions: Sequence[AbstractAction] = tuple() ): cps = _empty_components() @@ -817,10 +817,10 @@ def _othercomps( def dest_concentrations( self, - mix_vol: Quantity, + mix_vol: DecimalQuantity, actions: Sequence[AbstractAction] = tuple(), _othercomps: pd.DataFrame | None = None, - ) -> Sequence[Quantity[Decimal]]: + ) -> Sequence[DecimalQuantity]: if _othercomps is None and actions: _othercomps = self._othercomps(mix_vol, actions) if _othercomps is not None: @@ -836,10 +836,10 @@ def dest_concentrations( def each_volumes( self, - mix_vol: Quantity[Decimal] = Q_(DNAN, uL), + mix_vol: DecimalQuantity = Q_(DNAN, uL), actions: Sequence[AbstractAction] = tuple(), _othercomps: pd.DataFrame | None = None, - ) -> list[Quantity[Decimal]]: + ) -> list[DecimalQuantity]: ea_vols = [ mix_vol * r for r in _ratio( @@ -864,7 +864,7 @@ def each_volumes( def _mixlines( self, tablefmt: str | TableFormat, - mix_vol: Quantity[Decimal], + mix_vol: DecimalQuantity, actions: Sequence[AbstractAction] = tuple(), ) -> list[MixLine]: othercomps = self._othercomps(mix_vol, actions) diff --git a/src/alhambra_mixes/components.py b/src/alhambra_mixes/components.py index 5e1eb12..dbc9084 100644 --- a/src/alhambra_mixes/components.py +++ b/src/alhambra_mixes/components.py @@ -11,7 +11,15 @@ from .locations import WellPos, _parse_wellpos_optional from .logging import log from .printing import TableFormat -from .units import Q_, ZERO_VOL, Decimal, Quantity, _parse_conc_optional, nM, ureg +from .units import ( + Q_, + ZERO_VOL, + Decimal, + DecimalQuantity, + _parse_conc_optional, + nM, + ureg, +) from .util import _none_as_empty_string from .dictstructure import _structure, _unstructure, _STRUCTURE_CLASSES @@ -61,7 +69,7 @@ def _well_list(self) -> list[WellPos]: @property @abstractmethod - def concentration(self) -> Quantity[Decimal]: # pragma: no cover + def concentration(self) -> DecimalQuantity: # pragma: no cover "(Source) concentration of the component as a pint Quantity. NaN if undefined." ... @@ -98,9 +106,9 @@ def printed_name(self, tablefmt: str | TableFormat) -> str: def _update_volumes( self, - consumed_volumes: Dict[str, Quantity] = {}, - made_volumes: Dict[str, Quantity] = {}, - ) -> Tuple[Dict[str, Quantity], Dict[str, Quantity]]: + consumed_volumes: Dict[str, DecimalQuantity] = {}, + made_volumes: Dict[str, DecimalQuantity] = {}, + ) -> Tuple[Dict[str, DecimalQuantity], Dict[str, DecimalQuantity]]: """ Given a """ @@ -122,7 +130,7 @@ class Component(AbstractComponent): """ name: str - concentration: Quantity[Decimal] = attrs.field( + concentration: DecimalQuantity = attrs.field( converter=_parse_conc_optional, default=None, on_setattr=attrs.setters.convert ) # FIXME: this is not a great way to do this: should make code not give None @@ -145,8 +153,8 @@ def __eq__(self, other: Any) -> bool: return False if self.name != other.name: return False - if isinstance(self.concentration, Quantity) and isinstance( - other.concentration, Quantity + if isinstance(self.concentration, ureg.Quantity) and isinstance( + other.concentration, ureg.Quantity ): if isnan(self.concentration.m) and isnan(other.concentration.m): return True @@ -178,7 +186,7 @@ def _unstructure(self, experiment: "Experiment" | None = None) -> dict[str, Any] val = getattr(self, att.name) if val is att.default: continue - if isinstance(val, Quantity) and isnan(val.m): + if isinstance(val, ureg.Quantity) and isnan(val.m): continue d[att.name] = _unstructure(val) return d diff --git a/src/alhambra_mixes/dictstructure.py b/src/alhambra_mixes/dictstructure.py index 674bbbe..167ca29 100644 --- a/src/alhambra_mixes/dictstructure.py +++ b/src/alhambra_mixes/dictstructure.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any from .locations import WellPos -from .units import Quantity +from .units import ureg, PlainQuantity if TYPE_CHECKING: # pragma: no cover from attrs import Attribute @@ -35,7 +35,7 @@ def _structure(x: dict[str, Any], experiment: "Experiment" | None = None) -> Any def _unstructure(x: Any) -> Any: - if isinstance(x, Quantity): + if isinstance(x, ureg.Quantity): return str(x) elif isinstance(x, list): return [_unstructure(y) for y in x] diff --git a/src/alhambra_mixes/experiments.py b/src/alhambra_mixes/experiments.py index 664fc2d..a51ad53 100644 --- a/src/alhambra_mixes/experiments.py +++ b/src/alhambra_mixes/experiments.py @@ -19,7 +19,7 @@ import attrs from .dictstructure import _structure, _unstructure -from .units import DNAN, Q_, ZERO_VOL, Decimal, Quantity, uL +from .units import DNAN, Q_, ZERO_VOL, Decimal, DecimalQuantity, uL from .mixes import Mix from .mixes import VolumeError @@ -63,10 +63,10 @@ def add_mix( name: str = "", test_tube_name: str | None = None, *, - fixed_total_volume: Quantity[Decimal] | str = Q_(DNAN, uL), - fixed_concentration: str | Quantity[Decimal] | None = None, + fixed_total_volume: DecimalQuantity | str = Q_(DNAN, uL), + fixed_concentration: str | DecimalQuantity | None = None, buffer_name: str = "Buffer", - min_volume: Quantity[Decimal] | str = Q_(Decimal("0.5"), uL), + min_volume: DecimalQuantity | str = Q_(Decimal("0.5"), uL), check_volumes: bool | None = None, apply_reference: bool = True, ) -> Experiment: @@ -158,9 +158,11 @@ def __len__(self) -> int: def __iter__(self) -> Iterator[AbstractComponent]: return iter(self.components.values()) - def consumed_and_produced_volumes(self) -> Mapping[str, Tuple[Quantity, Quantity]]: - consumed_volume: Dict[str, Quantity] = {} - produced_volume: Dict[str, Quantity] = {} + def consumed_and_produced_volumes( + self, + ) -> Mapping[str, Tuple[DecimalQuantity, DecimalQuantity]]: + consumed_volume: Dict[str, DecimalQuantity] = {} + produced_volume: Dict[str, DecimalQuantity] = {} for component in self.components.values(): component._update_volumes(consumed_volume, produced_volume) return { diff --git a/src/alhambra_mixes/printing.py b/src/alhambra_mixes/printing.py index 49bd595..ed5ff21 100644 --- a/src/alhambra_mixes/printing.py +++ b/src/alhambra_mixes/printing.py @@ -167,7 +167,7 @@ def _html_row_with_attrs( def _formatter( - x: int | float | str | list[str] | Quantity[Decimal] | None, + x: int | float | str | list[str] | DecimalQuantity | None, italic: bool = False, tablefmt: str | TableFormat = "pipe", splits: list = [], @@ -180,7 +180,7 @@ def _formatter( out = f"{x:,.2f}" if isnan(x): out = _format_error_span(out, tablefmt) - elif isinstance(x, Quantity): + elif isinstance(x, ureg.Quantity): out = f"{x:,.2f~#P}" if isnan(x.m): out = _format_error_span(out, tablefmt) @@ -247,11 +247,11 @@ class MixLine: """ names: list[str] = attrs.field(factory=list) - source_conc: Quantity[Decimal] | str | None = None - dest_conc: Quantity[Decimal] | str | None = None - total_tx_vol: Quantity[Decimal] = NAN_VOL + source_conc: DecimalQuantity | str | None = None + dest_conc: DecimalQuantity | str | None = None + total_tx_vol: DecimalQuantity = NAN_VOL number: int = 1 - each_tx_vol: Quantity[Decimal] = NAN_VOL # | str | None = None + each_tx_vol: DecimalQuantity = NAN_VOL # | str | None = None plate: str = "" wells: list[WellPos] = attrs.field(factory=list) note: str | None = None diff --git a/src/alhambra_mixes/quantitate.py b/src/alhambra_mixes/quantitate.py index 4f5cc29..c77c22e 100644 --- a/src/alhambra_mixes/quantitate.py +++ b/src/alhambra_mixes/quantitate.py @@ -117,7 +117,7 @@ def parse_conc(conc: float | int | str | Quantity[D]) -> Quantity[D]: conc = f"{conc} µM" if isinstance(conc, str): - q = ureg(conc) + q = ureg.Quantity(conc) if not q.check(uM): raise ValueError( f"{conc} is not a valid quantity here (should be concentration)." @@ -143,7 +143,7 @@ def parse_nmol(nmoles: float | int | str | Quantity[D]) -> Quantity[D]: nmoles = f"{nmoles} nmol" if isinstance(nmoles, str): - q = ureg(nmoles) + q = ureg.Quantity(nmoles) if not q.check(ureg.nmol): raise ValueError(f"{nmoles} is not a valid quantity here (should be nmol).") return q diff --git a/src/alhambra_mixes/references.py b/src/alhambra_mixes/references.py index a196809..2b132fc 100644 --- a/src/alhambra_mixes/references.py +++ b/src/alhambra_mixes/references.py @@ -15,7 +15,7 @@ DNAN, Q_, Decimal, - Quantity, + DecimalQuantity, _parse_conc_optional, _parse_conc_required, nM, @@ -32,7 +32,9 @@ _REF_COLUMNS = ["Name", "Plate", "Well", "Concentration (nM)", "Sequence"] _REF_DTYPES = [object, object, object, np.float64, object] -RefFile: TypeAlias = "str | tuple[str, Quantity | str | dict[str, Quantity]]" +RefFile: TypeAlias = ( + "str | tuple[str, DecimalQuantity | str | dict[str, DecimalQuantity]]" +) def _new_ref_df() -> pd.DataFrame: @@ -99,7 +101,7 @@ def search( name: str | None = None, plate: str | None = None, well: str | WellPos | None = None, - concentration: str | Quantity[Decimal] | None = None, + concentration: str | DecimalQuantity | None = None, sequence: str | None = None, ) -> Reference: well = _parse_wellpos_optional(well) @@ -124,9 +126,9 @@ def get_concentration( name: str | None = None, plate: str | None = None, well: str | WellPos | None = None, - concentration: str | Quantity | None = None, + concentration: str | DecimalQuantity | None = None, sequence: str | None = None, - ) -> Quantity[Decimal]: + ) -> DecimalQuantity: valref = self.search(name, plate, well, concentration, sequence) if len(valref) == 1: @@ -178,15 +180,16 @@ def update( for filename in files_list: filetype = None all_conc = None - conc_dict: dict[str, Quantity] = {} + conc_dict: dict[str, DecimalQuantity] = {} if isinstance(filename, tuple): conc_info = filename[1] filepath = Path(filename[0]) - if isinstance(conc_info, Mapping): + if isinstance(conc_info, dict): conc_dict = { - k: _parse_conc_required(v) for k, v in conc_info.values() + k: _parse_conc_required(v) + for k, v in cast(dict[str, DecimalQuantity], conc_info).items() } if "default" in conc_dict: all_conc = _parse_conc_required(conc_dict["default"]) @@ -332,7 +335,9 @@ def compile(cls, files: Sequence[RefFile] | RefFile, round: int = -1) -> Referen def _parse_idt_coa(df: pd.DataFrame) -> pd.DataFrame: df.rename({"Sequence Name": "Name"}, axis="columns", inplace=True) df.loc[:, "Well"] = df.loc[:, "Well Position"].map(lambda x: str(WellPos(x))) - df.loc[:, "Concentration (nM)"] = df.loc[:, "Conc"].map(lambda x: ureg(x).m_as(nM)) + df.loc[:, "Concentration (nM)"] = df.loc[:, "Conc"].map( + lambda x: ureg.Quantity(x).m_as(nM) + ) df.loc[:, "Plate"] = None df.loc[:, "Sequence"] = df.loc[:, "Sequence"].str.replace(" ", "") return df.loc[:, _REF_COLUMNS] diff --git a/src/alhambra_mixes/units.py b/src/alhambra_mixes/units.py index 0cf92fc..b0d2d6f 100644 --- a/src/alhambra_mixes/units.py +++ b/src/alhambra_mixes/units.py @@ -2,10 +2,10 @@ import decimal from decimal import Decimal -from typing import Sequence, TypeVar, overload - +from typing import Sequence, TypeVar, Union, overload +from typing_extensions import TypeAlias import pint -from pint import Quantity +from pint.facets.plain import PlainQuantity # This needs to be here to make Decimal NaNs behave the way that NaNs # *everywhere else in the standard library* behave. @@ -21,7 +21,8 @@ "ZERO_VOL", "NAN_VOL", "Decimal", - "Quantity", + "PlainQuantity", + "DecimalQuantity", ] ureg = pint.UnitRegistry(non_int_type=Decimal) @@ -32,8 +33,10 @@ uM = ureg.uM nM = ureg.nM +DecimalQuantity: TypeAlias = "PlainQuantity[Decimal]" + -def Q_(qty: int | str | Decimal | float, unit: str | pint.Unit) -> pint.Quantity: +def Q_(qty: int | str | Decimal | float, unit: str | pint.Unit) -> PlainQuantity: "Convenient constructor for units, eg, :code:`Q_(5.0, 'nM')`. Ensures that the quantity is a Decimal." return ureg.Quantity(Decimal(qty), unit) @@ -46,34 +49,34 @@ class VolumeError(ValueError): ZERO_VOL = Q_("0.0", "µL") NAN_VOL = Q_("nan", "µL") -T = TypeVar("T") +T = TypeVar("T", bound=Union[float, Decimal]) @overload def _ratio( - top: Sequence[pint.Quantity[T]], bottom: Sequence[pint.Quantity[T]] + top: Sequence[PlainQuantity[T]], bottom: Sequence[PlainQuantity[T]] ) -> Sequence[T]: ... @overload -def _ratio(top: pint.Quantity[T], bottom: Sequence[pint.Quantity[T]]) -> Sequence[T]: +def _ratio(top: PlainQuantity[T], bottom: Sequence[PlainQuantity[T]]) -> Sequence[T]: ... @overload -def _ratio(top: Sequence[pint.Quantity[T]], bottom: pint.Quantity[T]) -> Sequence[T]: +def _ratio(top: Sequence[PlainQuantity[T]], bottom: PlainQuantity[T]) -> Sequence[T]: ... @overload -def _ratio(top: pint.Quantity[T], bottom: pint.Quantity[T]) -> T: +def _ratio(top: PlainQuantity[T], bottom: PlainQuantity[T]) -> T: ... def _ratio( - top: pint.Quantity[T] | Sequence[pint.Quantity[T]], - bottom: pint.Quantity[T] | Sequence[pint.Quantity[T]], + top: PlainQuantity[T] | Sequence[PlainQuantity[T]], + bottom: PlainQuantity[T] | Sequence[PlainQuantity[T]], ) -> T | Sequence[T]: if isinstance(top, Sequence) and isinstance(bottom, Sequence): return [(x / y).m_as("") for x, y in zip(top, bottom)] @@ -84,15 +87,15 @@ def _ratio( return (top / bottom).m_as("") -def _parse_conc_optional(v: str | pint.Quantity | None) -> pint.Quantity: +def _parse_conc_optional(v: str | PlainQuantity[T] | None) -> PlainQuantity[T]: """Parses a string or Quantity as a concentration; if None, returns a NaN concentration.""" if isinstance(v, str): - q = ureg(v) + q = ureg.Quantity(v) if not q.check(nM): raise ValueError(f"{v} is not a valid quantity here (should be molarity).") return q - elif isinstance(v, pint.Quantity): + elif isinstance(v, PlainQuantity): if not v.check(nM): raise ValueError(f"{v} is not a valid quantity here (should be molarity).") v = Q_(v.m, v.u) @@ -102,15 +105,15 @@ def _parse_conc_optional(v: str | pint.Quantity | None) -> pint.Quantity: raise ValueError -def _parse_conc_required(v: str | pint.Quantity) -> pint.Quantity: +def _parse_conc_required(v: str | PlainQuantity) -> PlainQuantity: """Parses a string or Quantity as a concentration, requiring that it result in a value.""" if isinstance(v, str): - q = ureg(v) + q = ureg.Quantity(v) if not q.check(nM): raise ValueError(f"{v} is not a valid quantity here (should be molarity).") return q - elif isinstance(v, pint.Quantity): + elif isinstance(v, PlainQuantity): if not v.check(nM): raise ValueError(f"{v} is not a valid quantity here (should be molarity).") v = Q_(v.m, v.u) @@ -118,18 +121,18 @@ def _parse_conc_required(v: str | pint.Quantity) -> pint.Quantity: raise ValueError(f"{v} is not a valid quantity here (should be molarity).") -def _parse_vol_optional(v: str | pint.Quantity) -> pint.Quantity: +def _parse_vol_optional(v: str | PlainQuantity) -> PlainQuantity: """Parses a string or quantity as a volume, returning a NaN volume if the value is None. """ # if isinstance(v, (float, int)): # FIXME: was in quantitate.py, but potentially unsafe # v = f"{v} µL" if isinstance(v, str): - q = ureg(v) + q = ureg.Quantity(v) if not q.check(uL): raise ValueError(f"{v} is not a valid quantity here (should be volume).") return q - elif isinstance(v, pint.Quantity): + elif isinstance(v, PlainQuantity): if not v.check(uL): raise ValueError(f"{v} is not a valid quantity here (should be volume).") v = Q_(v.m, v.u) @@ -139,18 +142,18 @@ def _parse_vol_optional(v: str | pint.Quantity) -> pint.Quantity: raise ValueError -def _parse_vol_optional_none_zero(v: str | pint.Quantity) -> pint.Quantity: +def _parse_vol_optional_none_zero(v: str | PlainQuantity) -> PlainQuantity: """Parses a string or quantity as a volume, returning a NaN volume if the value is None. """ # if isinstance(v, (float, int)): # FIXME: was in quantitate.py, but potentially unsafe # v = f"{v} µL" if isinstance(v, str): - q = ureg(v) + q = ureg.Quantity(v) if not q.check(uL): raise ValueError(f"{v} is not a valid quantity here (should be volume).") return q - elif isinstance(v, pint.Quantity): + elif isinstance(v, PlainQuantity): if not v.check(uL): raise ValueError(f"{v} is not a valid quantity here (should be volume).") v = Q_(v.m, v.u) @@ -160,18 +163,18 @@ def _parse_vol_optional_none_zero(v: str | pint.Quantity) -> pint.Quantity: raise ValueError -def _parse_vol_required(v: str | pint.Quantity) -> pint.Quantity: +def _parse_vol_required(v: str | PlainQuantity) -> PlainQuantity: """Parses a string or quantity as a volume, requiring that it result in a value. """ # if isinstance(v, (float, int)): # v = f"{v} µL" if isinstance(v, str): - q = ureg(v) + q = ureg.Quantity(v) if not q.check(uL): raise ValueError(f"{v} is not a valid quantity here (should be volume).") return q - elif isinstance(v, pint.Quantity): + elif isinstance(v, PlainQuantity): if not v.check(uL): raise ValueError(f"{v} is not a valid quantity here (should be volume).") v = Q_(v.m, v.u)