From 82076afbdec89c433ad484133b588cb31bcd9b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Schm=C3=B6lder?= Date: Sat, 22 Jun 2024 11:49:18 +0200 Subject: [PATCH 1/7] [WIP] Fix CSTR residual implementation --- CADETPythonSimulator/unit_operation.py | 32 +++++++++++++++----------- tests/test_unit_operation.py | 19 +++------------ 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/CADETPythonSimulator/unit_operation.py b/CADETPythonSimulator/unit_operation.py index 4865faf..9e0eec1 100644 --- a/CADETPythonSimulator/unit_operation.py +++ b/CADETPythonSimulator/unit_operation.py @@ -544,28 +544,32 @@ def compute_residual( t : float Time at which to evaluate the residual. """ - c_in = inlet_state.c + c_in = self.states['inlet']['c'] + c_in_dot = self.state_derivatives['inlet']['c'] - viscosity_in = inlet_state.viscosity + viscosity_in = self.states['inlet']['viscosity'] + c = self.states['bulk']['c'] + c_dot = self.state_derivatives['bulk']['c'] - c_in = self.y[0:self.n_comp] - c_in_dot = self.y_dot[0:self.n_comp] + V = self.states['bulk']['Volume'] + V_dot = self.state_derivatives['bulk']['Volume'] - c = self.y[self.n_comp, 2*self.n_comp] - c_dot = self.y_dot[self.n_comp, 2*self.n_comp] + # Handle inlet DOFs, which are simply copied to the residual + self.residuals['inlet']['c'] = c_in - V = self.y[-1] - V_dot = self.y_dot[-1] + # Handle bulk/outlet DOFs + Q_in = self.Q_in[0] + Q_out = self.Q_out[0] - # Handle inlet DOFs, which are simply copied to the residual - self.residual[0: self.n_comp] = c_in + # 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'] = c_dot * V + V_dot * c - Q_in * c_in + Q_out * c - # TODO: What about Q_in / Q_out? How are they passed to the unit operation? - for i in self.range(self.n_comp): - self.residual[self.n_comp + i] = c_dot[i] * V + V_dot * c[i] - Q_in * c_in[i] + Q_out * c[i] + self.residuals['bulk']['Volume'] = V_dot - self.Q_in + Q_out - self.residual[-1] = V_dot - Q_in + Q_out + # TODO: What about viscosities? class DeadEndFiltration(UnitOperationBase): diff --git a/tests/test_unit_operation.py b/tests/test_unit_operation.py index 5586f7a..ac79afd 100644 --- a/tests/test_unit_operation.py +++ b/tests/test_unit_operation.py @@ -354,27 +354,14 @@ def test_initialize(self, unit_operation: UnitOperationBase, expected: dict): ( CstrFixture(), { - 'y': [0, 0, 1, 2, 1], - 'y_dot': [0, 0, 0, 0, 0], + 'y': [0., 0., 0., 0., 0., 0., 0.], + 'y_dot': [0., 0., 0., 0., 0., 0., 0.], 'Q_in': 0, 'Q_out': 0, 't': 0, }, { - 'residual': [0, 0, 0, 0, 0] - }, - ), - ( - CstrFixture(), - { - 'y': [1, 2, 1, 2, 1], - 'y_dot': [0, 0, 0, 0, 0], - 'Q_in': 0, - 'Q_out': 0, - 't': 0, - }, - { - 'residual': [1, 2, 0, 0, 0] + 'residual': [0., 0., 0., 0., 0., 0., 0.] }, ), # ( From 264206daa4dc270f28735e586fd8672fe6967790 Mon Sep 17 00:00:00 2001 From: daklauss Date: Tue, 2 Jul 2024 13:20:15 +0200 Subject: [PATCH 2/7] Adding seperate residual calculation Added residual and testfiles Removed false risidual, + cstr residual v<0 => error Seperated error testing in another class Throw not implemented error @viscosity Notinitialized Error now inherits from CADETPython SimulatorError --- CADETPythonSimulator/exception.py | 9 ++- CADETPythonSimulator/residual.py | 84 ++++++++++++++++++++ CADETPythonSimulator/unit_operation.py | 24 ++++-- pyproject.toml | 5 ++ tests/test_residual.py | 103 +++++++++++++++++++++++++ tests/test_unit_operation.py | 70 +++++++++++++---- 6 files changed, 271 insertions(+), 24 deletions(-) create mode 100644 CADETPythonSimulator/residual.py create mode 100644 tests/test_residual.py diff --git a/CADETPythonSimulator/exception.py b/CADETPythonSimulator/exception.py index 6ebef7c..313171f 100644 --- a/CADETPythonSimulator/exception.py +++ b/CADETPythonSimulator/exception.py @@ -1,3 +1,8 @@ -class NotInitializedError(Exception): - """Exception raised when a unit operation is not yet initialized.""" + +class CADETPythonSimError(Exception): + """Typical Exception for Error Handling""" pass + +class NotInitializedError(CADETPythonSimError): + """Exception raised when a unit operation is not yet initialized.""" + pass \ No newline at end of file diff --git a/CADETPythonSimulator/residual.py b/CADETPythonSimulator/residual.py new file mode 100644 index 0000000..ffa2b8d --- /dev/null +++ b/CADETPythonSimulator/residual.py @@ -0,0 +1,84 @@ +import numpy as np +from CADETPythonSimulator.exception import CADETPythonSimError +import warnings + +def calculate_residual_volume_cstr( + V : float, + V_dot : float, + Q_in : float , + Q_out : float + ) -> float: + """ + Calculates the residual equations of the volume of a cstr. + + Parameters + ---------- + V : float + Volume within the CSTR + V_dot : float + Volume change rate of the CSTR + Q_in : float + Volume entering the Unit + Q_out : float + Volume leaving the Unit + Returns + ------- + float + Residual of the Flow equation of the CSTR with dimensions like the inpu + """ + + if V < 0: + raise CADETPythonSimError("V can't be less then zero") + + return V_dot - Q_in + Q_out + +def calculate_residual_concentration_cstr( + c : np.ndarray, + c_dot : np.ndarray, + V : float, + V_dot : float, + Q_in : float, + Q_out : float, + c_in : np.ndarray + ) -> np.ndarray : + """ + Calculates the residual equations of the concentration of a cstr + + Parameters + ---------- + c : np.ndarray + Concentration + c_dot : np.ndarray + Changing of the concentration + V : float + Volume within the CSTR + V_dot : float + Volume change rate of the CSTR + Q_in : float + Volume entering the Unit + Q_out : float + Volume leaving the Unit + c_in : np.ndarray + Initial concentration + """ + if V < 0: + raise CADETPythonSimError("V can't be less then zero") + + + return c_dot * V + V_dot * c - Q_in * c_in + Q_out * c + + +def calculate_residuals_visc_cstr(): + """ + Calculates the residual of the Viscosity equation of the CSTR + """ + warnings.warn("Viscosity of CSTR not yet implemented") + + return 0 + + +def calculate_residual_def(): + """ + Calculates the residual equations fo a dead end filtration equation. + """ + raise NotImplementedError \ No newline at end of file diff --git a/CADETPythonSimulator/unit_operation.py b/CADETPythonSimulator/unit_operation.py index 9e0eec1..fef492c 100644 --- a/CADETPythonSimulator/unit_operation.py +++ b/CADETPythonSimulator/unit_operation.py @@ -11,10 +11,14 @@ ) from CADETProcess.dynamicEvents import Section -from CADETPythonSimulator.exception import NotInitializedError +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 + ) from CADETPythonSimulator.rejection import RejectionBase from CADETPythonSimulator.cake_compressibility import CakeCompressibilityBase +from CADETPythonSimulator.viscosity import LogarithmicMixingViscosity, ViscosityBase class UnitOperationBase(Structure): @@ -475,9 +479,9 @@ def compute_residual( """ # Inlet DOFs are simply copied to the residual. - for i in range(self.n_dof_coupling): - residual[i] = self.y[i] + self.residuals['outlet']['c_poly'] = self.states['outlet']['c_poly'] + self.residuals['outlet']['viscosity'] = self.states['outlet']['viscosity'] class Outlet(UnitOperationBase): """System outlet.""" @@ -532,6 +536,7 @@ class Cstr(UnitOperationBase): } _state_structures = ['inlet', 'bulk'] + def compute_residual( self, t: float, @@ -565,12 +570,11 @@ def compute_residual( # 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'] = c_dot * V + V_dot * c - Q_in * c_in + Q_out * c - - self.residuals['bulk']['Volume'] = V_dot - self.Q_in + Q_out + self.residuals['bulk']['c'] = calculate_residual_concentration_cstr(c, c_dot, V, V_dot, Q_in, Q_out, c_in) - # TODO: What about viscosities? + self.residuals['bulk']['Volume'] = calculate_residual_volume_cstr(V, V_dot, Q_in, Q_out) + self.residuals['inlet']['viscosity'] = calculate_residuals_visc_cstr() class DeadEndFiltration(UnitOperationBase): """ @@ -653,6 +657,10 @@ def compute_residual( residual[self.n_dof_coupling + 2] = ((self.c(t) * y_dot[0]) / (1-self.c(t)/self.density)) - y_dot[2] + self.residuals['retentate'] + self.residuals['permeate'] + + class CrossFlowFiltration(UnitOperationBase): """ @@ -778,4 +786,4 @@ def compute_residual( Residual of the unit operation. """ - raise NotImplementedError() + raise NotImplementedError() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7bf1710..523485d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,3 +64,8 @@ markers = [ [tool.setuptools.dynamic] version = { attr = "CADETPythonSimulator.__version__" } + +[tool.ruff] +# Same as Black. +line-length = 88 +indent-width = 4 \ No newline at end of file diff --git a/tests/test_residual.py b/tests/test_residual.py new file mode 100644 index 0000000..1bca14c --- /dev/null +++ b/tests/test_residual.py @@ -0,0 +1,103 @@ +from CADETPythonSimulator.residual import ( + calculate_residual_volume_cstr, calculate_residual_concentration_cstr +) +from CADETPythonSimulator.exception import CADETPythonSimError +import pytest +import numpy as np + + +TestCaseConc = { + "values" : { + "c" : np.array([1, 2, 3]), + "c_dot" : np.array([4, 5, 6]), + "V" : 1, + "V_dot" : 2, + "Q_in" : 3, + "Q_out" : 4, + "c_in" : np.array([7, 8, 9]) + }, + "expected" : np.array([-11,-7,-3]) +} + + + +@pytest.mark.parametrize( + "parameters", + [ + TestCaseConc + ] +) +class TestResidualConcCSTR(): + def test_calculation_concentration_cstr(self, parameters): + + param_vec_conc = parameters["values"].values() + + np.testing.assert_equal(calculate_residual_concentration_cstr(*param_vec_conc), parameters["expected"]) + + + + + +TestCaseVol = { + "values" : { + "V" : 1, + "V_dot" : 2, + "Q_in" : 3, + "Q_out" : 4, + }, + "expected" : 3 +} + + + +@pytest.mark.parametrize( + "parameters", + [ + TestCaseVol + ] +) + +class TestResidualVolCSTR(): + def test_calculation_cstr(self, parameters): + + param_vec_volume = parameters["values"].values() + + np.testing.assert_equal(calculate_residual_volume_cstr(*param_vec_volume), parameters["expected"]) + +TestCaseConcError = { + "values" : { + "c" : np.array([1, 2, 3]), + "c_dot" : np.array([4, 5, 6]), + "V" : -1, + "V_dot" : 2, + "Q_in" : 3, + "Q_out" : 4, + "c_in" : np.array([7, 8, 9]) + }, + "expected" : np.array([-11,-7,-3]) +} + + +@pytest.mark.parametrize( + "parameters", + [ + TestCaseConcError + ] +) + + +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): + calculate_residual_concentration_cstr(*param_vec_volume) diff --git a/tests/test_unit_operation.py b/tests/test_unit_operation.py index ac79afd..b03d9e0 100644 --- a/tests/test_unit_operation.py +++ b/tests/test_unit_operation.py @@ -70,6 +70,7 @@ 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, @@ -335,7 +336,7 @@ def test_initialize(self, unit_operation: UnitOperationBase, expected: dict): @pytest.mark.parametrize( - "unit_operation, case, expected", + "unit_operation, case, residualfunc, expected", [ # ( # InletFixture(), @@ -354,15 +355,43 @@ def test_initialize(self, unit_operation: UnitOperationBase, expected: dict): ( CstrFixture(), { - 'y': [0., 0., 0., 0., 0., 0., 0.], - 'y_dot': [0., 0., 0., 0., 0., 0., 0.], - 'Q_in': 0, - 'Q_out': 0, - 't': 0, + 'states' : { + 'inlet' : { + 'c' : np.array([7, 8]), + 'viscosity' : [3] + }, + 'bulk' : { + 'c' : np.array([1, 2]), + 'Volume' : 1 + } + }, + 'state_derivatives' : { + 'inlet' : { + 'c' : [6, 7] + }, + 'bulk' : { + 'c' : np.array([4, 5]), + 'Volume' : 2 + } + }, + '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) + ], { - 'residual': [0., 0., 0., 0., 0., 0., 0.] - }, + 'inlet' : { + 'c' : np.array([7, 8]), + 'viscosity' : 0 + }, + 'bulk' : { + 'c' : np.array([-11,-7]), + 'Volume' : 3 + } + } ), # ( # DeadEndFiltrationFixture(), @@ -391,20 +420,33 @@ class TestUnitResidual(): def test_unit_residual( self, + monkeypatch, unit_operation: UnitOperationBase, case: dict, - expected: dict, + residualfunc: dict, + expected: dict ) -> NoReturn: """Test the residual of unit operations.""" - unit_operation.y = case['y'] - unit_operation.y_dot = case['y_dot'] - unit_operation.compute_residual(case['t']) + for funcname, func in residualfunc: + monkeypatch.setattr('CADETPythonSimulator.unit_operation.'+funcname, func ) + + for key, value in case['states'].items(): + unit_operation.states[key] = value + + for key, value in case['state_derivatives'].items(): + unit_operation.state_derivatives[key] = value + + unit_operation._Q_in = case['Q_in'] + unit_operation._Q_out = case['Q_out'] - np.testing.assert_almost_equal(unit_operation.r, expected['residual']) + unit_operation.compute_residual(3) + 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]) # %% Run tests if __name__ == "__main__": - pytest.main(["test_unit_operation.py"]) + pytest.main(["test_unit_operation.py"]) \ No newline at end of file From dd31d0bce9fa1cfc48154b682fb97c2ef61484fa Mon Sep 17 00:00:00 2001 From: daklauss Date: Mon, 15 Jul 2024 16:49:35 +0200 Subject: [PATCH 3/7] pipelining happening adding a pipeline.yml for testing with pytest on push adding ruff.yml wip addded a ruff workflow added python 3.12 to the pipeline to check when sundials and numpy are working --- .github/workflows/pipeline.yml | 54 +++++++++++++++ .github/workflows/ruff.yml | 10 +++ .gitignore | 1 + environment.yml | Bin 0 -> 438 bytes tests/test_residual.py | 120 +++++++++++++++++++++++++++++++-- 5 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/pipeline.yml create mode 100644 .github/workflows/ruff.yml create mode 100644 environment.yml diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml new file mode 100644 index 0000000..8178685 --- /dev/null +++ b/.github/workflows/pipeline.yml @@ -0,0 +1,54 @@ +name: pipeline + +on: + push: + branches: + - master + - dev + - add-dead-end-filtration + - test_ci + pull_request: + + +jobs: + test-job: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash -l {0} + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + + env: + CONDA_FILE: environment.yml + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Setup Conda Environment + uses: conda-incubator/setup-miniconda@v3 + with: + miniforge-variant: Mambaforge + miniforge-version: latest + use-mamba: true + activate-environment: cadpythonsim + channels: conda-forge, + + - name: install conda env + run: | + mamba env update -n cadpythonsim -f ${{ env.CONDA_FILE }} + + - name: Install + run: | + python -m pip install --upgrade pip + pip install -e ./[testing] + + - name: Test + run: | + pytest diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..780e902 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,10 @@ +name: ruff + +name: Ruff +on: [ push, pull_request ] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index fa31b95..26d5f8b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ debug* *.h5 *.egg-info +.vscode diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000000000000000000000000000000000000..0bd1f61436fc384c15308a1ff782f442bcde7bea GIT binary patch literal 438 zcmZXQv2MaZ5Jaa&;veXdaZi;fp{Jm?9Q(jlf;nLy5jx~bh`6;XJ>YIe!d6Q zIx1+bg&N{kQCCgsbkd+vJ?LCBunu(K1bvZT=j;Mjk(sV;sVKCEdDekC^IE= Date: Mon, 29 Jul 2024 14:27:49 +0200 Subject: [PATCH 4/7] first implementation of easy def model residuals + unit operation still need testing --- CADETPythonSimulator/residual.py | 95 ++++++++++++++++++++++-- CADETPythonSimulator/unit_operation.py | 99 ++++++++++++++------------ 2 files changed, 145 insertions(+), 49 deletions(-) diff --git a/CADETPythonSimulator/residual.py b/CADETPythonSimulator/residual.py index ffa2b8d..da87b03 100644 --- a/CADETPythonSimulator/residual.py +++ b/CADETPythonSimulator/residual.py @@ -68,7 +68,7 @@ def calculate_residual_concentration_cstr( return c_dot * V + V_dot * c - Q_in * c_in + Q_out * c -def calculate_residuals_visc_cstr(): +def calculate_residual_visc_cstr(): """ Calculates the residual of the Viscosity equation of the CSTR """ @@ -77,8 +77,95 @@ def calculate_residuals_visc_cstr(): return 0 -def calculate_residual_def(): +def calculate_residual_cake_vol_def( + V_dot_f : float, + eff : np.ndarray, + molar_volume : np.ndarray, + c_in : np.ndarray, + V_dot_C : float + ) -> float: + """ + Residual equation for the Volume + + Parameters + ---------- + V_dot_f : float + flowrate of incoming feed + eff : float + efficency of the filter + gamma : float + portion of suspended material + V_dot_C : float + change of Cake Volume + """ + + return -V_dot_C + np.sum(eff * molar_volume * c_in * V_dot_f) + + +def calculate_residual_press_easy_def( + V_dot_P : float, + V_C : float, + deltap : float, + A :float, + mu : float, + Rm : float, + alpha : float + ) -> float: + """ + Calculates the residual equations fo a dead end filtration equation for the pressure + in the easy model. + + Parameters + ---------- + V_dot_P : 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_P + deltap * A *hyd_resistance + + +def calculate_residual_perm_easy_def( + Q_in : float, + V_dot_C : float, + V_dot_P : float + ) -> float: + """ + Calculates the residual equations fo a dead end filtration equation for the permeate Volume + in the easy model. + + Parameters + ---------- + Q_in : float + Flow entering the unit operation + V_dot_P : float + FLow of the Permeate through the membrane and Cake + V_dot_C : float + Volume of the Retentate becoming the Cake + """ + + + return -Q_in + V_dot_C + V_dot_P + + +def calculate_residual_visc_def(): + """ - Calculates the residual equations fo a dead end filtration equation. + Calculates the residual of the Viscosity equation of the CSTR """ - raise NotImplementedError \ No newline at end of file + warnings.warn("Viscosity of def not yet implemented") + + return 0 \ No newline at end of file diff --git a/CADETPythonSimulator/unit_operation.py b/CADETPythonSimulator/unit_operation.py index fef492c..c2ca9e6 100644 --- a/CADETPythonSimulator/unit_operation.py +++ b/CADETPythonSimulator/unit_operation.py @@ -14,7 +14,13 @@ 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_perm_easy_def, + calculate_residual_visc_def ) from CADETPythonSimulator.rejection import RejectionBase from CADETPythonSimulator.cake_compressibility import CakeCompressibilityBase @@ -564,8 +570,8 @@ def compute_residual( self.residuals['inlet']['c'] = c_in # Handle bulk/outlet DOFs - Q_in = self.Q_in[0] - Q_out = self.Q_out[0] + Q_in = self.Q_in + Q_out = self.Q_out # 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] @@ -574,7 +580,7 @@ def compute_residual( 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): """ @@ -592,73 +598,76 @@ 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 = { + bulk = { 'dimensions': (), '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', 'bulk'] membrane_area = UnsignedFloat() membrane_resistance = UnsignedFloat() + specific_cake_resistance = UnsignedFloat() + molar_density = np.ndarray() + efficency = np.ndarray() _parameters = [ 'membrane_area', 'membrane_resistance', + 'specific_cake_resistance', + 'molar_volume', + 'efficency' ] - 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_in = y[0: self.n_comp] - viscosity_in = y[self.n_comp] + c = self.states['bulk']['c'] + c_dot = self.state_derivatives['bulk']['c'] - densities = self.component_system.densities + V = self.states['bulk']['Volume'] + V_dot = self.state_derivatives['bulk']['Volume'] - 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] + deltap = self.states['cake']['pressure'] - residual[self.n_dof_coupling + 2] = ((self.c(t) * y_dot[0]) / (1-self.c(t)/self.density)) - y_dot[2] + #parameters + efficency = self.parameters['efficency'] + molar_volume = self.parameters['molar_volume'] + membrane_area = self.parameters['membrane_area'] + membrane_resistance = self.parameters['membrane_resistance'] + specific_cake_resistance = self.parameters['specific_cake_resistance'] - 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, efficency, 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_perm_easy_def(Q_in, V_dot_C, Q_p) + self.residuals['cake']['viscosity'] = calculate_residual_visc_def() + + self.residuals['bulk']['c'] = calculate_residual_concentration_cstr(c, c_dot, V, V_dot, Q_p, Q_out, c_in) + self.residuals['bulk']['Volume'] = calculate_residual_volume_cstr(V, V_dot, Q_p, Q_out) + self.residuals['inlet']['viscosity'] = calculate_residual_visc_cstr() From 31d644732b5e2992418de04e358242d0b1493225 Mon Sep 17 00:00:00 2001 From: daklauss Date: Mon, 29 Jul 2024 16:57:19 +0200 Subject: [PATCH 5/7] Fixed Testing Not all tests where found, now all are executed .... but not all finish --- CADETPythonSimulator/unit_operation.py | 10 +++++----- tests/test_unit_operation.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CADETPythonSimulator/unit_operation.py b/CADETPythonSimulator/unit_operation.py index c2ca9e6..a57ce34 100644 --- a/CADETPythonSimulator/unit_operation.py +++ b/CADETPythonSimulator/unit_operation.py @@ -570,8 +570,8 @@ def compute_residual( self.residuals['inlet']['c'] = c_in # Handle bulk/outlet DOFs - Q_in = self.Q_in - Q_out = self.Q_out + 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] @@ -613,8 +613,8 @@ class DeadEndFiltration(UnitOperationBase): membrane_area = UnsignedFloat() membrane_resistance = UnsignedFloat() specific_cake_resistance = UnsignedFloat() - molar_density = np.ndarray() - efficency = np.ndarray() + molar_volume = SizedUnsignedNdArray(size = 'n_comp') + efficency = SizedUnsignedNdArray(size = 'n_comp') _parameters = [ 'membrane_area', @@ -667,7 +667,7 @@ def compute_residual( self.residuals['bulk']['c'] = calculate_residual_concentration_cstr(c, c_dot, V, V_dot, Q_p, Q_out, c_in) self.residuals['bulk']['Volume'] = calculate_residual_volume_cstr(V, V_dot, Q_p, Q_out) - self.residuals['inlet']['viscosity'] = calculate_residual_visc_cstr() + self.residuals['bulk']['viscosity'] = calculate_residual_visc_cstr() diff --git a/tests/test_unit_operation.py b/tests/test_unit_operation.py index b03d9e0..b4fad0f 100644 --- a/tests/test_unit_operation.py +++ b/tests/test_unit_operation.py @@ -379,7 +379,7 @@ def test_initialize(self, unit_operation: UnitOperationBase, expected: dict): }, [ ("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_visc_cstr", lambda *args : 0), ("calculate_residual_volume_cstr", lambda V, V_dot, Q_in, Q_out: V_dot - Q_in + Q_out) ], { From 321bba771b8d85db9e3af6d372c8ed6f0e56b45c Mon Sep 17 00:00:00 2001 From: daklauss Date: Wed, 31 Jul 2024 09:32:12 +0200 Subject: [PATCH 6/7] implemented componentsystem that inherits from cadet process and fixes the small bug with the molecular weight, added molecular volume first implementation of the def unit_operation and residual equations still both still need testing and unit_operation def doesn't have rejection model yet --- CADETPythonSimulator/componentsystem.py | 265 ++++++++++++++++++++++++ CADETPythonSimulator/residual.py | 33 +-- CADETPythonSimulator/system_solver.py | 1 + CADETPythonSimulator/unit_operation.py | 100 ++++++--- pyproject.toml | 6 +- tests/test_residual.py | 39 +++- tests/test_unit_operation.py | 22 +- 7 files changed, 394 insertions(+), 72 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/residual.py b/CADETPythonSimulator/residual.py index da87b03..8fc2318 100644 --- a/CADETPythonSimulator/residual.py +++ b/CADETPythonSimulator/residual.py @@ -23,10 +23,10 @@ def calculate_residual_volume_cstr( Volume leaving the Unit Returns ------- - float + float Residual of the Flow equation of the CSTR with dimensions like the inpu """ - + if V < 0: raise CADETPythonSimError("V can't be less then zero") @@ -43,7 +43,7 @@ def calculate_residual_concentration_cstr( ) -> np.ndarray : """ Calculates the residual equations of the concentration of a cstr - + Parameters ---------- c : np.ndarray @@ -114,7 +114,7 @@ def calculate_residual_press_easy_def( """ Calculates the residual equations fo a dead end filtration equation for the pressure in the easy model. - + Parameters ---------- V_dot_P : np.ndarray @@ -138,34 +138,11 @@ def calculate_residual_press_easy_def( return -V_dot_P + deltap * A *hyd_resistance -def calculate_residual_perm_easy_def( - Q_in : float, - V_dot_C : float, - V_dot_P : float - ) -> float: - """ - Calculates the residual equations fo a dead end filtration equation for the permeate Volume - in the easy model. - - Parameters - ---------- - Q_in : float - Flow entering the unit operation - V_dot_P : float - FLow of the Permeate through the membrane and Cake - V_dot_C : float - Volume of the Retentate becoming the Cake - """ - - - return -Q_in + V_dot_C + V_dot_P - def calculate_residual_visc_def(): - """ Calculates the residual of the Viscosity equation of the CSTR """ warnings.warn("Viscosity of def not yet implemented") - return 0 \ No newline at end of file + return 0 diff --git a/CADETPythonSimulator/system_solver.py b/CADETPythonSimulator/system_solver.py index fbd04ca..ce9d6f3 100644 --- a/CADETPythonSimulator/system_solver.py +++ b/CADETPythonSimulator/system_solver.py @@ -423,6 +423,7 @@ def couple_unit_operations( origin_port = origin_info['port'] y_origin_unit = y_initial[self.unit_slices[origin_unit]] + s_unit = origin_unit.get_outlet_state(y_origin_unit, origin_port) s_new += s_unit * Q_destination # Accumulate weighted states diff --git a/CADETPythonSimulator/unit_operation.py b/CADETPythonSimulator/unit_operation.py index a57ce34..6013593 100644 --- a/CADETPythonSimulator/unit_operation.py +++ b/CADETPythonSimulator/unit_operation.py @@ -14,12 +14,11 @@ 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_residual_volume_cstr, + calculate_residual_concentration_cstr, calculate_residual_visc_cstr, calculate_residual_press_easy_def, calculate_residual_cake_vol_def, - calculate_residual_perm_easy_def, calculate_residual_visc_def ) from CADETPythonSimulator.rejection import RejectionBase @@ -115,7 +114,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.") @@ -573,12 +572,14 @@ 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_residual_visc_cstr() @@ -600,34 +601,38 @@ class DeadEndFiltration(UnitOperationBase): """ cake = { 'dimensions': (), - 'entries': {'c': 'n_comp', 'viscosity': 1, 'pressure': 1, 'cakevolume': 1, 'permeate': 1}, + 'entries': {'c': 'n_comp', + 'viscosity': 1, + 'pressure': 1, + 'cakevolume': 1, + 'permeate': 1 + }, 'n_inlet_ports': 1, } - bulk = { + permeate = { 'dimensions': (), 'entries': {'c': 'n_comp', 'viscosity': 1, 'Volume': 1}, 'n_outlet_ports': 1, } - _state_structures = ['cake', 'bulk'] + _state_structures = ['cake', 'permeate'] membrane_area = UnsignedFloat() membrane_resistance = UnsignedFloat() specific_cake_resistance = UnsignedFloat() - molar_volume = SizedUnsignedNdArray(size = 'n_comp') - efficency = SizedUnsignedNdArray(size = 'n_comp') +# molar_volume = SizedUnsignedNdArray(size = 'n_comp') => component system erben von cadet process + rejection = Typed(ty=RejectionBase) _parameters = [ 'membrane_area', 'membrane_resistance', 'specific_cake_resistance', - 'molar_volume', - 'efficency' + 'rejection' ] def compute_residual( self, t: float, - ) -> NoReturn: + ) -> NoReturn: Q_in = self.Q_in[0] Q_out = self.Q_out[0] @@ -643,11 +648,11 @@ def compute_residual( viscosity_in = self.states['cake']['viscosity'] - c = self.states['bulk']['c'] - c_dot = self.state_derivatives['bulk']['c'] + c = self.states['permeate']['c'] + c_dot = self.state_derivatives['permeate']['c'] - V = self.states['bulk']['Volume'] - V_dot = self.state_derivatives['bulk']['Volume'] + V = self.states['permeate']['Volume'] + V_dot = self.state_derivatives['permeate']['Volume'] deltap = self.states['cake']['pressure'] @@ -660,14 +665,53 @@ def compute_residual( # 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, efficency, 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_perm_easy_def(Q_in, V_dot_C, Q_p) + self.residuals['cake']['cakevolume'] = calculate_residual_cake_vol_def( + Q_in, + efficency, + 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() - self.residuals['bulk']['c'] = calculate_residual_concentration_cstr(c, c_dot, V, V_dot, Q_p, Q_out, c_in) - self.residuals['bulk']['Volume'] = calculate_residual_volume_cstr(V, V_dot, Q_p, Q_out) - self.residuals['bulk']['viscosity'] = calculate_residual_visc_cstr() + new_c_in = (1-efficency)*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() @@ -795,4 +839,4 @@ def compute_residual( Residual of the unit operation. """ - raise NotImplementedError() \ No newline at end of file + raise NotImplementedError() 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 diff --git a/tests/test_residual.py b/tests/test_residual.py index 61f3d99..640bd82 100644 --- a/tests/test_residual.py +++ b/tests/test_residual.py @@ -1,13 +1,14 @@ 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 import pytest import numpy as np -""" -q_in q_out, was sind physikalisch sinnvolle szenarien -""" + # random number test TestCaseConc_level1 = { @@ -37,7 +38,7 @@ "expected" : np.array([0,]) } -# flow in and out are equal, but concentrations going into the unit is not +# flow in and out are equal, but concentrations going into the unit is not TestCaseConc_diffcin = { "values" : { "c" : np.array([0.1,]), @@ -51,7 +52,7 @@ "expected" : np.array([-0.1,]) } -#flow in and out are not equal, concentrantions going in are +#flow in and out are not equal, concentrantions going in are TestCaseConc_diffvol = { "values" : { "c" : np.array([0.1,]), @@ -83,8 +84,8 @@ @pytest.mark.parametrize( "parameters", [ - TestCaseConc_level1, - TestCaseConc_equal, + TestCaseConc_level1, + TestCaseConc_equal, TestCaseConc_diffcin, TestCaseConc_diffvol, TestCaseConc_diffvolc @@ -95,7 +96,10 @@ def test_calculation_concentration_cstr(self, parameters): param_vec_conc = parameters["values"].values() - np.testing.assert_array_almost_equal(calculate_residual_concentration_cstr(*param_vec_conc), parameters["expected"]) + np.testing.assert_array_almost_equal( + calculate_residual_concentration_cstr(*param_vec_conc), + parameters["expected"] + ) @@ -172,7 +176,21 @@ def test_calculation_cstr(self, parameters): param_vec_volume = parameters["values"].values() - np.testing.assert_equal(calculate_residual_volume_cstr(*param_vec_volume), parameters["expected"]) + np.testing.assert_equal( + calculate_residual_volume_cstr(*param_vec_volume), + parameters["expected"] + ) + + + + +class TestResidualCakeDEF(): + def test_calculation_def(self, parameters): + + param_vec_volume = parameters["values"].values() + + np.testing.assert_equal(1,1) + TestCaseConcError = { "values" : { @@ -211,3 +229,4 @@ def test_calculation_concentration_cstr_error(self, parameters): with pytest.raises(CADETPythonSimError): calculate_residual_concentration_cstr(*param_vec_volume) + diff --git a/tests/test_unit_operation.py b/tests/test_unit_operation.py index b4fad0f..fc592d3 100644 --- a/tests/test_unit_operation.py +++ b/tests/test_unit_operation.py @@ -378,9 +378,18 @@ def test_initialize(self, unit_operation: UnitOperationBase, expected: dict): '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_residual_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' : { @@ -444,9 +453,12 @@ 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 bf77cc11620ed6f37f36a9f08585c7be59fb5c05 Mon Sep 17 00:00:00 2001 From: daklauss Date: Wed, 31 Jul 2024 17:33:09 +0200 Subject: [PATCH 7/7] Added tests to the residual calculation of DEF CAKEVOLUME and PRESSURE DROP Adjusted tests for unit_operation to the new CPSComponentsystem --- CADETPythonSimulator/residual.py | 14 +-- CADETPythonSimulator/unit_operation.py | 14 ++- tests/test_residual.py | 130 ++++++++++++++++++++++--- tests/test_unit_operation.py | 4 +- 4 files changed, 135 insertions(+), 27 deletions(-) diff --git a/CADETPythonSimulator/residual.py b/CADETPythonSimulator/residual.py index 8fc2318..52c97c8 100644 --- a/CADETPythonSimulator/residual.py +++ b/CADETPythonSimulator/residual.py @@ -79,7 +79,7 @@ def calculate_residual_visc_cstr(): def calculate_residual_cake_vol_def( V_dot_f : float, - eff : np.ndarray, + rejection : np.ndarray, molar_volume : np.ndarray, c_in : np.ndarray, V_dot_C : float @@ -90,16 +90,16 @@ def calculate_residual_cake_vol_def( Parameters ---------- V_dot_f : float - flowrate of incoming feed - eff : float - efficency of the filter + Flowrate of incoming feed + rejection : float + Rejection of the filter gamma : float - portion of suspended material + Portion of suspended material V_dot_C : float - change of Cake Volume + Change of Cake Volume """ - return -V_dot_C + np.sum(eff * molar_volume * c_in * V_dot_f) + return -V_dot_C + np.sum(rejection * molar_volume * c_in * V_dot_f) def calculate_residual_press_easy_def( diff --git a/CADETPythonSimulator/unit_operation.py b/CADETPythonSimulator/unit_operation.py index 6013593..1a2a1bd 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 ( @@ -40,7 +41,7 @@ class UnitOperationBase(Structure): """ name = String() - component_system = Typed(ty=ComponentSystem) + component_system = Typed(ty=CPSComponentSystem) _state_structures = [] _parameters = [] @@ -657,17 +658,20 @@ def compute_residual( deltap = self.states['cake']['pressure'] #parameters - efficency = self.parameters['efficency'] - molar_volume = self.parameters['molar_volume'] + 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'] + rejection = np.array( + [self.rejection.get_rejection(mw) for mw in molecular_weights]) + # 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, - efficency, + rejection, molar_volume, c_in, V_dot_C @@ -692,7 +696,7 @@ def compute_residual( self.residuals['cake']['viscosity'] = calculate_residual_visc_def() - new_c_in = (1-efficency)*c_in + new_c_in = (1-rejection)*c_in self.residuals['permeate']['c'] = calculate_residual_concentration_cstr( c, diff --git a/tests/test_residual.py b/tests/test_residual.py index 640bd82..643288d 100644 --- a/tests/test_residual.py +++ b/tests/test_residual.py @@ -11,7 +11,7 @@ # random number test -TestCaseConc_level1 = { +TestCaseCSTRConc_level1 = { "values" : { "c" : np.array([1, 2, 3]), "c_dot" : np.array([4, 5, 6]), @@ -25,7 +25,7 @@ } # flow in and out are equal, concentrations to -TestCaseConc_equal = { +TestCaseCSTRConc_equal = { "values" : { "c" : np.array([0.1,]), "c_dot" : np.array([0,]), @@ -39,7 +39,7 @@ } # flow in and out are equal, but concentrations going into the unit is not -TestCaseConc_diffcin = { +TestCaseCSTRConc_diffcin = { "values" : { "c" : np.array([0.1,]), "c_dot" : np.array([0,]), @@ -53,7 +53,7 @@ } #flow in and out are not equal, concentrantions going in are -TestCaseConc_diffvol = { +TestCaseCSTRConc_diffvol = { "values" : { "c" : np.array([0.1,]), "c_dot" : np.array([0,]), @@ -67,7 +67,7 @@ } #flow in and out are not, equal, concentrations aren't equal too -TestCaseConc_diffvolc = { +TestCaseCSTRConc_diffvolc = { "values" : { "c" : np.array([0.1,]), "c_dot" : np.array([0.2,]), @@ -84,11 +84,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(): @@ -181,15 +181,119 @@ def test_calculation_cstr(self, parameters): 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 +} -class TestResidualCakeDEF(): +# 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_volume = parameters["values"].values() + param_vec_cake_vol = parameters["values"].values() - np.testing.assert_equal(1,1) + 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() + + np.testing.assert_equal(calculate_residual_press_easy_def(*param_vec_pressure), + parameters["expected"] + ) TestCaseConcError = { diff --git a/tests/test_unit_operation.py b/tests/test_unit_operation.py index fc592d3..eaeec36 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, @@ -15,7 +15,7 @@ # %% Unit Operation Fixtures -class TwoComponentFixture(ComponentSystem): +class TwoComponentFixture(CPSComponentSystem): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs)