from __future__ import annotations
import warnings
from collections import defaultdict
from dataclasses import dataclass
from typing import Any, Iterator, Optional
from addict import Dict
from CADETProcess import CADETProcessError
from CADETProcess.dataStructure import deprecated_alias
__all__ = ["ComponentSystem", "Component", "Species"]
[docs]
@dataclass
class Species:
"""
A specific chemical form of a component.
Attributes
----------
name : str
Name of the species.
charge : int
Charge of the species. Default is 0.
molar_mass : float, optional
Molar mass in kg/mol.
density : float, optional
Pure-component density in kg/m³.
"""
name: str
charge: int = 0
molar_mass: Optional[float] = None
density: Optional[float] = None
@property
def molecular_weight(self) -> Optional[float]:
"""Deprecated. Use molar_mass."""
warnings.warn(
"`molecular_weight` is deprecated; use `molar_mass` instead.",
DeprecationWarning,
stacklevel=2,
)
return self.molar_mass
@molecular_weight.setter
def molecular_weight(self, value: Optional[float]) -> None:
warnings.warn(
"`molecular_weight` is deprecated; use `molar_mass` instead.",
DeprecationWarning,
stacklevel=2,
)
self.molar_mass = value
def __str__(self) -> str: # noqa: D105
return self.name
[docs]
class Component:
"""
A conserved chemical entity that may exist as multiple species.
Parameters
----------
name : str, optional
Name of the component.
species : str | list[str], optional
Name(s) of the subspecies. If None the component name is used.
charge : int | list[int | None], optional
Charge(s) of the subspecies.
molar_mass : float | list[float | None], optional
Molar mass(es) of the subspecies in kg/mol.
density : float | list[float | None], optional
Density(ies) of the subspecies in kg/m³.
See Also
--------
Species
ComponentSystem
"""
@deprecated_alias(molecular_weight="molar_mass")
def __init__(
self,
name: str | None = None,
species: str | list[str | None] = None,
charge: int | list[int | None] = None,
molar_mass: float | list[float | None] = None,
density: float | list[float | None] = None,
) -> None:
self.name = name
self._species: list[Species] = []
if species is None:
self._add_species(name, charge, molar_mass, density)
elif isinstance(species, str):
self._add_species(species, charge, molar_mass, density)
elif isinstance(species, list):
if charge is None:
charge = len(species) * [None]
if molar_mass is None:
molar_mass = len(species) * [None]
if density is None:
density = len(species) * [None]
for i, spec in enumerate(species):
self._add_species(spec, charge[i], molar_mass[i], density[i])
else:
raise CADETProcessError("Could not determine number of species")
def _add_species(
self,
name: str | None,
charge: int | None,
molar_mass: float | None,
density: float | None,
) -> Species:
kwargs: dict[str, Any] = {}
if charge is not None:
kwargs["charge"] = charge
if molar_mass is not None:
kwargs["molar_mass"] = molar_mass
if density is not None:
kwargs["density"] = density
species = Species(name, **kwargs)
self._species.append(species)
return species
[docs]
def add_species(
self,
species: str | Species,
charge: int | None = None,
molar_mass: float | None = None,
density: float | None = None,
) -> Species:
"""
Add a subspecies to the component.
Parameters
----------
species : str | Species
Species or name of the species to add.
charge : int, optional
Charge of the species.
molar_mass : float, optional
Molar mass in kg/mol.
density : float, optional
Pure-component density in kg/m³.
Returns
-------
Species
The added species.
"""
if isinstance(species, Species):
self._species.append(species)
return species
return self._add_species(species, charge, molar_mass, density)
@property
def species(self) -> list[Species]:
"""list[Species]: The subspecies of the component."""
return self._species
@property
def n_species(self) -> int:
"""int: Number of subspecies."""
return len(self._species)
@property
def label(self) -> list[str]:
"""list[str]: Names of the subspecies."""
return [s.name for s in self._species]
@property
def charge(self) -> list[int | None]:
"""list[int | None]: Charges of the subspecies."""
return [s.charge for s in self._species]
@property
def molar_mass(self) -> list[float | None]:
"""list[float | None]: Molar masses of the subspecies in kg/mol."""
return [s.molar_mass for s in self._species]
@property
def molecular_weight(self) -> list[float | None]:
"""Deprecated. Use molar_mass."""
warnings.warn(
"`molecular_weight` is deprecated; use `molar_mass` instead.",
DeprecationWarning,
stacklevel=2,
)
return self.molar_mass
@property
def density(self) -> list[float | None]:
"""list[float | None]: Densities of the subspecies in kg/m³."""
return [s.density for s in self._species]
def __str__(self) -> str: # noqa: D105
return self.name if self.name is not None else ""
def __iter__(self) -> Iterator[Species]: # noqa: D105
yield from self._species
[docs]
class ComponentSystem:
"""
An ordered collection of components defining the chemical system.
Parameters
----------
components : int | list[str | Component], optional
Number of anonymous components, or an explicit list of names or
Component instances.
name : str, optional
Name of the system.
charges : list[int | None], optional
Charges per component.
molar_masses : list[float | None], optional
Molar masses per component in kg/mol.
densities : list[float | None], optional
Densities per component in kg/m³.
See Also
--------
Species
Component
"""
@deprecated_alias(molecular_weights="molar_masses")
def __init__(
self,
components: int | list[str | Component | None] = None,
name: str | None = None,
charges: list[int | None] = None,
molar_masses: list[float | None] = None,
densities: list[float | None] = None,
) -> None:
self.name = name
self._components: list[Component] = []
if components is None:
return
if isinstance(components, int):
n_comp = components
components = [str(i) for i in range(n_comp)]
elif isinstance(components, list):
n_comp = len(components)
else:
raise CADETProcessError("Could not determine number of components")
if charges is None:
charges = n_comp * [None]
if molar_masses is None:
molar_masses = n_comp * [None]
if densities is None:
densities = n_comp * [None]
for i, comp in enumerate(components):
self.add_component(
comp,
charge=charges[i],
molar_mass=molar_masses[i],
density=densities[i],
)
@property
def components(self) -> list[Component]:
"""list[Component]: Components in the system."""
return self._components
@property
def components_dict(self) -> dict[str, Component]:
"""dict[str, Component]: Components indexed by name."""
return {name: comp for name, comp in zip(self.names, self._components)}
@property
def n_components(self) -> int:
"""int: Number of components."""
return len(self._components)
@property
def n_comp(self) -> int:
"""int: Total number of species."""
return self.n_species
@property
def n_species(self) -> int:
"""int: Total number of species."""
return sum(comp.n_species for comp in self._components)
[docs]
def add_component(
self,
component: str | Component,
*args: Any,
**kwargs: Any,
) -> None:
"""
Add a component to the system.
Parameters
----------
component : str | Component
Component instance or name of the component to add.
*args, **kwargs
Passed to Component constructor when component is a string.
"""
if not isinstance(component, Component):
component = Component(component, *args, **kwargs)
if component.name in self.names:
raise CADETProcessError(
f"Component '{component.name}' already exists in ComponentSystem."
)
self._components.append(component)
[docs]
def remove_component(self, component: str | Component) -> None:
"""
Remove a component from the system.
Parameters
----------
component : str | Component
Name or instance of the component to remove.
"""
if isinstance(component, str):
try:
component = self.components_dict[component]
except KeyError:
raise CADETProcessError("Unknown Component.")
if component not in self._components:
raise CADETProcessError("Unknown Component.")
self._components.remove(component)
@property
def indices(self) -> dict[str, list[int]]:
"""dict[str, list[int]]: Species indices per component name."""
indices = defaultdict(list)
index = 0
for comp in self._components:
for _ in comp.species:
indices[comp.name].append(index)
index += 1
return Dict(indices)
@property
def species_indices(self) -> dict[str, int]:
"""dict[str, int]: Index per species name."""
indices = Dict()
index = 0
for comp in self._components:
for spec in comp.species:
indices[spec.name] = index
index += 1
return indices
@property
def names(self) -> list[str]:
"""list[str]: Component names."""
return [
comp.name if comp.name is not None else str(i)
for i, comp in enumerate(self._components)
]
@property
def species(self) -> list[str]:
"""list[str]: Species names in order."""
result = []
index = 0
for comp in self._components:
for label in comp.label:
result.append(label if label is not None else str(index))
index += 1
return result
@property
def charges(self) -> list[int | None]:
"""list[int | None]: Charges per species."""
return [charge for comp in self._components for charge in comp.charge]
@property
def molar_masses(self) -> list[float | None]:
"""list[float | None]: Molar masses per species in kg/mol."""
return [mm for comp in self._components for mm in comp.molar_mass]
@property
def molecular_weights(self) -> list[float | None]:
"""Deprecated. Use molar_masses."""
warnings.warn(
"`molecular_weights` is deprecated; use `molar_masses` instead.",
DeprecationWarning,
stacklevel=2,
)
return self.molar_masses
@property
def densities(self) -> list[float | None]:
"""list[float | None]: Densities per species in kg/m³."""
return [density for comp in self._components for density in comp.density]
def __repr__(self) -> str: # noqa: D105
return f"{self.__class__.__name__}({self.names!r})"
def __len__(self) -> int: # noqa: D105
return self.n_comp
def __iter__(self) -> Iterator[Component]: # noqa: D105
yield from self._components
def __getitem__(self, item: int) -> Component: # noqa: D105
return self._components[item]