Source code for AFL.automation.mixing.Component

import warnings

import periodictable  # type: ignore
import copy
from pyparsing import ParseException
from typing import Optional, Dict, Iterator, Tuple, Union

from AFL.automation.shared.units import units, AVOGADROS_NUMBER, enforce_units  # type: ignore
from AFL.automation.shared.warnings import MixWarning


[docs] class Component: """ Component of a mixture This class defines the basic properties of a component within a mixture. It includes attributes for mass, volume, density, formula, and scattering length density (SLD). It also enforces unit specifications for these attributes. Parameters ---------- name : str The name of the component. mass : str | units.Quantity | None The mass of the component, by default None. volume : str | units.Quantity | None The volume of the component, by default None. density : str | units.Quantity | None The density of the component, by default None. formula : Optional[str], optional The chemical formula of the component, by default None. sld : str | units.Quantity | None The scattering length density of the component, by default None. """
[docs] def __init__(self, name: str, mass: Optional[units.Quantity] = None, volume: Optional[units.Quantity] = None, density: Optional[units.Quantity] = None, formula: Optional[str] = None, sld: Optional[units.Quantity] = None, uid: Optional[str] = None, solute: bool = False) -> None: self.name: str = name self._mass: Optional[units.Quantity] = enforce_units(mass, 'mass') self._volume: Optional[units.Quantity] = enforce_units(volume, 'volume') self._density: Optional[units.Quantity] = enforce_units(density, 'density') self._sld: Optional[units.Quantity] = sld #need to add sld units self.solute = solute if not self.solute and not self.has_density: warnings.warn( ( f'Component "{name}" initialized with solute=False and no density specification.\n' f'Assuming this is in error and setting solute=True. You can fix this by adding "{name}"\n' f'to the solutes argument to Solution() or by passing solute=True to Component()\n' ) , MixWarning, stacklevel=2) self.solute = True if formula is None: self.formula = name else: self.formula = formula self.uid = uid
[docs] def emit(self) -> Dict[str, Union[str, units.Quantity]]: return { 'name': self.name, 'density': self.density, 'formula': self.formula, 'sld': self.sld, }
def __str__(self) -> str: if self.solute: out_str = '<Solute ' else: out_str = '<Solvent ' out_str += f' M={self.mass:4.3f}' if self.has_mass else ' M=None' out_str += f' V={self.volume:4.3f}' if self.has_volume else ' V=None' out_str += f' D={self.density:4.3f}' if self.has_density else ' D=None' out_str += '>' return out_str def __repr__(self) -> str: return self.__str__()
[docs] def __hash__(self) -> int: """Needed so Components can be dictionary keys""" return id(self)
[docs] def copy(self) -> 'Component': return copy.deepcopy(self)
[docs] def __iter__(self) -> Iterator[Tuple[str, 'Component']]: """Dummy iterator to mimic behavior of Mixture.""" for name, component in [(self.name, self)]: yield name, component
@property def mass(self) -> Optional[units.Quantity]: return self._mass @mass.setter def mass(self, value: units.Quantity) -> None: value = enforce_units(value, 'mass') self._mass = value
[docs] def set_mass(self, value: units.Quantity) -> 'Component': """Setter for inline mass changes""" component = self.copy() component.mass = value return component
@property def volume(self) -> Optional[units.Quantity]: if (not self.is_solute) and (self.has_mass and self.has_density): return enforce_units(self._mass / self._density, 'volume') # type: ignore else: return None @volume.setter def volume(self, value: units.Quantity) -> None: if self.solute: raise ValueError('Can\'t set volume on a solute') elif not self.has_density: raise ValueError('Can\'t set volume without specifying density') else: value = enforce_units(value, 'volume') self.mass = enforce_units(value * self._density, 'mass')
[docs] def set_volume(self, value: units.Quantity) -> 'Component': """Setter for inline volume changes""" if self.solute: raise ValueError('Can\'t set volume on a solute') component = self.copy() component.volume = value return component
@property def density(self) -> Optional[units.Quantity]: return self._density @density.setter def density(self, value: units.Quantity) -> None: value = enforce_units(value, 'density') self._density = value @property def formula(self) -> Optional[periodictable.formula]: return self._formula @formula.setter def formula(self, value: Optional[str]) : if value is None: self._formula = None else: try: self._formula = periodictable.formula(value) except (ValueError, ParseException): self._formula = None @property def moles(self) -> Optional[units.Quantity]: if self.has_formula: return self._mass / (self.formula.molecular_mass * units('g')) / AVOGADROS_NUMBER # type: ignore else: return None @property def sld(self) -> Optional[units.Quantity]: if self._sld is not None: return self._sld elif self.has_formula and self.has_density: self.formula.density = self.density.to('g/ml').magnitude # type: ignore sld = self.formula.neutron_sld(wavelength=5.0)[0] # type: ignore return sld * 1e-6 * units('angstrom^(-2)') else: return None @sld.setter def sld(self, value: units.Quantity) -> None: self._sld = value @property def is_solute(self) -> bool: return self.solute or (not self.has_volume) @property def is_solvent(self) -> bool: return (not self.solute) and self.has_volume @property def has_mass(self) -> bool: return self._mass is not None @property def has_volume(self) -> bool: if self._volume is not None or (self.has_mass and self.has_density): return True @property def has_density(self) -> bool: return self._density is not None @property def has_formula(self) -> bool: return self._formula is not None @property def has_sld(self) -> bool: return self._sld is not None or (self.has_formula and self.has_density) def __add__(self, other: 'Component') -> 'Component': if not (self.name == other.name): raise ValueError(f'Can only add components of the same name. Not {self.name} and {other.name}') if not (self.density == other.density): raise ValueError(f'Density mismatch in component.__add__: {self.density} and {other.density}') component = self.copy() component.mass = enforce_units(component._mass + other._mass, 'mass') # type: ignore return component