From 3c29895a9f2b94a83916f56fab59d0a41ba30369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Wed, 21 Aug 2024 10:20:35 +0200 Subject: [PATCH 1/6] Add ruff linter settings --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 523485d..8aa8299 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,4 +68,8 @@ version = { attr = "CADETPythonSimulator.__version__" } [tool.ruff] # Same as Black. line-length = 88 -indent-width = 4 \ No newline at end of file +indent-width = 4 + +[tool.ruff.lint] +select = ["E", "F", "W"] +ignore = ["F401"] \ No newline at end of file From 1252f0f8a261bb48c77a8acdfb83db92389b258b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Wed, 21 Aug 2024 10:15:15 +0200 Subject: [PATCH 2/6] Report test coverage in pipeline --- .github/workflows/pipeline.yml | 8 +++++++- .gitignore | 2 ++ environment.yml | Bin 438 -> 480 bytes pyproject.toml | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index a696d60..9999683 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -50,5 +50,11 @@ jobs: pip install -e ./[testing] - name: Test + if: always() run: | - pytest + coverage run -m pytest + + - name: Coverage report + if: always() + run: | + coverage report -m diff --git a/.gitignore b/.gitignore index 26d5f8b..ad78454 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ debug* *.h5 *.egg-info .vscode +.coverage + diff --git a/environment.yml b/environment.yml index 0bd1f61436fc384c15308a1ff782f442bcde7bea..63b4ba3a146b361acb20068f66cc89c2ce221ae1 100644 GIT binary patch delta 53 ucmdnS{D65v-^2+C6W^TDP+&knT_8J|A)ld)A(f$sA(0^+Nb@pqfiVD+9}6-7 delta 14 WcmaFByp4H6AEUy=-o%NYjsXBHtOle2 diff --git a/pyproject.toml b/pyproject.toml index 8aa8299..538ab26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,4 +72,4 @@ indent-width = 4 [tool.ruff.lint] select = ["E", "F", "W"] -ignore = ["F401"] \ No newline at end of file +ignore = ["F401"] From 143dd7f73b7cd58f707ad174e6193e41959ba8e0 Mon Sep 17 00:00:00 2001 From: daklauss Date: Tue, 6 Aug 2024 12:46:31 +0200 Subject: [PATCH 3/6] Add tests for rejection model --- tests/test_rejection.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/test_rejection.py diff --git a/tests/test_rejection.py b/tests/test_rejection.py new file mode 100644 index 0000000..6ec773d --- /dev/null +++ b/tests/test_rejection.py @@ -0,0 +1,33 @@ +import numpy as np +import pytest + +from CADETPythonSimulator.rejection import ( + RejectionBase, StepCutOff +) + + +TestCaseCutof = { + "model": StepCutOff, + "model_param": { + "cutoff_weight": 0.5 + }, + "weights": [0, 0.5, 1], + "expected": [0, 1, 1] + } + +@pytest.mark.parametrize( + "parameters", + [ + TestCaseCutof + ] +) + +class TestRejection(): + def test_get_rejection(self, parameters): + """ + Test to check wheter the get_rejection function works as intended + """ + model = parameters["model"](**parameters["model_param"]) + + solution = [model.get_rejection(weight) for weight in parameters["weights"]] + np.testing.assert_array_almost_equal(solution, parameters["expected"]) From de50cf1b3d08db34933d9a87849b9fc9ddd54a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Wed, 21 Aug 2024 10:31:27 +0200 Subject: [PATCH 4/6] Formatting --- CADETPythonSimulator/unit_operation.py | 20 ++++---- CADETPythonSimulator/viscosity.py | 12 +++-- tests/test_residual.py | 52 +++++++++---------- tests/test_unit_operation.py | 71 +++++++++++++++----------- 4 files changed, 88 insertions(+), 67 deletions(-) diff --git a/CADETPythonSimulator/unit_operation.py b/CADETPythonSimulator/unit_operation.py index be23726..b79f991 100644 --- a/CADETPythonSimulator/unit_operation.py +++ b/CADETPythonSimulator/unit_operation.py @@ -88,7 +88,7 @@ def initialize(self) -> NoReturn: @property def states(self) -> dict[str, State]: - """dict: State array blocks of the unit operation, indexed by name.""" + """dict: State array block of the unit operation, indexed by name.""" if self._states is None: raise NotInitializedError("Unit operation state is not yet initialized.") @@ -109,7 +109,7 @@ def y(self, y: np.ndarray) -> NoReturn: @property def state_derivatives(self) -> dict[str, State]: - """dict: State derivative array blocks of the unit operation, indexed by name.""" + """dict: State derivative array block of the unit operation, indexed by name.""" if self._state_derivatives is None: raise NotInitializedError("Unit operation state is not yet initialized.") @@ -321,7 +321,7 @@ def get_outlet_state( def get_outlet_state_flat( self, unit_port_index: int - ) -> NoReturn: + ) -> dict[str, np.ndarray]: """ Get the state of the unit operation outlet for a given port. @@ -567,14 +567,16 @@ def compute_residual( Q_in = self.Q_in[0] Q_out = self.Q_out[0] - # for i in range(self.n_comp): - # self.residuals['bulk']['c'][i] = c_dot[i] * V + V_dot * c[i] - Q_in * c_in[i] + Q_out * c[i] - # Alternative: Can we vectorize this? - self.residuals['bulk']['c'] = calculate_residual_concentration_cstr(c, c_dot, V, V_dot, Q_in, Q_out, c_in) - self.residuals['bulk']['Volume'] = calculate_residual_volume_cstr(V, V_dot, Q_in, Q_out) + self.residuals['bulk']['c'] = calculate_residual_concentration_cstr( + c, c_dot, V, V_dot, Q_in, Q_out, c_in + ) + + self.residuals['bulk']['Volume'] = calculate_residual_volume_cstr( + V, V_dot, Q_in, Q_out + ) - self.residuals['inlet']['viscosity'] = calculate_residuals_visc_cstr() + self.residuals['inlet']['viscosity'] = calculate_residual_visc_cstr() class DeadEndFiltration(UnitOperationBase): """ diff --git a/CADETPythonSimulator/viscosity.py b/CADETPythonSimulator/viscosity.py index 4abd69b..2169d63 100644 --- a/CADETPythonSimulator/viscosity.py +++ b/CADETPythonSimulator/viscosity.py @@ -9,7 +9,9 @@ class ViscosityBase(Structure): """Base class for mixed viscosity calculations.""" @abstractmethod - def get_mixture_viscosity(self, viscosities: np.ndarray, fractions: np.ndarray) -> float: + def get_mixture_viscosity( + self, viscosities: np.ndarray, fractions: np.ndarray + ) -> float: """Calculate mixed viscosity with given viscosities and volume fractions. Parameters @@ -44,7 +46,9 @@ def _validate_viscosities_input( class AverageViscosity(ViscosityBase): """Calculate mixed viscosity using the average mean.""" - def get_mixture_viscosity(self, viscosities: np.ndarray, fractions: np.ndarray) -> float: + def get_mixture_viscosity( + self, viscosities: np.ndarray, fractions: np.ndarray + ) -> float: """Calculate mixed viscosity using the arithmetic mean. Parameters @@ -68,7 +72,9 @@ def get_mixture_viscosity(self, viscosities: np.ndarray, fractions: np.ndarray) class LogarithmicMixingViscosity(ViscosityBase): """Calculate mixed viscosity using the logarithmic mixing rule.""" - def get_mixture_viscosity(self, viscosities: np.ndarray, fractions: np.ndarray) -> float: + def get_mixture_viscosity( + self, viscosities: np.ndarray, fractions: np.ndarray + ) -> float: """Calculate mixed viscosity using the logarithmic mixing rule. Parameters diff --git a/tests/test_residual.py b/tests/test_residual.py index b267e7c..d25c253 100644 --- a/tests/test_residual.py +++ b/tests/test_residual.py @@ -7,8 +7,8 @@ from CADETPythonSimulator.exception import CADETPythonSimError -# random number test -TestCaseConc_level1 = { +# Arbitrary parameter values +TestCaseCSTRConc_level1 = { "values": { "c": np.array([1, 2, 3]), "c_dot": np.array([4, 5, 6]), @@ -21,8 +21,8 @@ "expected": np.array([-11, -7, -3]) } -# flow in and out are equal, concentrations to -TestCaseConc_equal = { +# Flow in and out are equal, concentrations are equal +TestCaseCSTRConc_equal = { "values": { "c": np.array([0.1,]), "c_dot": np.array([0,]), @@ -35,8 +35,8 @@ "expected": np.array([0,]) } -# flow in and out are equal, but concentrations going into the unit are not -TestCaseConc_diffcin = { +# Flow in and out are equal, concentrations differ +TestCaseCSTRConc_diffcin = { "values": { "c": np.array([0.1,]), "c_dot": np.array([0,]), @@ -49,8 +49,8 @@ "expected": np.array([-0.1,]) } -# flow in and out are not equal, concentrantions going in are -TestCaseConc_diffvol = { +# Flow in and out differ, concentrations are equal +TestCaseCSTRConc_diffvol = { "values": { "c": np.array([0.1,]), "c_dot": np.array([0,]), @@ -63,8 +63,8 @@ "expected": np.array([0,]) } -# flow in and out are not, equal, concentrations aren't equal too -TestCaseConc_diffvolc = { +# Flow in and out differ, concentrations differ +TestCaseCSTRConc_diffvolc = { "values": { "c": np.array([0.1,]), "c_dot": np.array([0.2,]), @@ -81,11 +81,11 @@ @pytest.mark.parametrize( "parameters", [ - TestCaseConc_level1, - TestCaseConc_equal, - TestCaseConc_diffcin, - TestCaseConc_diffvol, - TestCaseConc_diffvolc + TestCaseCSTRConc_level1, + TestCaseCSTRConc_equal, + TestCaseCSTRConc_diffcin, + TestCaseCSTRConc_diffvol, + TestCaseCSTRConc_diffvolc ] ) class TestResidualConcCSTR(): @@ -93,12 +93,13 @@ def test_calculation_concentration_cstr(self, parameters): param_vec_conc = parameters["values"].values() - residual = calculate_residual_concentration_cstr(*param_vec_conc) + np.testing.assert_array_almost_equal( + calculate_residual_concentration_cstr(*param_vec_conc), + parameters["expected"] + ) - np.testing.assert_array_almost_equal(residual, parameters["expected"]) - -# random number test +# Arbitrary parameter values TestCaseVol = { "values": { "V": 1, @@ -120,7 +121,7 @@ def test_calculation_concentration_cstr(self, parameters): "expected": 0 } -# Flow in is larger than out +# Flow in is larger than flow out TestCaseVol_inge = { "values": { "V": 1, @@ -131,7 +132,7 @@ def test_calculation_concentration_cstr(self, parameters): "expected": 0 } -# Flow in is lesser than out +# Flow in is sameller than flow out TestCaseVol_inle = { "values": { "V": 1, @@ -142,11 +143,10 @@ def test_calculation_concentration_cstr(self, parameters): "expected": 0 } -# Residual does not depend on Volumne - +# Residual does not depend on volume TestCaseVol_vol = { "values": { - "V": 1e10, + "V": 1, "V_dot": 0, "Q_in": 0, "Q_out": 0, @@ -167,10 +167,10 @@ def test_calculation_concentration_cstr(self, parameters): ) class TestResidualVolCSTR(): def test_calculation_cstr(self, parameters): - param_vec_volume = parameters["values"].values() - residual = calculate_residual_volume_cstr(*param_vec_volume) + np.testing.assert_equal(residual, parameters["expected"]) + np.testing.assert_equal(residual, parameters["expected"]) diff --git a/tests/test_unit_operation.py b/tests/test_unit_operation.py index b03d9e0..b25ae33 100644 --- a/tests/test_unit_operation.py +++ b/tests/test_unit_operation.py @@ -14,6 +14,7 @@ ) + # %% Unit Operation Fixtures class TwoComponentFixture(ComponentSystem): def __init__(self, *args, **kwargs): @@ -70,7 +71,6 @@ def __init__(self, component_system=None, name='cstr', *args, **kwargs): super().__init__(component_system, name, *args, **kwargs) - class DeadEndFiltrationFixture(UnitOperationFixture, DeadEndFiltration): def __init__(self, component_system=None, @@ -111,7 +111,6 @@ def __init__(self, super().__init__(component_system, name, *args, **kwargs) - # %% Unit Operation State Structure @pytest.mark.parametrize( @@ -355,41 +354,51 @@ def test_initialize(self, unit_operation: UnitOperationBase, expected: dict): ( CstrFixture(), { - 'states' : { - 'inlet' : { - 'c' : np.array([7, 8]), - 'viscosity' : [3] + 'states': { + 'inlet': { + 'c': np.array([7, 8]), + 'viscosity': [3] }, - 'bulk' : { - 'c' : np.array([1, 2]), - 'Volume' : 1 + 'bulk': { + 'c': np.array([1, 2]), + 'Volume': 1 } }, - 'state_derivatives' : { - 'inlet' : { - 'c' : [6, 7] + 'state_derivatives': { + 'inlet': { + 'c': [6, 7] }, - 'bulk' : { - 'c' : np.array([4, 5]), - 'Volume' : 2 + 'bulk': { + 'c': np.array([4, 5]), + 'Volume': 2 } }, - 'Q_in' : [3], - 'Q_out' : [4] + 'Q_in': [3], + 'Q_out': [4] }, [ - ("calculate_residual_concentration_cstr", lambda c, c_dot, V, V_dot, Q_in, Q_out, c_in: c_dot * V + V_dot * c - Q_in * c_in + Q_out * c), - ("calculate_residuals_visc_cstr", lambda *args : 0), - ("calculate_residual_volume_cstr", lambda V, V_dot, Q_in, Q_out: V_dot - Q_in + Q_out) + ( + "calculate_residual_concentration_cstr", + lambda c, c_dot, V, V_dot, Q_in, Q_out, c_in: + c_dot * V + V_dot * c - Q_in * c_in + Q_out * c + ), + ( + "calculate_residual_visc_cstr", + lambda *args: 0 + ), + ( + "calculate_residual_volume_cstr", + lambda V, V_dot, Q_in, Q_out: V_dot - Q_in + Q_out + ) ], { - 'inlet' : { - 'c' : np.array([7, 8]), - 'viscosity' : 0 + 'inlet': { + 'c': np.array([7, 8]), + 'viscosity': 0 }, - 'bulk' : { - 'c' : np.array([-11,-7]), - 'Volume' : 3 + 'bulk': { + 'c': np.array([-11, -7]), + 'Volume': 3 } } ), @@ -429,7 +438,7 @@ def test_unit_residual( """Test the residual of unit operations.""" for funcname, func in residualfunc: - monkeypatch.setattr('CADETPythonSimulator.unit_operation.'+funcname, func ) + monkeypatch.setattr('CADETPythonSimulator.unit_operation.'+funcname, func) for key, value in case['states'].items(): unit_operation.states[key] = value @@ -444,9 +453,13 @@ def test_unit_residual( for unit_module, module_dict in expected.items(): for property, value in module_dict.items(): - np.testing.assert_equal(value, unit_operation.residuals[unit_module][property]) + np.testing.assert_equal( + value, + unit_operation.residuals[unit_module][property] + ) + # %% Run tests if __name__ == "__main__": - pytest.main(["test_unit_operation.py"]) \ No newline at end of file + pytest.main(["test_unit_operation.py"]) From 15a7fe715f71d98dce330532ef0282bad74d5f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Wed, 21 Aug 2024 10:27:01 +0200 Subject: [PATCH 5/6] Add custom ComponentSystem class --- CADETPythonSimulator/componentsystem.py | 265 ++++++++++++++++++++++++ CADETPythonSimulator/unit_operation.py | 3 +- tests/test_unit_operation.py | 4 +- 3 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 CADETPythonSimulator/componentsystem.py diff --git a/CADETPythonSimulator/componentsystem.py b/CADETPythonSimulator/componentsystem.py new file mode 100644 index 0000000..d790180 --- /dev/null +++ b/CADETPythonSimulator/componentsystem.py @@ -0,0 +1,265 @@ +from CADETProcess.processModel import ComponentSystem, Component, Species +from CADETProcess.dataStructure import UnsignedFloat, String, Integer +from CADETProcess.dataStructure import Structure + +from CADETPythonSimulator.exception import CADETPythonSimError +from functools import wraps + +class CPSSpecies(Structure): + """Species class. + + Represent a species in a chemical system. + Same as in cadet Process but with added + density and Volume + + Attributes + ---------- + name : str + The name of the species. + charge : int, optional + The charge of the species. Default is 0. + molecular_weight : float + The molecular weight of the species. + density : float + Density of the species. + molecular_volume : float + The molecular volume of the species + + """ + name = String() + charge = Integer(default=0) + molecular_weight = UnsignedFloat() + density = UnsignedFloat() + molecular_volume = UnsignedFloat() + +class CPSComponent(Component): + """Information about single component. + Inherits from CadetProcess Component + Same function but with fixed molecular weight and added densities and volume + + A component can contain subspecies (e.g. differently charged variants). + + Attributes + ---------- + name : String + Name of the component. + species : list + List of Subspecies. + n_species : int + Number of Subspecies. + label : list + Name of component (including species). + charge : list + Charge of component (including species). + molecular_weight : list + Molecular weight of component (including species). + density : list + density of component (including species). + molecular_volume : list + Molecular volume of component (including species). + + See Also + -------- + Species + ComponentSystem + + """ + def __init__(self, + name=None, + species=None, + charge=None, + molecular_weight=None, + density=None, + molecular_volume=None): + + self.name = name + self._species = [] + + if species is None: + self.add_species(name, charge, molecular_weight, density, molecular_volume) + elif isinstance(species, str): + self.add_species(species, + charge, molecular_weight, density, molecular_volume) + elif isinstance(species, list): + if charge is None: + charge = len(species) * [None] + if molecular_weight is None: + molecular_weight = len(species) * [None] + if density is None: + density = len(species) * [None] + if molecular_volume is None: + molecular_volume = len(species) * [None] + for i, spec in enumerate(species): + self.add_species(spec, + charge[i], molecular_weight[i], density[i], molecular_volume[i]) + else: + raise CADETPythonSimError("Could not determine number of species") + + def add_species(self, species, *args, **kwargs): + if not isinstance(species, CPSSpecies): + species = CPSSpecies(species, *args, **kwargs) + self._species.append(species) + + @property + def molecular_volume(self): + """list of float or None: The molecular volume of the subspecies.""" + return [spec.molecular_volume for spec in self.species] + + @property + def density(self): + """list of float or None: The density of the subspecies.""" + return [spec.density for spec in self.species] + + @property + def molecular_weight(self): + """list of float or None: The molecular weights of the subspecies.""" + return [spec.molecular_weight for spec in self.species] + +class CPSComponentSystem(ComponentSystem): + """Information about components in system. Inherits from Component System. Adds + molecular Volume to the Component System. + + A component can contain subspecies (e.g. differently charged variants). + + Attributes + ---------- + name : String + Name of the component system. + components : list + List of individual components. + n_species : int + Number of Subspecies. + n_comp : int + Number of all component species. + n_components : int + Number of components. + indices : dict + Component indices. + names : list + Names of all components. + species : list + Names of all component species. + charge : list + Charges of all components species. + molecular_weight : list + Molecular weights of all component species. + molecular_volume : list + Molecular volume of all component species. + + See Also + -------- + Species + Component + + """ + + def __init__( + self, + components=None, + name=None, + charges=None, + molecular_weights=None, + densities=None, + molecular_volume=None + ): + """Initialize the ComponentSystem object. + + Parameters + ---------- + components : int, list, None + The number of components or the list of components to be added. + If None, no components are added. + name : str, None + The name of the ComponentSystem. + charges : list, None + The charges of each component. + molecular_weights : list, None + The molecular weights of each component. + densities : list, None + Densities of each component + molecular_volume : list, None + The molecular volume of each component. + + Raises + ------ + CADETProcessError + If the `components` argument is neither an int nor a list. + + """ + + self.name = name + + self._components = [] + + 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 CADETPythonSimError("Could not determine number of components") + + if charges is None: + charges = n_comp * [None] + if molecular_weights is None: + molecular_weights = n_comp * [None] + if densities is None: + densities = n_comp * [None] + if molecular_volume is None: + molecular_volume = n_comp * [None] + + for i, comp in enumerate(components): + self.add_component( + comp, + charge=charges[i], + molecular_weight=molecular_weights[i], + density=densities[i], + molecular_volume=molecular_volume[i] + ) + + @wraps(CPSComponent.__init__) + def add_component(self, component, *args, **kwargs): + """ + Add a component to the system. + + Parameters + ---------- + component : {str, Component} + The class of the component to be added. + *args : list + The positional arguments to be passed to the component class's constructor. + **kwargs : dict + The keyword arguments to be passed to the component class's constructor. + + """ + if not isinstance(component, CPSComponent): + component = CPSComponent(component, *args, **kwargs) + + if component.name in self.names: + raise CADETPythonSimError( + f"Component '{component.name}' " + "already exists in ComponentSystem." + ) + + self._components.append(component) + + @property + def molecular_volumes(self): + """list: List of species molecular volumes.""" + molecular_volumes = [] + for comp in self.components: + molecular_volumes += comp.molecular_volume + + return molecular_volumes + + @property + def densities(self): + """list: List of species densities.""" + densities = [] + for comp in self.components: + densities += comp.density + + return densities diff --git a/CADETPythonSimulator/unit_operation.py b/CADETPythonSimulator/unit_operation.py index b79f991..86fb63e 100644 --- a/CADETPythonSimulator/unit_operation.py +++ b/CADETPythonSimulator/unit_operation.py @@ -11,6 +11,7 @@ ) from CADETProcess.dynamicEvents import Section +from CADETPythonSimulator.componentsystem import CPSComponentSystem from CADETPythonSimulator.exception import NotInitializedError, CADETPythonSimError from CADETPythonSimulator.state import State, state_factory from CADETPythonSimulator.residual import ( @@ -35,7 +36,7 @@ class UnitOperationBase(Structure): """ name = String() - component_system = Typed(ty=ComponentSystem) + component_system = Typed(ty=CPSComponentSystem) _state_structures = [] _parameters = [] diff --git a/tests/test_unit_operation.py b/tests/test_unit_operation.py index b25ae33..c78b813 100644 --- a/tests/test_unit_operation.py +++ b/tests/test_unit_operation.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from CADETProcess.processModel import ComponentSystem +from CADETPythonSimulator.componentsystem import CPSComponentSystem from CADETPythonSimulator.unit_operation import ( UnitOperationBase, @@ -16,7 +16,7 @@ # %% Unit Operation Fixtures -class TwoComponentFixture(ComponentSystem): +class TwoComponentFixture(CPSComponentSystem): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From 40c072d9312e24b2ac1632d31260f13c83c441f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Wed, 21 Aug 2024 10:30:29 +0200 Subject: [PATCH 6/6] Add implementation for simple dead end filtation unit --- CADETPythonSimulator/residual.py | 70 ++++++++++++- CADETPythonSimulator/unit_operation.py | 135 +++++++++++++++++-------- tests/test_residual.py | 115 ++++++++++++++++++++- tests/test_unit_operation.py | 73 +++++++++++-- 4 files changed, 335 insertions(+), 58 deletions(-) diff --git a/CADETPythonSimulator/residual.py b/CADETPythonSimulator/residual.py index 80d152c..5d3447c 100644 --- a/CADETPythonSimulator/residual.py +++ b/CADETPythonSimulator/residual.py @@ -77,6 +77,70 @@ def calculate_residual_visc_cstr(): return 0 -def calculate_residual_def(): - """Calculate the residual equations fo a dead end filtration equation.""" - raise NotImplementedError +def calculate_residual_cake_vol_def( + V_dot_f: float, + rejection: np.ndarray, + molar_volume: np.ndarray, + c_in: np.ndarray, + V_dot_C: float + ) -> float: + """ + Residual equation for the Cake Volume. + + Parameters + ---------- + V_dot_f : float + Flowrate of incoming feed + rejection : float + Rejection of the filter + gamma : float + Portion of suspended material + V_dot_C : float + Change of Cake Volume + """ + return -V_dot_C + np.sum(rejection * molar_volume * c_in * V_dot_f) + + +def calculate_residual_press_easy_def( + V_dot_Perm: float, + V_C: float, + deltap: float, + A: float, + mu: float, + Rm: float, + alpha: float + ) -> float: + """ + Calculate the residual equations fo a dead end filtration equation for the pressure + in the easy model. + + Parameters + ---------- + V_dot_Perm : np.ndarray + FLow of the Permeate through the membrane and Cake + V_C : float + Volume of the Cake + deltap : float + Pressure drop in this unit + A : float + Filtration area + mu : float + dynamic Viscosity + Rm : float + resistance of the medium + alpha : float + Specific cake resistance + """ + hyd_resistance = (Rm + alpha*V_C/A) * mu + + return -V_dot_Perm + deltap * A *hyd_resistance + + + +def calculate_residual_visc_def(): + """ + Calculate the residual of the Viscosity equation of the CSTR. + """ + warnings.warn("Viscosity of def not yet implemented") + + return 0 diff --git a/CADETPythonSimulator/unit_operation.py b/CADETPythonSimulator/unit_operation.py index 86fb63e..c774307 100644 --- a/CADETPythonSimulator/unit_operation.py +++ b/CADETPythonSimulator/unit_operation.py @@ -15,7 +15,12 @@ from CADETPythonSimulator.exception import NotInitializedError, CADETPythonSimError from CADETPythonSimulator.state import State, state_factory from CADETPythonSimulator.residual import ( - calculate_residual_volume_cstr, calculate_residual_concentration_cstr, calculate_residuals_visc_cstr + calculate_residual_volume_cstr, + calculate_residual_concentration_cstr, + calculate_residual_visc_cstr, + calculate_residual_press_easy_def, + calculate_residual_cake_vol_def, + calculate_residual_visc_def ) from CADETPythonSimulator.rejection import RejectionBase from CADETPythonSimulator.cake_compressibility import CakeCompressibilityBase @@ -595,9 +600,14 @@ class DeadEndFiltration(UnitOperationBase): Model for cake compressibility. """ - retentate = { + cake = { 'dimensions': (), - 'entries': {'c': 'n_comp', 'viscosity': 1, 'Rc': 1, 'mc': 'n_comp'}, + 'entries': {'c': 'n_comp', + 'viscosity': 1, + 'pressure': 1, + 'cakevolume': 1, + 'permeate': 1 + }, 'n_inlet_ports': 1, } permeate = { @@ -605,63 +615,106 @@ class DeadEndFiltration(UnitOperationBase): 'entries': {'c': 'n_comp', 'viscosity': 1, 'Volume': 1}, 'n_outlet_ports': 1, } - _state_structures = ['retentate', 'permeate'] - - rejection_model = Typed(ty=RejectionBase) - cake_compressibility_model = Typed(ty=CakeCompressibilityBase) + _state_structures = ['cake', 'permeate'] membrane_area = UnsignedFloat() membrane_resistance = UnsignedFloat() + specific_cake_resistance = UnsignedFloat() + rejection = Typed(ty=RejectionBase) _parameters = [ 'membrane_area', 'membrane_resistance', + 'specific_cake_resistance', + 'rejection' ] - def delta_p(self): - raise NotImplementedError() + def compute_residual( + self, + t: float, + ) -> NoReturn: - def specific_cake_resistance(self, delta_p: float) -> float: - """ - Compute specific resistance as a function of delta_p. + Q_in = self.Q_in[0] + Q_out = self.Q_out[0] - Parameters - ---------- - delta_p : float - Pressure difference. + c_in = self.states['cake']['c'] + c_in_dot = self.state_derivatives['cake']['c'] - Returns - ------- - float - Specific cake resistance. + V_C = self.states['cake']['cakevolume'] + V_dot_C = self.state_derivatives['cake']['cakevolume'] - """ - raise self.cake_compressibility_model.specific_cake_resistance(delta_p) + V_p = self.states['cake']['permeate'] + Q_p = self.state_derivatives['cake']['cakevolume'] - def compute_residual( - self, - t: float, - y: np.ndarray, - y_dot: np.ndarray, - residual: np.ndarray - ) -> NoReturn: - # 0, 1, 2 - # y = Vp, Rc, mc - # TODO: Needs to be extended to include c_in / c_out - # y = [*c_i_in], viscosity_in, Vp, Rc, mc, [*c_i_out], viscosity_out + viscosity_in = self.states['cake']['viscosity'] + + c = self.states['permeate']['c'] + c_dot = self.state_derivatives['permeate']['c'] - c_in = y[0: self.n_comp] - viscosity_in = y[self.n_comp] + V = self.states['permeate']['Volume'] + V_dot = self.state_derivatives['permeate']['Volume'] - densities = self.component_system.densities + deltap = self.states['cake']['pressure'] - residual[self.n_dof_coupling + 0] = ((self.membrane_area*self.delta_p(t)/viscosity_in)/(self.membrane_resistance+y[1])) - y_dot[0] - residual[self.n_dof_coupling + 1] = (1/self.membrane_area) * (y_dot[2] * self.specific_cake_resistance(self.p(t))) - y_dot[1] + #parameters + molecular_weights = self.component_system.molecular_weights + molar_volume = self.component_system.molecular_volumes + membrane_area = self.parameters['membrane_area'] + membrane_resistance = self.parameters['membrane_resistance'] + specific_cake_resistance = self.parameters['specific_cake_resistance'] - residual[self.n_dof_coupling + 2] = ((self.c(t) * y_dot[0]) / (1-self.c(t)/self.density)) - y_dot[2] + rejection = np.array( + [self.rejection.get_rejection(mw) for mw in molecular_weights]) - self.residuals['retentate'] - self.residuals['permeate'] + # Handle inlet DOFs, which are simply copied to the residual + self.residuals['cake']['c'] = c_in + self.residuals['cake']['cakevolume'] = calculate_residual_cake_vol_def( + Q_in, + rejection, + molar_volume, + c_in, + V_dot_C + ) + + self.residuals['cake']['pressure'] = calculate_residual_press_easy_def( + Q_p, + V_C, + deltap, + membrane_area, + viscosity_in, + membrane_resistance, + specific_cake_resistance + ) + + self.residuals['cake']['permeate'] = calculate_residual_volume_cstr( + V_C, + V_dot_C, + Q_in, + Q_p + ) + + self.residuals['cake']['viscosity'] = calculate_residual_visc_def() + + new_c_in = (1-rejection)*c_in + + self.residuals['permeate']['c'] = calculate_residual_concentration_cstr( + c, + c_dot, + V, + V_dot, + Q_p, + Q_out, + new_c_in + ) + + self.residuals['permeate']['Volume'] = calculate_residual_volume_cstr( + V, + V_dot, + Q_p, + Q_out + ) + + self.residuals['permeate']['viscosity'] = calculate_residual_visc_cstr() diff --git a/tests/test_residual.py b/tests/test_residual.py index d25c253..6781de2 100644 --- a/tests/test_residual.py +++ b/tests/test_residual.py @@ -2,7 +2,10 @@ import pytest from CADETPythonSimulator.residual import ( - calculate_residual_volume_cstr, calculate_residual_concentration_cstr + calculate_residual_volume_cstr, + calculate_residual_concentration_cstr, + calculate_residual_cake_vol_def, + calculate_residual_press_easy_def ) from CADETPythonSimulator.exception import CADETPythonSimError @@ -172,6 +175,114 @@ def test_calculation_cstr(self, parameters): np.testing.assert_equal(residual, parameters["expected"]) +# Testcase 1: Membrane rejects all +TestCaseDEFCake_rejects_all = { + "values": { + "V_dot_f": 1.0, + "rejection": np.array([1, 1]), + "molar_volume": np.array([1, 1]), + "c_in": np.array([0.5, 0.5]), + "V_dot_C": 1.0 + }, + "expected": 0 +} + + +# Testcase 2: Membrane rejects nothing +TestCaseDEFCake_rejects_not = { + "values": { + "V_dot_f": 1.0, + "rejection": np.array([0, 0]), + "molar_volume": np.array([1, 1]), + "c_in": np.array([0.5, 0.5]), + "V_dot_C": 0.0 + }, + "expected": 0 +} + +# Testcase 3: Membrane rejects only Component 2 +TestCaseDEFCake_rejects_2 = { + "values": { + "V_dot_f": 1.0, + "rejection": np.array([0, 1]), + "molar_volume": np.array([1, 1]), + "c_in": np.array([0.5, 0.5]), + "V_dot_C": 0.5 + }, + "expected": 0 +} + +# Testcase 4: Component 2 is larger then 1 +TestCaseDEFCake_C2_le_C1 = { + "values": { + "V_dot_f": 1.0, + "rejection": np.array([1, 1]), + "molar_volume": np.array([0.5, 1]), + "c_in": np.array([0.5, 0.5]), + "V_dot_C": 0.75 + }, + "expected": 0 +} + + +@pytest.mark.parametrize( + "parameters", + [ + TestCaseDEFCake_rejects_all, + TestCaseDEFCake_rejects_not, + TestCaseDEFCake_rejects_2, + TestCaseDEFCake_C2_le_C1 + ] +) +class TestResidualCakeVolDEF(): + def test_calculation_def(self, parameters): + param_vec_cake_vol = parameters["values"].values() + np.testing.assert_equal( + calculate_residual_cake_vol_def(*param_vec_cake_vol), + parameters["expected"] + ) + + +# Case 1 : Equally large hyraulic resistance +TestCaseDEFPressureDrop = { + "values": { + "V_dot_P": 1, + "V_C": 1, + "deltap": 0.5, + "A": 1, + "mu": 1, + "Rm": 1, + "alpha": 1, + }, + "expected": 0 +} + +# Case 2 : No cake yet +TestCaseDEFPressureDrop_no_cake = { + "values": { + "V_dot_P": 0.5, + "V_C": 0, + "deltap": 0.5, + "A": 1, + "mu": 1, + "Rm": 1, + "alpha": 1, + }, + "expected": 0 +} + + +@pytest.mark.parametrize( + "parameters", + [ + TestCaseDEFPressureDrop, + TestCaseDEFPressureDrop_no_cake + ] +) +class TestResidualPressureDropDEF(): + def test_calculation_def(self, parameters): + param_vec_pressure = parameters["values"].values() + residual = calculate_residual_press_easy_def(*param_vec_pressure) np.testing.assert_equal(residual, parameters["expected"]) @@ -198,14 +309,12 @@ def test_calculation_cstr(self, parameters): class TestResidualError(): def test_calculation_vol_cstr_error(self, parameters): - param_vec_volume = parameters["values"].values() with pytest.raises(CADETPythonSimError): calculate_residual_volume_cstr(*list(param_vec_volume)[2:6]) def test_calculation_concentration_cstr_error(self, parameters): - param_vec_volume = parameters["values"].values() with pytest.raises(CADETPythonSimError): diff --git a/tests/test_unit_operation.py b/tests/test_unit_operation.py index c78b813..2ef7aae 100644 --- a/tests/test_unit_operation.py +++ b/tests/test_unit_operation.py @@ -13,6 +13,7 @@ _2DGRM ) +from CADETPythonSimulator.rejection import StepCutOff # %% Unit Operation Fixtures @@ -20,8 +21,8 @@ class TwoComponentFixture(CPSComponentSystem): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.add_component('A', molecular_weight=1e3, density=1e3) - self.add_component('B', molecular_weight=10e3, density=1e3) + self.add_component('A', molecular_weight=1e3, density=1e3, molecular_volume=1) + self.add_component('B', molecular_weight=10e3, density=1e3, molecular_volume=1) class UnitOperationFixture(UnitOperationBase): @@ -76,7 +77,9 @@ def __init__(self, component_system=None, name='dead_end_filtration', membrane_area=1, - membrane_resistance=1e-9, + membrane_resistance=1, + specific_cake_resistance=1, + rejection=StepCutOff(cutoff_weight=0), *args, **kwargs ): @@ -84,6 +87,8 @@ def __init__(self, self.membrane_area = membrane_area self.membrane_resistance = membrane_resistance + self.specific_cake_resistance = specific_cake_resistance + self.rejection = rejection class CrossFlowFiltrationFixture(UnitOperationFixture, CrossFlowFiltration): @@ -183,7 +188,7 @@ def __init__(self, 'n_outlet_ports': 1, 'n_dof': 10, 'states': { - 'retentate': [0., 1., 2., 3., 4., 5.], + 'cake': [0., 1., 2., 3., 4., 5.], 'permeate': [6., 7., 8., 9.], }, 'inlet_state': { @@ -402,13 +407,59 @@ def test_initialize(self, unit_operation: UnitOperationBase, expected: dict): } } ), - # ( - # DeadEndFiltrationFixture(), - # { - # 'expected_residual': { - # }, - # }, - # ), + ( + DeadEndFiltrationFixture(), + { + 'states': { + 'cake': { + 'c': np.array([0.5, 0.5]), + 'viscosity': 1, + 'pressure': 1, + 'cakevolume': 1, + 'permeate': 1, + }, + 'permeate': { + 'c': np.array([0.5, 0.5]), + 'viscosity': 1, + 'Volume': 1, + } + }, + 'state_derivatives': { + 'cake': { + 'c': np.array([0.5, 0.5]), + 'viscosity': 1, + 'pressure': 1, + 'cakevolume': 1, + 'permeate': 1, + }, + 'permeate': { + 'c': np.array([0.5, 0.5]), + 'viscosity': 1, + 'Volume': 1, + } + }, + 'Q_in': [1], + 'Q_out': [1] + }, + [ + ('CPSComponentSystem.molecular_weights', [1, 1]), + ('CPSComponentSystem.molecular_volumes', [1, 1]) + ], + { + 'cake': { + 'c': np.array([0.5, 0.5]), + 'viscosity': 0, + 'pressure': 1, + 'cakevolume': 0, + 'permeate': 1, + }, + 'permeate': { + 'c': np.array([1.5, 1.5]), + 'viscosity': 0, + 'Volume': 1, + } + } + ), # ( # CrossFlowFiltrationFixture(), # {