├── tests ├── __init__.py ├── test_pymoo.py ├── test_deprecation.py ├── test_numerics.py ├── test_cache.py ├── test_transform.py ├── dataStructure │ └── test_nested_dict.py └── test_fractions.py ├── examples ├── __init__.py ├── recycling │ ├── __init__.py │ └── index.md ├── batch_elution │ ├── __init__.py │ ├── optimization_single.md │ ├── optimization_single.py │ ├── optimization_multi.md │ └── optimization_multi.py ├── load_wash_elute │ ├── __init__.py │ ├── index.md │ ├── lwe_concentration.md │ ├── lwe_concentration.py │ ├── lwe_hic.md │ ├── lwe_hic.py │ ├── lwe_flow_rate.md │ └── lwe_flow_rate.py ├── characterize_chromatographic_system │ ├── __init__.py │ ├── system_periphery.md │ ├── system_periphery.py │ ├── index.md │ ├── Yamamoto_method.md │ └── Yamamoto_method.py └── README.md ├── Makefile ├── docs ├── source │ ├── license.md │ ├── _static │ │ └── logo.png │ ├── reference │ │ ├── tools.md │ │ ├── comparison.md │ │ ├── log.md │ │ ├── transform.md │ │ ├── equilibria.md │ │ ├── plotting.md │ │ ├── reference.md │ │ ├── settings.md │ │ ├── simulator.md │ │ ├── smoothing.md │ │ ├── solution.md │ │ ├── modelBuilder.md │ │ ├── optimization.md │ │ ├── performance.md │ │ ├── processModel.md │ │ ├── stationarity.md │ │ ├── dataStructure.md │ │ ├── dynamicEvents.md │ │ ├── fractionation.md │ │ ├── CADETProcessError.md │ │ ├── simulationResults.md │ │ └── index.md │ ├── _templates │ │ └── autosummary │ │ │ ├── method.rst │ │ │ ├── attribute.rst │ │ │ ├── property.rst │ │ │ ├── module.rst │ │ │ └── class.rst │ ├── user_guide │ │ ├── tools │ │ │ ├── figures │ │ │ │ └── compartment_complex.png │ │ │ └── index.md │ │ ├── process_evaluation │ │ │ └── index.md │ │ ├── process_model │ │ │ ├── index.md │ │ │ ├── component_system.md │ │ │ ├── binding_model.md │ │ │ └── reaction.md │ │ ├── optimization │ │ │ ├── variable_normalization.md │ │ │ ├── index.md │ │ │ ├── figures │ │ │ │ ├── no_transform.svg │ │ │ │ ├── single_evaluation_object.svg │ │ │ │ └── callbacks.svg │ │ │ └── parallel_evaluation.md │ │ └── overview.md │ ├── bibliography.md │ ├── release_notes │ │ ├── index.md │ │ ├── v0.7.3.md │ │ ├── v0.10.1.md │ │ ├── v0.7.2.md │ │ ├── v0.7.1.md │ │ ├── v0.11.1.md │ │ ├── v0.9.1.md │ │ ├── v0.7.0.md │ │ ├── template.md │ │ └── v0.8.0.md │ ├── index.md │ └── conf.py └── Makefile ├── environment.yml ├── .github ├── dependabot.yml ├── workflows │ ├── ruff.yml │ ├── publish-to-pypi.yml │ ├── pipeline.yml │ └── coverage.yml ├── PULL_REQUEST_TEMPLATE │ ├── release.md │ └── default.md └── ISSUE_TEMPLATE │ ├── feature_request.yml │ └── bug_report.yml ├── .gitignore ├── CADETProcess ├── tools │ └── __init__.py ├── CADETProcessError.py ├── __init__.py ├── fractionation │ └── __init__.py ├── sysinfo.py ├── hashing.py ├── dynamicEvents │ └── __init__.py ├── comparison │ ├── __init__.py │ ├── peaks.py │ └── shape.py ├── equilibria │ ├── __init__.py │ └── initial_conditions.py ├── modelBuilder │ ├── __init__.py │ └── batchElutionBuilder.py ├── simulator │ └── __init__.py ├── dataStructure │ ├── __init__.py │ ├── encoder.py │ ├── parameter_group.py │ ├── deprecation.py │ └── cache.py ├── metric.py ├── numerics.py ├── processModel │ └── __init__.py ├── optimization │ └── __init__.py └── settings.py ├── CONTRIBUTORS.md ├── .readthedocs.yaml ├── CITATION.bib ├── .pre-commit-config.yaml ├── .zenodo.json └── pyproject.toml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/recycling/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/batch_elution/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/load_wash_elute/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/characterize_chromatographic_system/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | pip install -r requirements.txt 3 | 4 | test: 5 | py.test tests 6 | -------------------------------------------------------------------------------- /docs/source/license.md: -------------------------------------------------------------------------------- 1 | (license)= 2 | # License 3 | 4 | ```{literalinclude} ../../LICENSE 5 | :language: none 6 | ``` 7 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This is a collection of examples and case studies performed using **CADET-Process**. 4 | -------------------------------------------------------------------------------- /docs/source/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fau-advanced-separations/CADET-Process/HEAD/docs/source/_static/logo.png -------------------------------------------------------------------------------- /docs/source/reference/tools.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | ```{eval-rst} 4 | .. automodule:: CADETProcess.tools 5 | :members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/comparison.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.comparison 3 | :no-inherited-members: 4 | :no-special-members: 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/source/reference/log.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.log 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: cadet-process 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python>=3.10.* 6 | - cadet>=5.0.3 7 | - libsqlite<3.49 8 | - pip 9 | -------------------------------------------------------------------------------- /docs/source/reference/transform.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.transform 3 | :members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/method.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | {{ fullname }} 4 | {{ underline }} 5 | 6 | .. currentmodule:: {{ module }} 7 | 8 | .. automethod:: {{ objname }} 9 | -------------------------------------------------------------------------------- /docs/source/reference/equilibria.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.equilibria 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/plotting.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.plotting 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/reference.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.reference 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/settings.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.settings 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/simulator.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.simulator 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/smoothing.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.smoothing 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/solution.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.solution 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/attribute.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | {{ fullname }} 4 | {{ underline }} 5 | 6 | .. currentmodule:: {{ module }} 7 | 8 | .. autoattribute:: {{ objname }} 9 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/property.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | {{ fullname }} 4 | {{ underline }} 5 | 6 | .. currentmodule:: {{ module }} 7 | 8 | .. autoproperty:: {{ objname }} 9 | -------------------------------------------------------------------------------- /docs/source/reference/modelBuilder.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.modelBuilder 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/optimization.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.optimization 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/performance.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.performance 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/processModel.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.processModel 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/stationarity.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.stationarity 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/dataStructure.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.dataStructure 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/dynamicEvents.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.dynamicEvents 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/fractionation.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.fractionation 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/CADETProcessError.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.CADETProcessError 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/reference/simulationResults.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. automodule:: CADETProcess.simulationResults 3 | :no-members: 4 | :no-inherited-members: 5 | :no-special-members: 6 | ``` 7 | -------------------------------------------------------------------------------- /docs/source/user_guide/tools/figures/compartment_complex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fau-advanced-separations/CADET-Process/HEAD/docs/source/user_guide/tools/figures/compartment_complex.png -------------------------------------------------------------------------------- /docs/source/bibliography.md: -------------------------------------------------------------------------------- 1 | ```{eval-rst} 2 | .. only:: html 3 | 4 | Bibliography 5 | ============ 6 | 7 | ``` 8 | 9 | ```{bibliography} ./references.bib 10 | :style: unsrt 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | .pytest_cache/ 4 | .ipynb_checkpoints 5 | docs/source/reference/generated/* 6 | docs/build/* 7 | docs/jupyter_execute/* 8 | docs/source/reference/generated/* 9 | docs/source/examples/ 10 | debug* 11 | *.h5 12 | log/ 13 | diskcache_* 14 | results_* 15 | *.egg-info 16 | .vscode 17 | -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | name: Ruff 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - dev 7 | pull_request: 8 | jobs: 9 | ruff: 10 | runs-on: ubuntu-latest 11 | continue-on-error: true 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: astral-sh/ruff-action@v3 15 | with: 16 | args: check --output-format=github 17 | -------------------------------------------------------------------------------- /docs/source/release_notes/index.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | This is the list of changes to **CADET-Process**. For full details, see the [commit logs](https://github.com/fau-advanced-separations/CADET-Process/). 4 | 5 | ```{toctree} 6 | :maxdepth: 1 7 | 8 | v0.7.0 9 | v0.7.1 10 | v0.7.2 11 | v0.7.3 12 | v0.8.0 13 | v0.9.0 14 | v0.9.1 15 | v0.10.0 16 | v0.10.1 17 | v0.11.0 18 | v0.11.1 19 | ``` 20 | -------------------------------------------------------------------------------- /CADETProcess/tools/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================================= 3 | Tools (:mod:`CADETProcess.tools`) 4 | ================================= 5 | 6 | .. currentmodule:: CADETProcess.tools 7 | 8 | A collection of tools extending the functionatlity of **CADET-Process**. 9 | 10 | Yamamoto's Method 11 | ================= 12 | 13 | A module to fit isotherm parameters using Yamamoto's method. 14 | 15 | .. autosummary:: 16 | :toctree: generated/ 17 | 18 | yamamoto 19 | 20 | """ 21 | -------------------------------------------------------------------------------- /examples/characterize_chromatographic_system/system_periphery.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | formats: md:myst,py:percent 4 | text_representation: 5 | extension: .md 6 | format_name: myst 7 | format_version: 0.13 8 | jupytext_version: 1.17.1 9 | kernelspec: 10 | display_name: Python 3 11 | language: python 12 | name: python3 13 | --- 14 | 15 | # Model System Periphery 16 | 17 | Show Foto 18 | 19 | Show Abstraction 20 | 21 | Show Model 22 | 23 | 24 | Fit Parameters 25 | 26 | Missing: 27 | - Data 28 | - Fotos 29 | -------------------------------------------------------------------------------- /CADETProcess/CADETProcessError.py: -------------------------------------------------------------------------------- 1 | """ 2 | ========================================================= 3 | CADETProcessError (:mod:`CADETProcess.CADETProcessError`) 4 | ========================================================= 5 | 6 | .. currentmodule:: CADETProcess.CADETProcessError 7 | 8 | Exceptions in **CADET-Process**. 9 | 10 | .. autosummary:: 11 | :toctree: generated/ 12 | 13 | CADETProcessError 14 | 15 | """ # noqa 16 | 17 | 18 | class CADETProcessError(Exception): 19 | """Exception for errors in CADET-Process.""" 20 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | ## Special thanks to everyone who has helped with this project 4 | 5 | * [Ronald Jäpel](https://github.com/ronald-jaepel) 6 | * [Florian Schnunk](https://github.com/flo-schu) 7 | 8 | ## Funding Acknowledgement 9 | 10 | Johannes Schmölder has received support from the IMI2/ EU/EFPIA joint undertaking Inno4Vac (grant no. 101007799). 11 | 12 | ## I would like to join this list. How can I help the project? 13 | 14 | For more information, please refer to our [CONTRIBUTING](CONTRIBUTING.md) guide. 15 | -------------------------------------------------------------------------------- /CADETProcess/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | CADET-Process 3 | ======== 4 | 5 | CADET-Process is a Python package for modelling, simulating and optimizing 6 | advanced chromatographic systems. It serves as an inteface for CADET, but also 7 | for other solvers. 8 | 9 | See https://cadet-process.readthedocs.io for complete documentation. 10 | """ 11 | 12 | # Version information 13 | name = "CADET-Process" 14 | __version__ = "0.11.1" 15 | 16 | # Imports 17 | from .CADETProcessError import * 18 | 19 | from .settings import Settings 20 | 21 | settings = Settings() 22 | -------------------------------------------------------------------------------- /CADETProcess/fractionation/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================================================= 3 | Fractionation (:mod:`CADETProcess.fractionation`) 4 | ================================================= 5 | 6 | .. currentmodule:: CADETProcess.fractionation 7 | 8 | A module to calculate process performance indicators. 9 | 10 | .. autosummary:: 11 | :toctree: generated/ 12 | 13 | Fraction 14 | FractionPool 15 | Fractionator 16 | FractionationOptimizer 17 | 18 | """ 19 | 20 | from .fractions import * 21 | from .fractionator import * 22 | from .fractionationOptimizer import * 23 | -------------------------------------------------------------------------------- /docs/source/release_notes/v0.7.3.md: -------------------------------------------------------------------------------- 1 | # v0.7.3 2 | 3 | **CADET-Process** v0.7.3 is a hotfix release which fixes a minor issue with the optimization module. 4 | All users are encouraged to upgrade to this release. 5 | 6 | This release requires Python 3.8+ 7 | 8 | ## Highlights of this release 9 | - Properly read constraints and constraints violation in pymoo. 10 | - Use `meta_front` if `n_multi_criteria_decision_functions` > 0. 11 | - Only run CI "on push" for dev and master branch. 12 | 13 | ## Pull requests for 0.7.3 14 | - [28](https://github.com/fau-advanced-separations/CADET-Process/pull/28): Fix/pymoo cv 15 | -------------------------------------------------------------------------------- /docs/source/release_notes/v0.10.1.md: -------------------------------------------------------------------------------- 1 | # v0.10.1 2 | 3 | **CADET-Process** v0.10.1 is a hotfix release which fixes an upstream dependency issue by pinning a lower version of the package libsqlite. 4 | This release fixes issues arising during the setup of new environments 5 | 6 | This release requires Python 3.10+. 7 | 8 | ## Issues closed for 0.10.1 9 | 10 | - [229](https://github.com/fau-advanced-separations/CADET-Process/issues/229): Fix problem with libsqlite `3.49.1` 11 | 12 | --- 13 | 14 | **Full Changelog**: [Compare v0.10.0 to v0.10.1](https://github.com/fau-advanced-separations/CADET-Process/compare/v0.10.0...v0.10.1) 15 | -------------------------------------------------------------------------------- /CADETProcess/sysinfo.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | import psutil 4 | 5 | uname = platform.uname() 6 | cpu_freq = psutil.cpu_freq() 7 | memory = psutil.virtual_memory() 8 | memory_total = psutil._common.bytes2human(memory.total) 9 | 10 | system_information = { 11 | "system": uname.system, 12 | "release": uname.release, 13 | "machine": uname.machine, 14 | "processor": uname.processor, 15 | "n_cores": psutil.cpu_count(logical=True), 16 | "n_cores_physical": psutil.cpu_count(logical=False), 17 | "max_frequency": cpu_freq.max, 18 | "min_frequency": cpu_freq.min, 19 | "memory_total": memory_total, 20 | } 21 | -------------------------------------------------------------------------------- /examples/characterize_chromatographic_system/system_periphery.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: md:myst,py:percent 5 | # text_representation: 6 | # extension: .py 7 | # format_name: percent 8 | # format_version: '1.3' 9 | # jupytext_version: 1.17.1 10 | # kernelspec: 11 | # display_name: Python 3 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # %% [markdown] 17 | # # Model System Periphery 18 | # 19 | # Show Foto 20 | # 21 | # Show Abstraction 22 | # 23 | # Show Model 24 | # 25 | # 26 | # Fit Parameters 27 | # 28 | # Missing: 29 | # - Data 30 | # - Fotos 31 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-lts-latest 11 | tools: 12 | python: "mambaforge-latest" 13 | jobs: 14 | install: 15 | - pip install .[all] --group docs 16 | 17 | # Build documentation in the docs/ directory with Sphinx 18 | sphinx: 19 | configuration: docs/source/conf.py 20 | 21 | # Optionally declare the Python requirements required to build your docs 22 | conda: 23 | environment: environment.yml 24 | -------------------------------------------------------------------------------- /CADETProcess/hashing.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from typing import Any 3 | 4 | 5 | def digest_string(obj: Any, length: int = 8) -> str: 6 | """ 7 | Compute a short digest of a string. 8 | 9 | Parameters 10 | ---------- 11 | obj : Any 12 | Input to digest. Will be converted to string. 13 | length : int, optional 14 | Length of the returned digest (default is 8). 15 | 16 | Returns 17 | ------- 18 | digest : str 19 | Hexadecimal string digest of the input. 20 | 21 | Examples 22 | -------- 23 | >>> digest_string("hello world") 24 | 'b94d27b9' 25 | """ 26 | s = str(obj) 27 | return hashlib.sha256(s.encode()).hexdigest()[:length] 28 | -------------------------------------------------------------------------------- /docs/source/release_notes/v0.7.2.md: -------------------------------------------------------------------------------- 1 | # v0.7.2 2 | 3 | **CADET-Process** v0.7.2 is a hotfix release which fixes a minor issue with the optimization module. 4 | All users are encouraged to upgrade to this release. 5 | 6 | This release requires Python 3.8+ 7 | 8 | ## Highlights of this release 9 | - Fix issue where unknown (internal) optimizer options raise exception in `scipy.minimize(method='trust_constr')` (See also post in [forum](https://forum.cadet-web.de/t/trustconstr-optimizer-scipy-in-cadet-process/689/4)). 10 | - Include bound objects in `scipy.minimice` function call. 11 | 12 | ## Pull requests for 0.7.2 13 | - [24](https://github.com/fau-advanced-separations/CADET-Process/pull/24): Fix/trust constr. 14 | -------------------------------------------------------------------------------- /CITATION.bib: -------------------------------------------------------------------------------- 1 | % As an open-source project, CADET-Process relies on the support and recognition from users and researchers to thrive. 2 | % Therefore, we kindly ask that any publications or projects leveraging the capabilities of CADET-Process acknowledge its creators and their contributions by citing an adequate selection of our publications. 3 | 4 | @Article{Schmoelder2020, 5 | author = {Schmölder, Johannes and Kaspereit, Malte}, 6 | title = {A {{Modular Framework}} for the {{Modelling}} and {{Optimization}} of {{Advanced Chromatographic Processes}}}, 7 | doi = {10.3390/pr8010065}, 8 | number = {1}, 9 | pages = {65}, 10 | volume = {8}, 11 | journal = {Processes}, 12 | year = {2020}, 13 | } 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/release.md: -------------------------------------------------------------------------------- 1 | ## Workflow to release a new version `vX.Y.Z` 2 | 3 | - [ ] Create new branch `vX.Y.Z` from `dev` 4 | - [ ] Bump version in `CADETProcess/__init__.py` and `.zenodo.json` 5 | - [ ] Add release notes 6 | - [ ] General description 7 | - [ ] Deprecations / other changes 8 | - [ ] Closed Issues/PRs 9 | - [ ] Add entry in `index.md` 10 | - [ ] Commit with message `vX.Y.Z` 11 | - [ ] Add tag (`git tag 'vX.Y.Z'`) 12 | - [ ] Push and open PR (base onto `master`). Also push tag: `git push origin --tag` 13 | - [ ] When ready, rebase again onto `dev` (in case changes were made) 14 | - [ ] Merge into master 15 | - [ ] Make release on GitHub using tag and release notes. 16 | - [ ] Check that workflows automatically publish to PyPI and readthedocs 17 | -------------------------------------------------------------------------------- /CADETProcess/dynamicEvents/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================================================== 3 | Dynamic Events (:mod:`CADETProcess.dynamicEvents`) 4 | ================================================== 5 | 6 | .. currentmodule:: CADETProcess.dynamicEvents 7 | 8 | This module provides functionality for dynamic changes of parameters in CADET-Process. 9 | 10 | Sections and Timelines 11 | ====================== 12 | 13 | .. autosummary:: 14 | :toctree: generated/ 15 | 16 | Section 17 | TimeLine 18 | MultiTimeLine 19 | 20 | Events and EventHandlers 21 | ======================== 22 | 23 | .. autosummary:: 24 | :toctree: generated/ 25 | 26 | Event 27 | Duration 28 | EventHandler 29 | 30 | """ 31 | 32 | from .section import * 33 | from .event import * 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/default.md: -------------------------------------------------------------------------------- 1 | ## Your Checklist for This Pull Request 2 | 3 | 🚨 Please review the [guidelines for contributing](../CONTRIBUTING.md) to this repository. 4 | 5 | - [ ] Ensure you are requesting to **pull a topic/feature/bugfix branch** (right side). Do not request from your main branch! 6 | - [ ] Ensure you are making a pull request against the `dev` branch. Your branch should be based off our `dev` branch. 7 | - [ ] Verify that the commit message(s) match our requested structure. 8 | - [ ] Confirm that your code additions will pass both code linting checks and unit tests. 9 | 10 | ## Description 11 | 12 | Please provide a detailed description of your pull request. Include the purpose of the changes, any issues addressed, and other relevant information that will help reviewers understand your modifications. 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | 3 | # pre-commit-hooks 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.1.0 6 | hooks: 7 | - id: check-merge-conflict 8 | - id: check-yaml 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | # Jupytext 12 | - repo: https://github.com/mwouts/jupytext 13 | rev: v1.14.5 14 | hooks: 15 | - id: jupytext 16 | files: 'examples/[^/]+/' 17 | types_or: [markdown, python] 18 | exclude: | 19 | (?x)^( 20 | README.md| 21 | examples/[^/]+/index.md| 22 | examples/[^/]+/index.py 23 | )$ 24 | args: [--sync] 25 | # ruff 26 | repos: 27 | - repo: https://github.com/astral-sh/ruff-pre-commit 28 | # Ruff version. 29 | rev: v0.11.13 30 | hooks: 31 | # Run the linter. 32 | - id: ruff-check 33 | args: [ --fix ] 34 | -------------------------------------------------------------------------------- /CADETProcess/comparison/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | =========================================== 3 | Comparison (:mod:`CADETProcess.comparison`) 4 | =========================================== 5 | 6 | .. currentmodule:: CADETProcess.comparison 7 | 8 | Classes for comparing simulation results with a reference. 9 | 10 | Difference Metrics 11 | ================== 12 | 13 | .. autosummary:: 14 | :toctree: generated/ 15 | 16 | DifferenceBase 17 | SSE 18 | RMSE 19 | NRMSE 20 | Norm 21 | L1 22 | L2 23 | AbsoluteArea 24 | RelativeArea 25 | Shape 26 | PeakHeight 27 | PeakPosition 28 | BreakthroughHeight 29 | BreakthroughPosition 30 | 31 | Comparator 32 | ========== 33 | 34 | .. autosummary:: 35 | :toctree: generated/ 36 | 37 | Comparator 38 | 39 | """ 40 | 41 | from .difference import * 42 | from .comparator import * 43 | -------------------------------------------------------------------------------- /docs/source/release_notes/v0.7.1.md: -------------------------------------------------------------------------------- 1 | # v0.7.1 2 | 3 | **CADET-Process** v0.7.1 is a hotfix release which fixes a couple of minor issues. 4 | All users are encouraged to upgrade to this release. 5 | 6 | This release requires Python 3.8+ 7 | 8 | ## Highlights of this release 9 | - Fix `check_connection` for `FlowSheet` with single `Cstr` (see [22](https://github.com/fau-advanced-separations/CADET-Process/issues/22)) 10 | - Include missing isotherm models in `__all__` (see [here](https://forum.cadet-web.de/t/change-in-mobilephasemodulator-isotherm/671)) 11 | - Fix time coordinates in plot function. 12 | 13 | ## Issues closed for 0.7.1 14 | - [22](https://github.com/fau-advanced-separations/CADET-Process/issues/22): FlowSheet.check_connection() returns False for single Cstr 15 | 16 | ## Pull requests for 0.7.1 17 | - [23](https://github.com/fau-advanced-separations/CADET-Process/pull/23): Fix FlowSheet.check_connections() for Cstr #23 18 | -------------------------------------------------------------------------------- /docs/source/release_notes/v0.11.1.md: -------------------------------------------------------------------------------- 1 | # v0.11.1 2 | 3 | **CADET-Process** v0.11.1 is a hotfix release which fixes a couple of small issues. 4 | We strongly encourage all users to upgrade to this version for better performance and new functionalities. 5 | 6 | This release requires Python 3.10+. 7 | 8 | ## Issues closed for 0.11.1 9 | 10 | - [#281](https://github.com/fau-advanced-separations/CADET-Process/pull/281): Fix n_bound_states in Spreading model by @schmoelder 11 | - [#282](https://github.com/fau-advanced-separations/CADET-Process/pull/282): Update Zenodo.json by @hannahlanzrath 12 | - [#284](https://github.com/fau-advanced-separations/CADET-Process/pull/284): Fix overlay plot by @schmoelder 13 | - [#287](https://github.com/fau-advanced-separations/CADET-Process/pull/287): Do not change labels when using log scale by @schmoelder 14 | 15 | --- 16 | 17 | **Full Changelog**: [Compare v0.11.0 to v0.11.1](https://github.com/fau-advanced-separations/CADET-Process/compare/v0.11.0...v0.11.1) 18 | -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | ```{include} ../../README.md 2 | ``` 3 | 4 | ```{toctree} 5 | :maxdepth: 2 6 | :caption: User guide 7 | :hidden: 8 | 9 | user_guide/overview 10 | user_guide/process_model/index 11 | user_guide/simulator 12 | user_guide/process_evaluation/index 13 | user_guide/optimization/index 14 | user_guide/tools/index 15 | ``` 16 | 17 | ```{toctree} 18 | :maxdepth: 2 19 | :caption: Case Studies 20 | :hidden: 21 | 22 | examples/batch_elution/process 23 | examples/load_wash_elute/index 24 | examples/recycling/index 25 | examples/characterize_chromatographic_system/column_transport_parameters 26 | examples/characterize_chromatographic_system/binding_model_parameters 27 | examples/characterize_chromatographic_system/Yamamoto_method 28 | ``` 29 | 30 | ```{toctree} 31 | :maxdepth: 2 32 | :caption: API Reference 33 | :hidden: 34 | 35 | reference/index 36 | ``` 37 | 38 | ```{toctree} 39 | :maxdepth: 2 40 | :caption: References 41 | :hidden: 42 | 43 | license 44 | release_notes/index 45 | bibliography 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/source/user_guide/process_evaluation/index.md: -------------------------------------------------------------------------------- 1 | (process_evaluation_guide)= 2 | # Process Evaluation 3 | **CADET-Process** offers multiple tools for evaluating simulation results. 4 | 5 | To quantify the performance of a chromatographic process, performance indicators such as purity or recovery yield must be calculated from the chromatograms. 6 | The {mod}`~CADETProcess.fractionation` module provides a {class}`~CADETProcess.fractionation.Fractionator` class for this purpose. 7 | It enables the automatic determination of optimal fractionation times and the calculation of various performance indicators. 8 | 9 | Furthermore, the {mod}`~CADETProcess.comparison` module allows for quantitative comparison of simulation results with experimental data or reference simulations. 10 | This module provides various metrics for quantifying differences between datasets and calculating residuals, which is crucial for parameter estimation. 11 | 12 | ```{toctree} 13 | :maxdepth: 2 14 | 15 | fractionation 16 | comparison 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/source/user_guide/tools/index.md: -------------------------------------------------------------------------------- 1 | (tools_guide)= 2 | # Tools 3 | 4 | **CADET-Process** provides various tools that can be used for pre- and postprocessing. 5 | 6 | ## Model Builder 7 | 8 | The {mod}`~CADETProcess.modelBuilder` module offers classes that simplify the setup of compartment models used in bioreactors and carousel/SMB systems, where multiple columns are used, and their positions are changed dynamically during operation. 9 | 10 | ```{toctree} 11 | :maxdepth: 2 12 | 13 | carousel_builder 14 | smb_design 15 | compartment_builder 16 | ``` 17 | 18 | ## Buffer equlibria and pH 19 | 20 | The {mod}`~CADETProcess.equilibria` module contains methods to calculate buffer capacity and simulate pH using deprotonation reactions. 21 | These tools are essential for the optimization of bioprocesses and the design of chromatographic separation processes, where accurate pH control is critical for efficient separation. 22 | 23 | ```{toctree} 24 | :maxdepth: 2 25 | 26 | deprotonation_reactions 27 | buffer_capacity 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/recycling/index.md: -------------------------------------------------------------------------------- 1 | (recycling_example)= 2 | # Recycling Techniques 3 | 4 | The design of preparative chromatography poses major challenges in balancing the interdependence of product purity and yield, as well as the efficiency of the column and its pressure drop. 5 | To improve difficult separations, it may be necessary to increase the column's separation efficiency by using a longer column or reducing the particle size of the stationary phase. 6 | However, using a longer column or a particles can result in an excessively high pressure drop. 7 | To overcome this issue, several recycling techniques exist to effectively improve the separation. 8 | By connecting the column's outlet to its inlet, the separation can pass through the column multiple times, without the need for a longer column or smaller particles, and hence, with a manageable pressure drop. 9 | 10 | In the following different recycling concepts are introduced. 11 | 12 | ```{toctree} 13 | :maxdepth: 1 14 | 15 | clr_process 16 | clr_optimization 17 | mrssr_process 18 | mrssr_optimization 19 | ``` 20 | -------------------------------------------------------------------------------- /CADETProcess/equilibria/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | =========================================== 3 | Equilibria (:mod:`CADETProcess.equilibria`) 4 | =========================================== 5 | 6 | .. currentmodule:: CADETProcess.equilibria 7 | 8 | A collection of tools to calculate reaction and binding equilibria. 9 | 10 | Buffer Capacity 11 | =============== 12 | 13 | Calculate buffer capacity 14 | 15 | .. autosummary:: 16 | :toctree: generated/ 17 | 18 | buffer_capacity 19 | 20 | Reaction Equilibria 21 | =================== 22 | 23 | Calculate consistent initial conditions of reaction systems. 24 | 25 | .. autosummary:: 26 | :toctree: generated/ 27 | 28 | reaction_equilibria 29 | 30 | Initial Conditions 31 | ================== 32 | 33 | Calculate consistent initial conditions of reaction/binding system. 34 | 35 | .. autosummary:: 36 | :toctree: generated/ 37 | 38 | initial_conditions 39 | 40 | """ 41 | 42 | from .ptc import * 43 | from .reaction_equilibria import * 44 | from .initial_conditions import * 45 | from .buffer_capacity import * 46 | -------------------------------------------------------------------------------- /CADETProcess/modelBuilder/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | =============================================== 3 | ModelBuilder (:mod:`CADETProcess.modelBuilder`) 4 | =============================================== 5 | 6 | .. currentmodule:: CADETProcess.modelBuilder 7 | 8 | The ``modelBuilder`` module provides functionality for setting up complex ``Process`` 9 | models 10 | 11 | CarouselBuilder 12 | =============== 13 | 14 | A module for building carousel systems like SMB, MCSGP, etc. 15 | 16 | .. autosummary:: 17 | :toctree: generated/ 18 | 19 | SerialZone 20 | ParallelZone 21 | CarouselBuilder 22 | 23 | 24 | CompartmentBuilder 25 | ================== 26 | 27 | A module for building compartment model systems. 28 | 29 | .. autosummary:: 30 | :toctree: generated/ 31 | 32 | CompartmentBuilder 33 | 34 | """ 35 | 36 | from . import carouselBuilder 37 | from .carouselBuilder import * 38 | from .compartmentBuilder import * 39 | 40 | from .batchElutionBuilder import BatchElution 41 | from .clrBuilder import CLR 42 | from .flipFlopBuilder import FlipFlop 43 | from .lweBuilder import LWE 44 | from .mrssrBuilder import MRSSR 45 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish CADET-Process to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release-build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.x" 18 | 19 | - name: Build release distributions 20 | run: | 21 | python -m pip install --upgrade pip build 22 | python -m build 23 | 24 | - name: Upload release distributions 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: release-dists 28 | path: dist/ 29 | 30 | pypi-publish: 31 | runs-on: ubuntu-latest 32 | needs: release-build 33 | permissions: 34 | id-token: write 35 | 36 | steps: 37 | - name: Download release distributions 38 | uses: actions/download-artifact@v4 39 | with: 40 | name: release-dists 41 | path: dist/ 42 | 43 | - name: Publish to PyPI 44 | uses: pypa/gh-action-pypi-publish@release/v1 45 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = CADET-Process 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile html clean 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | html: 23 | mkdir -p "$(SOURCEDIR)"/examples/parameter_estimation/reference_data 24 | cp -r ../examples/parameter_estimation/reference_data "$(SOURCEDIR)"/examples/parameter_estimation/ 25 | mkdir -p "$(SOURCEDIR)"/examples/parameter_estimation/reference_simulation 26 | cp -r ../examples/parameter_estimation/reference_simulation "$(SOURCEDIR)"/examples/parameter_estimation/ 27 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 28 | 29 | -------------------------------------------------------------------------------- /CADETProcess/simulator/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ========================================= 3 | Simulator (:mod:`CADETProcess.simulator`) 4 | ========================================= 5 | 6 | .. currentmodule:: CADETProcess.simulator 7 | 8 | The ``simulator`` module provides functionality for simulating a ``Process``. 9 | 10 | Simulator 11 | ========= 12 | 13 | .. autosummary:: 14 | :toctree: generated/ 15 | 16 | SimulatorBase 17 | 18 | CADET 19 | ----- 20 | 21 | .. autosummary:: 22 | :toctree: generated/ 23 | 24 | Cadet 25 | 26 | Futher settings: 27 | 28 | .. autosummary:: 29 | :toctree: generated/ 30 | 31 | ModelSolverParameters 32 | UnitParameters 33 | AdsorptionParameters 34 | ReactionParameters 35 | SolverParameters 36 | SolverTimeIntegratorParameters 37 | ReturnParameters 38 | SensitivityParameters 39 | 40 | 41 | SimulationResults 42 | ================= 43 | 44 | After simulation, the ``Simulator`` returns a ``SimulationResults`` object. 45 | 46 | .. currentmodule:: CADETProcess.simulationResults 47 | 48 | .. autosummary:: 49 | :toctree: generated/ 50 | 51 | SimulationResults 52 | 53 | """ 54 | 55 | from .simulator import SimulatorBase 56 | from .cadetAdapter import Cadet 57 | -------------------------------------------------------------------------------- /CADETProcess/dataStructure/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | =================================================== 3 | Data Structures (:mod:`CADETProcess.dataStructure`) 4 | =================================================== 5 | 6 | .. currentmodule:: CADETProcess.dataStructure 7 | 8 | This module provides datastructures to simplify defining setters and getters for 9 | (model) parameters. 10 | It is mostly based on the data model introduced in [1]_ 11 | 12 | Notes 13 | ----- 14 | 15 | At some point it might be considered to switch to attrs 16 | (see `#15 `_). 17 | 18 | References 19 | ---------- 20 | .. [1] Jones, B. K., Beazley, D. (2013). 21 | Python Cookbook: Recipes for Mastering Python 3. United States: O'Reilly Media. 22 | 23 | 24 | .. autosummary:: 25 | :toctree: generated/ 26 | 27 | dataStructure 28 | aggregator 29 | parameter 30 | parameter_group 31 | cache 32 | diskcache 33 | nested_dict 34 | 35 | """ 36 | 37 | from .dataStructure import * 38 | from .aggregator import * 39 | from .parameter import * 40 | from .parameter_group import * 41 | from .cache import * 42 | from .diskcache import * 43 | from .nested_dict import * 44 | from .deprecation import * 45 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {{ fullname }} 2 | {{ underline }} 3 | 4 | .. currentmodule:: {{ module }} 5 | 6 | .. automodule:: {{ fullname }} 7 | 8 | {% block attributes %} 9 | {% if attributes %} 10 | .. rubric:: {{ _('Module Attributes') }} 11 | 12 | .. autosummary:: 13 | :toctree: 14 | {% for item in attributes %} 15 | {{ item }} 16 | {%- endfor %} 17 | {% endif %} 18 | {% endblock %} 19 | 20 | {% block functions %} 21 | {% if functions %} 22 | .. rubric:: {{ _('Functions') }} 23 | 24 | .. autosummary:: 25 | :toctree: 26 | {% for item in functions %} 27 | {{ item }} 28 | {%- endfor %} 29 | {% endif %} 30 | {% endblock %} 31 | 32 | {% block classes %} 33 | {% if classes %} 34 | .. rubric:: {{ _('Classes') }} 35 | 36 | .. autosummary:: 37 | :toctree: 38 | {% for item in classes %} 39 | {{ item }} 40 | {%- endfor %} 41 | {% endif %} 42 | {% endblock %} 43 | 44 | {% block exceptions %} 45 | {% if exceptions %} 46 | .. rubric:: {{ _('Exceptions') }} 47 | 48 | .. autosummary:: 49 | :toctree: 50 | {% for item in exceptions %} 51 | {{ item }} 52 | {%- endfor %} 53 | {% endif %} 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /examples/characterize_chromatographic_system/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | formats: md:myst,py:percent 4 | text_representation: 5 | extension: .md 6 | format_name: myst 7 | format_version: 0.13 8 | jupytext_version: 1.14.5 9 | kernelspec: 10 | display_name: Python 3 11 | language: python 12 | name: python3 13 | --- 14 | 15 | (chrom_system_characterization)= 16 | # Characterization of Chromatographic System: Modeling Periphery, Transport, and Binding Parameters 17 | 18 | To accurately model a chromatographic system in **CADET-Process**, several steps are necessary. 19 | First, the system periphery, including components such as tubing and valves, must be characterized to account for any dead volumes that may cause time shifts in the signal, as well as dispersion that can lead to peak broadening 20 | Next, column parameters such as porosities and axial dispersion need to be determined. 21 | Finally, the binding model parameters must be characterized, as they determine the separation of components. 22 | 23 | In the following sections, the steps required to model a chromatographic system in **CADET-Process** are outlined, including how to fit the model to experimental data. 24 | 25 | ```{toctree} 26 | :maxdepth: 1 27 | 28 | system_periphery 29 | column_transport_parameters 30 | particle_porosity 31 | binding_model_parameters 32 | ``` 33 | -------------------------------------------------------------------------------- /CADETProcess/dataStructure/encoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any 3 | 4 | import numpy as np 5 | 6 | __all__ = ["NumpyArrayEncoder"] 7 | 8 | 9 | class NumpyArrayEncoder(json.JSONEncoder): 10 | """ 11 | Custom JSON encoder for NumPy data types. 12 | 13 | This encoder extends `json.JSONEncoder` to support serialization of NumPy integers, 14 | floating-point numbers, and arrays. It converts NumPy data types to native Python 15 | data types that can be serialized to JSON. 16 | """ 17 | 18 | def default(self, obj: Any) -> Any: 19 | """ 20 | Convert NumPy data types to native Python data types for JSON serialization. 21 | 22 | Parameters 23 | ---------- 24 | obj : Any 25 | The object to serialize. 26 | 27 | Returns 28 | ------- 29 | Any 30 | The object converted to a JSON-serializable type. 31 | 32 | Raises 33 | ------ 34 | TypeError 35 | If the object is not of a supported type. 36 | """ 37 | if isinstance(obj, np.integer): 38 | return int(obj) 39 | elif isinstance(obj, np.floating): 40 | return float(obj) 41 | elif isinstance(obj, np.ndarray): 42 | return obj.tolist() 43 | else: 44 | return super(NumpyArrayEncoder, self).default(obj) 45 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ fullname }} 2 | {{ underline }} 3 | 4 | .. currentmodule:: {{ module }} 5 | 6 | .. autoclass:: {{ objname }} 7 | :no-members: 8 | :no-inherited-members: 9 | :no-special-members: 10 | 11 | {% block methods %} 12 | .. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages. 13 | .. autosummary:: 14 | :toctree: 15 | {% for item in all_methods %} 16 | {%- if not item.startswith('_') or item in ['__call__', '__mul__', '__getitem__', '__len__'] %} 17 | {{ name }}.{{ item }} 18 | {%- endif -%} 19 | {%- endfor %} 20 | {% for item in inherited_members %} 21 | {%- if item in ['__call__', '__mul__', '__getitem__', '__len__'] %} 22 | {{ name }}.{{ item }} 23 | {%- endif -%} 24 | {%- endfor %} 25 | {% endblock %} 26 | 27 | {% block attributes %} 28 | {% if attributes %} 29 | .. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages. 30 | .. autosummary:: 31 | :toctree: 32 | {% for item in all_attributes %} 33 | {%- if not item.startswith('_') %} 34 | {{ name }}.{{ item }} 35 | {%- endif -%} 36 | {%- endfor %} 37 | {% endif %} 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /docs/source/reference/index.md: -------------------------------------------------------------------------------- 1 | # CADET-Process API 2 | 3 | Listed below are all of the modules in **CADET-Process**. 4 | While we try our best to keep everything stable, changes in the API can occur at any point. 5 | 6 | - {mod}`CADETProcess.CADETProcessError` 7 | - {mod}`CADETProcess.settings` 8 | - {mod}`CADETProcess.log` 9 | - {mod}`CADETProcess.dataStructure` 10 | - {mod}`CADETProcess.dynamicEvents` 11 | - {mod}`CADETProcess.processModel` 12 | - {mod}`CADETProcess.solution` 13 | - {mod}`CADETProcess.reference` 14 | - {mod}`CADETProcess.simulationResults` 15 | - {mod}`CADETProcess.simulator` 16 | - {mod}`CADETProcess.comparison` 17 | - {mod}`CADETProcess.optimization` 18 | - {mod}`CADETProcess.fractionation` 19 | - {mod}`CADETProcess.stationarity` 20 | - {mod}`CADETProcess.performance` 21 | - {mod}`CADETProcess.plotting` 22 | - {mod}`CADETProcess.smoothing` 23 | - {mod}`CADETProcess.transform` 24 | - {mod}`CADETProcess.modelBuilder` 25 | - {mod}`CADETProcess.equilibria` 26 | - {mod}`CADETProcess.tools` 27 | 28 | 29 | ```{toctree} 30 | :maxdepth: 1 31 | :hidden: 32 | 33 | CADETProcessError 34 | settings 35 | log 36 | dataStructure 37 | dynamicEvents 38 | processModel 39 | solution 40 | reference 41 | simulationResults 42 | simulator 43 | comparison 44 | optimization 45 | fractionation 46 | stationarity 47 | performance 48 | plotting 49 | smoothing 50 | transform 51 | modelBuilder 52 | equilibria 53 | tools 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/source/release_notes/v0.9.1.md: -------------------------------------------------------------------------------- 1 | # v0.9.1 2 | 3 | **CADET-Process** v0.9.1 is a hotfix release which fixes a couple of minor issues. 4 | All users are encouraged to upgrade to this release, as there are a large number of bug-fixes and optimizations. 5 | 6 | This release requires Python 3.10+ 7 | 8 | ## Highlights and new features of this release 9 | 10 | - Fix updating {class}`~CADETProcess.optimization.ParetoFront` with a from a {class}`~CADETProcess.optimization.Population`. 11 | - Add option to instantiate a {class}`~CADETProcess.optimization.Population` from an {class}`~CADETProcess.optimization.OptimizationProblem`. 12 | - Add option to include meta scores and infeasible points in {meth}`~CADETProcess.optimization.Population.plot_pareto`. 13 | - Add option to set time axis in plots to seconds. 14 | - Migrate to `pyproject.toml`. 15 | 16 | ## Issues closed for 0.9.1 17 | 18 | - [121](https://github.com/fau-advanced-separations/CADET-Process/issues/121): optimization_problem.evaluate_callbacks(ind) doesn't evaluate if callback frequency is set to != 1 19 | 20 | ## Pull requests for 0.9.1 21 | 22 | - [122](https://github.com/fau-advanced-separations/CADET-Process/pull/122): Extend example of binding parameter estimation to include parameter transformation 23 | - [125](https://github.com/fau-advanced-separations/CADET-Process/pull/125): Use seconds in plot time axis 24 | - [126](https://github.com/fau-advanced-separations/CADET-Process/pull/126): AxInterface Runner `staging_required` should be a property 25 | -------------------------------------------------------------------------------- /CADETProcess/metric.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any 3 | 4 | import numpy as np 5 | 6 | 7 | class MetricBase(ABC): 8 | """ 9 | Abstract base class for metrics used in optimization, fractionation, and comparison. 10 | 11 | Attributes 12 | ---------- 13 | n_metrics : int 14 | Number of metrics, default is 1. 15 | bad_metrics : float 16 | Value representing a bad metric, default is infinity. 17 | 18 | """ 19 | 20 | n_metrics = 1 21 | bad_metrics = np.inf 22 | 23 | @abstractmethod 24 | def evaluate(self) -> Any: 25 | """ 26 | Evaluate the metric. 27 | 28 | This method should be implemented by subclasses to perform specific metric calculations. 29 | """ 30 | pass 31 | 32 | def __call__(self, *args: Any, **kwargs: Any) -> Any: 33 | """ 34 | Invoke the evaluate method. 35 | 36 | Parameters 37 | ---------- 38 | *args : tuple 39 | Variable length argument list passed to evaluate. 40 | **kwargs : dict 41 | Arbitrary keyword arguments passed to evaluate. 42 | 43 | Returns 44 | ------- 45 | Any 46 | The result of the metric evaluation. 47 | """ 48 | return self.evaluate(*args, **kwargs) 49 | 50 | def __str__(self) -> str: 51 | """ 52 | Return the class name as the string representation of the instance. 53 | 54 | Returns 55 | ------- 56 | str 57 | The name of the class. 58 | """ 59 | return self.__class__.__name__ 60 | -------------------------------------------------------------------------------- /.zenodo.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "CADET-Process v0.11.1", 3 | "upload_type": "software", 4 | "creators": [ 5 | { 6 | "name": "Schmölder, Johannes", 7 | "orcid": "0000-0003-0446-7209", 8 | "affiliation": "Forschungszentrum Jülich" 9 | }, 10 | { 11 | "name": "Jäpel, Ronald", 12 | "orcid": "0000-0002-4912-5176", 13 | "affiliation": "Forschungszentrum Jülich" 14 | }, 15 | { 16 | "name": "Klauß, Daniel", 17 | "orcid": "0009-0005-2022-7776", 18 | "affiliation": "Forschungszentrum Jülich" 19 | }, 20 | { 21 | "name": "Lanzrath, Hannah", 22 | "orcid": "0000-0002-2675-9002", 23 | "affiliation": "Forschungszentrum Jülich" 24 | }, 25 | { 26 | "name": "Schunck, Florian", 27 | "orcid": "0000-0001-7245-953X", 28 | "affiliation": "Forschungszentrum Jülich" 29 | }, 30 | { 31 | "name": "Berger, Antonia", 32 | "orcid": "0009-0002-0207-9042", 33 | "affiliation": "Forschungszentrum Jülich" 34 | } 35 | ], 36 | "license": "GPL-3.0", 37 | "keywords": [ 38 | "modeling", 39 | "simulation", 40 | "biotechnology", 41 | "process", 42 | "chromatography", 43 | "CADET", 44 | "general rate model", 45 | "Python" 46 | ], 47 | "version": "0.11.1", 48 | "access_right": "open", 49 | "communities": [{"identifier": "open-source"}], 50 | "doi": "10.5281/zenodo.14202878" 51 | } 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a new feature or enhancement for the project 3 | labels: [enhancement] 4 | assignees: [] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## Feature Request Checklist 11 | 12 | - Ensure that this feature is not already implemented or suggested in another issue. 13 | - Provide a clear and descriptive title for the feature request. 14 | - Clearly describe the feature, its purpose, and any potential benefits. 15 | - Include relevant examples or use cases that illustrate the need. 16 | - Consider potential challenges or impacts. 17 | 18 | - type: textarea 19 | id: feature_description 20 | attributes: 21 | label: Feature Description 22 | description: Describe the feature, including purpose, functionality, use cases, and any other relevant info. 23 | placeholder: | 24 | - Purpose: What problem does this solve? 25 | - Functionality: How should it work? 26 | - Use Cases: Where would it be useful? 27 | - Additional info: Diagrams, references, mockups, etc. 28 | render: markdown 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: potential_challenges 34 | attributes: 35 | label: Potential Challenges 36 | description: List any technical limitations, compatibility issues, or implementation concerns. 37 | placeholder: E.g. impacts on existing modules, performance concerns, platform dependencies... 38 | render: markdown 39 | validations: 40 | required: false 41 | 42 | -------------------------------------------------------------------------------- /tests/test_pymoo.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import unittest 3 | 4 | from CADETProcess import settings 5 | from CADETProcess.optimization import U_NSGA3 6 | 7 | from tests.test_optimization_problem import setup_optimization_problem 8 | 9 | 10 | class Test_OptimizationProblemSimple(unittest.TestCase): 11 | def tearDown(self): 12 | shutil.rmtree("./results_simple", ignore_errors=True) 13 | settings.working_directory = None 14 | 15 | def test_restart_from_checkpoint(self): 16 | class Callback: 17 | def __init__(self, n_calls=0, kill=True): 18 | self.n_calls = n_calls 19 | self.kill = kill 20 | 21 | def __call__(self, results): 22 | if self.kill and self.n_calls == 2: 23 | raise Exception("Kill this!") 24 | self.n_calls += 1 25 | 26 | callback = Callback() 27 | optimization_problem = setup_optimization_problem() 28 | optimization_problem.add_callback(callback) 29 | 30 | optimizer = U_NSGA3() 31 | optimizer.n_max_gen = 5 32 | 33 | try: 34 | opt_results = optimizer.optimize( 35 | optimization_problem, 36 | save_results=True, 37 | use_checkpoint=False, 38 | ) 39 | except Exception: 40 | pass 41 | 42 | callback.kill = False 43 | 44 | optimization_problem = setup_optimization_problem() 45 | optimization_problem.add_callback(callback) 46 | 47 | opt_results = optimizer.optimize( 48 | optimization_problem, 49 | save_results=True, 50 | use_checkpoint=True, 51 | ) 52 | 53 | 54 | if __name__ == "__main__": 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /docs/source/release_notes/v0.7.0.md: -------------------------------------------------------------------------------- 1 | # v0.7.0 2 | 3 | **CADET-Process** v0.7.0 is the culmination of 10 months of hard work. 4 | It contains many new features, numerous bug-fixes, improved test coverage and better 5 | documentation. 6 | There have been a number of deprecations and API changes in this release, which are documented below. 7 | All users are encouraged to upgrade to this release, as there are a large number of bug-fixes and 8 | optimizations. 9 | 10 | This release requires Python 3.8+ 11 | 12 | 13 | ## Highlights of this release 14 | - Complete overhaul of optimization problem structure: 15 | - Evaluation objects 16 | - Evaluators 17 | - Caching using diskcache 18 | - Add optimization variable normalization. 19 | - Complete overhaul of optimization results. 20 | - Improved plots for objectives space, Pareto fronts. 21 | - Improved checkpoints. 22 | - Check functions before running simulations and optimizations. 23 | - Add Parameter Sensitivities. 24 | - Provide derivatives and anti-derivatives of solution objects. 25 | - Allow slicing solution in any dimension (including components). 26 | - Add Yamamoto's method to determine isotherm parameters. 27 | - Complete overhaul of documentation: 28 | - Improved docstrings coverage 29 | - New user guide 30 | - Many new unit and integration tests. 31 | 32 | ## Issues closed for 0.7.0 33 | - [2](https://github.com/fau-advanced-separations/CADET-Process/issues/2): Add Parameter Sensitivities 34 | - [10](https://github.com/fau-advanced-separations/CADET-Process/issues/10): Update Pymoo Interface to v0.6.0 35 | 36 | 37 | ## Pull requests for 0.7.0 38 | - [13](https://github.com/fau-advanced-separations/CADET-Process/pull/13): Change normalization for NRMSE from max solution to max reference 39 | - [16](https://github.com/fau-advanced-separations/CADET-Process/pull/16): Overhaul docs 40 | -------------------------------------------------------------------------------- /CADETProcess/dataStructure/parameter_group.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from CADETProcess import CADETProcessError 4 | 5 | __all__ = ["ParameterWrapper"] 6 | 7 | 8 | class ParameterWrapper: 9 | """ 10 | Base class for converting the config from objects such as units. 11 | 12 | Attributes 13 | ---------- 14 | _base_class : type 15 | Type constraint for wrapped object 16 | _wrapped_object : obj 17 | Object whose config is to be converted 18 | 19 | Raises 20 | ------ 21 | CADETProcessError 22 | If the wrapped_object is no instance of the base_class. 23 | """ 24 | 25 | _base_class = object 26 | 27 | def __init__(self, wrapped_object: Any) -> None: 28 | """Construct ParameterWrapper object.""" 29 | if not isinstance(wrapped_object, self._baseClass): 30 | raise CADETProcessError(f"Expected {self._baseClass}") 31 | 32 | model = wrapped_object.model 33 | try: 34 | self.model_parameters = self._model_parameters[model] 35 | except KeyError: 36 | raise CADETProcessError("Model Type not defined") 37 | 38 | self._wrapped_object = wrapped_object 39 | 40 | @property 41 | def parameters(self) -> dict: 42 | """dict: Parameters dictionary.""" 43 | model_parameters = {} 44 | 45 | model_parameters[self._model_type] = self.model_parameters["name"] 46 | 47 | for key, value in self.model_parameters["parameters"].items(): 48 | value = getattr(self._wrapped_object, value) 49 | if value is not None: 50 | model_parameters[key] = value 51 | 52 | for key, value in self.model_parameters.get("fixed", dict()).items(): 53 | if isinstance(value, list): 54 | value = self._wrapped_object.n_comp * value 55 | model_parameters[key] = value 56 | 57 | return model_parameters 58 | -------------------------------------------------------------------------------- /docs/source/release_notes/template.md: -------------------------------------------------------------------------------- 1 | # v0.7.0 2 | 3 | **CADET-Process** v0.7.0 is the culmination of 10 months of hard work. 4 | It contains many new features, numerous bug-fixes, improved test coverage and better documentation. 5 | There have been a number of deprecations and API changes in this release, which are documented below. 6 | All users are encouraged to upgrade to this release, as there are a large number of bug-fixes and optimizations. 7 | Before upgrading, we recommend that users check that their own code does not use deprecated **CADET-Process** functionality (to do so, run your code with ``python -Wd`` and check for ``DeprecationWarning``). 8 | 9 | This release requires Python 3.10+ 10 | 11 | ## Highlights and new features of this release 12 | 13 | ### {mod}`CADETProcess.processModel` improvements 14 | 15 | ### {mod}`CADETProcess.comparison` improvements 16 | 17 | ### {mod}`CADETProcess.fractionation` improvements 18 | 19 | ### {mod}`CADETProcess.optimization` improvements 20 | 21 | ### {mod}`CADETProcess.modelBuilder` improvements 22 | 23 | ## Deprecated features 24 | 25 | The following functions will be removed in a future release. 26 | Users are suggested to upgrade to [...] 27 | 28 | ## Expired Deprecations 29 | 30 | The following previously deprecated features are now removed: 31 | 32 | - Change 1 33 | - Change 2 34 | 35 | ## Other changes 36 | 37 | - Change 1 38 | - Change 2 39 | 40 | ## Issues closed for 0.7.0 41 | 42 | - [2](https://github.com/fau-advanced-separations/CADET-Process/issues/2): Add Parameter Sensitivities 43 | - [10](https://github.com/fau-advanced-separations/CADET-Process/issues/10): Update Pymoo Interface to v0.6.0 44 | 45 | ## Pull requests for 0.7.0 46 | 47 | - [13](https://github.com/fau-advanced-separations/CADET-Process/pull/13): Change normalization for NRMSE from max solution to max reference 48 | - [16](https://github.com/fau-advanced-separations/CADET-Process/pull/16): Overhaul docs 49 | -------------------------------------------------------------------------------- /docs/source/user_guide/process_model/index.md: -------------------------------------------------------------------------------- 1 | (process_model_guide)= 2 | # Process Model 3 | 4 | Starting point of process development is the setup of the {class}`~CADETProcess.processModel.Process` (see {ref}`Framework overview `) model, i.e., the specific configuration of the chromatographic process. 5 | This is realized using {class}`UnitOperations ` as building blocks. 6 | A {class}`UnitOperation ` represents the physico-chemical behavior of an apparatus and holds the model parameters. 7 | For more information refer to {ref}`unit_operation_guide`. 8 | 9 | All {class}`UnitOperations ` can be associated with {class}`BindingModels ` that describe the interaction of components with surfaces or chromatographic stationary phases. 10 | For this purpose, a variety of equilibrium relations, for example,the simple {class}`~CADETProcess.processModel.Linear` adsorption isotherm, competitive forms of the {class}`~CADETProcess.processModel.Langmuir` and the {class}`~CADETProcess.processModel.BiLangmuir` models, as well as the competitive {class}`~CADETProcess.processModel.StericMassAction` law can be selected. 11 | For more information refer to {ref}`binding_models_guide`. 12 | 13 | Moreover, {class}`ReactionModels ` can be used to model chemical reactions. 14 | For more information refer to {ref}`reaction_models_guide`. 15 | 16 | Multiple {class}`UnitOperation ` can be connected in a {class}`~CADETProcess.processModel.FlowSheet` which describes the mass transfer between the individual units. 17 | For more information refer to {ref}`flow_sheet_guide`. 18 | 19 | Finally, dynamic {class}`Events ` can be defined to model dynamic changes of model parameters, including flow rates system connectivity. 20 | For more information refer to {ref}`process_guide`. 21 | 22 | In the following, the different modules are introduced. 23 | 24 | 25 | ```{toctree} 26 | :maxdepth: 2 27 | 28 | component_system 29 | binding_model 30 | reaction 31 | unit_operation 32 | flow_sheet 33 | process 34 | ``` 35 | -------------------------------------------------------------------------------- /examples/characterize_chromatographic_system/Yamamoto_method.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | formats: md:myst,py:percent 4 | text_representation: 5 | extension: .md 6 | format_name: myst 7 | format_version: 0.13 8 | jupytext_version: 1.17.1 9 | kernelspec: 10 | display_name: Python 3 (ipykernel) 11 | language: python 12 | name: python3 13 | --- 14 | 15 | ```{code-cell} 16 | :tags: [remove-cell] 17 | 18 | from pathlib import Path 19 | import sys 20 | 21 | root_dir = Path('../../../../').resolve() 22 | sys.path.append(root_dir.as_posix()) 23 | ``` 24 | 25 | # The Yamamoto method 26 | 27 | This example demonstrates how to estimate SMA binding parameters based on multiple gradient elution chromatograms 28 | using the Yamamoto method. 29 | 30 | ```{code-cell} 31 | import numpy as np 32 | 33 | from CADETProcess.processModel import ComponentSystem 34 | from CADETProcess.tools.yamamoto import GradientExperiment, fit_parameters 35 | 36 | from binding_model_parameters import create_column_model, create_in_silico_experimental_data 37 | 38 | if __name__ == "__main__": 39 | component_system = ComponentSystem(['Salt', 'Protein']) 40 | column = create_column_model(component_system, final_salt_concentration=600, initial_salt_concentration=50) 41 | 42 | column_volume = column.length * ((column.diameter / 2) ** 2) * np.pi 43 | 44 | create_in_silico_experimental_data() 45 | 46 | exp_5cv = np.loadtxt("experimental_data/5.csv", delimiter=",") 47 | exp_30cv = np.loadtxt("experimental_data/30.csv", delimiter=",") 48 | exp_120cv = np.loadtxt("experimental_data/120.csv", delimiter=",") 49 | 50 | experiment_1 = GradientExperiment(exp_5cv[:, 0], exp_5cv[:, 1], exp_5cv[:, 2], 5 * column_volume) 51 | experiment_2 = GradientExperiment(exp_30cv[:, 0], exp_30cv[:, 1], exp_30cv[:, 2], 30 * column_volume) 52 | experiment_3 = GradientExperiment(exp_120cv[:, 0], exp_120cv[:, 1], exp_120cv[:, 2], 120 * column_volume) 53 | 54 | experiments = [experiment_1, experiment_2, experiment_3] 55 | 56 | for experiment in experiments: 57 | experiment.plot() 58 | 59 | yamamoto_results = fit_parameters(experiments, column) 60 | 61 | print('yamamoto_results.characteristic_charge =', yamamoto_results.characteristic_charge) 62 | print('yamamoto_results.k_eq =', yamamoto_results.k_eq) 63 | 64 | yamamoto_results.plot() 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/source/user_guide/process_model/component_system.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | kernelspec: 6 | display_name: Python 3 7 | name: python3 8 | --- 9 | 10 | ```{code-cell} ipython3 11 | :tags: [remove-cell] 12 | 13 | import sys 14 | sys.path.append('../../../../') 15 | ``` 16 | 17 | (component_system_guide)= 18 | # Component System 19 | The {class}`~CADETProcess.processModel.ComponentSystem` ensures that all parts of the process have the same number of components. 20 | Moreover, components can be named which automatically adds legends to the plot methods. 21 | 22 | The easiest way to initiate a {class}`~CADETProcess.processModel.ComponentSystem` is to simply pass the number of components in the constructor. 23 | 24 | ```{code-cell} ipython3 25 | from CADETProcess.processModel import ComponentSystem 26 | 27 | component_system = ComponentSystem(2) 28 | ``` 29 | 30 | Alternatively, a list of strings for the component names can be passed: 31 | 32 | ```{code-cell} ipython3 33 | component_system = ComponentSystem(['A', 'B']) 34 | ``` 35 | 36 | For more complicated systems, it is recommended to add components individually using {meth}`~CADETProcess.processModel.ComponentSystem.add_component`. 37 | For this purpose, add the name, as well as additional properties such as: 38 | - `charge` 39 | - `molecular_weight` 40 | 41 | ```{code-cell} ipython3 42 | component_system = ComponentSystem() 43 | component_system.add_component( 44 | 'A', 45 | charge=1 46 | ) 47 | ``` 48 | 49 | Moreover, a {class}`~CADETProcess.processModel.Component` can have different {class}`~CADETProcess.processModel.Species`. 50 | For example, in some situations, charged species need to be considered separately. 51 | However, for plotting, only the total concentration might be required. 52 | To register a {class}`~CADETProcess.processModel.Species` in the {class}`~CADETProcess.processModel.ComponentSystem`, add a `species` argument. 53 | Note that this requires the number of entries to match for all properties. 54 | 55 | ```{code-cell} ipython3 56 | component_system = ComponentSystem() 57 | component_system.add_component( 58 | 'Proton', 59 | species=['H+'], 60 | charge=[1] 61 | ) 62 | component_system.add_component( 63 | 'Ammonia', 64 | species=['NH4+', 'NH3'], 65 | charge=[1, 0] 66 | ) 67 | print(component_system.names) 68 | print(component_system.species) 69 | ``` 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a reproducible bug or unexpected behavior 3 | labels: [bug] 4 | assignees: [] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## Bug Report Checklist 11 | 12 | - Ensure the bug hasn’t already been reported. 13 | - Provide a minimal, reproducible example if possible. 14 | - Clearly describe what you expected and what actually happened. 15 | - Include relevant environment info and error messages. 16 | 17 | - type: textarea 18 | id: description 19 | attributes: 20 | label: Bug Description 21 | description: What happened? What did you expect to happen? 22 | placeholder: A clear and concise description of the problem. 23 | render: markdown 24 | validations: 25 | required: true 26 | 27 | - type: textarea 28 | id: steps_to_reproduce 29 | attributes: 30 | label: Steps to Reproduce 31 | description: How can we reproduce this bug? 32 | placeholder: | 33 | 1. Run command X 34 | 2. Pass input Y 35 | 3. Observe output Z 36 | render: markdown 37 | validations: 38 | required: true 39 | 40 | - type: input 41 | id: version 42 | attributes: 43 | label: CADET-Process Version 44 | description: Provide the exact version or commit hash you were using. 45 | placeholder: e.g. v0.6.0 or commit `abc123` 46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | id: environment 51 | attributes: 52 | label: Environment 53 | description: Describe the environment in which the bug occurred. 54 | placeholder: | 55 | - OS: Ubuntu 22.04 56 | - Python: 3.11.3 57 | - Conda environment: yes/no 58 | - CADET-Process: v0.11.0 59 | - CADET-Python: v1.0.0 60 | - CADET-Core: v5.0.4 61 | 62 | render: markdown 63 | validations: 64 | required: true 65 | 66 | - type: textarea 67 | id: logs 68 | attributes: 69 | label: Relevant Logs or Tracebacks 70 | description: Paste any relevant log output, error messages, or stack traces. 71 | placeholder: | 72 | ``` 73 | Traceback (most recent call last): 74 | File ... 75 | ``` 76 | render: text 77 | validations: 78 | required: false 79 | 80 | -------------------------------------------------------------------------------- /examples/characterize_chromatographic_system/Yamamoto_method.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: md:myst,py:percent 5 | # text_representation: 6 | # extension: .py 7 | # format_name: percent 8 | # format_version: '1.3' 9 | # jupytext_version: 1.17.1 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # %% tags=["remove-cell"] 17 | from pathlib import Path 18 | import sys 19 | 20 | root_dir = Path('../../../../').resolve() 21 | sys.path.append(root_dir.as_posix()) 22 | 23 | # %% [markdown] 24 | # # The Yamamoto method 25 | # 26 | # This example demonstrates how to estimate SMA binding parameters based on multiple gradient elution chromatograms 27 | # using the Yamamoto method. 28 | 29 | # %% 30 | import numpy as np 31 | 32 | from CADETProcess.processModel import ComponentSystem 33 | from CADETProcess.tools.yamamoto import GradientExperiment, fit_parameters 34 | 35 | from binding_model_parameters import create_column_model, create_in_silico_experimental_data 36 | 37 | if __name__ == "__main__": 38 | component_system = ComponentSystem(['Salt', 'Protein']) 39 | column = create_column_model(component_system, final_salt_concentration=600, initial_salt_concentration=50) 40 | 41 | column_volume = column.length * ((column.diameter / 2) ** 2) * np.pi 42 | 43 | create_in_silico_experimental_data() 44 | 45 | exp_5cv = np.loadtxt("experimental_data/5.csv", delimiter=",") 46 | exp_30cv = np.loadtxt("experimental_data/30.csv", delimiter=",") 47 | exp_120cv = np.loadtxt("experimental_data/120.csv", delimiter=",") 48 | 49 | experiment_1 = GradientExperiment(exp_5cv[:, 0], exp_5cv[:, 1], exp_5cv[:, 2], 5 * column_volume) 50 | experiment_2 = GradientExperiment(exp_30cv[:, 0], exp_30cv[:, 1], exp_30cv[:, 2], 30 * column_volume) 51 | experiment_3 = GradientExperiment(exp_120cv[:, 0], exp_120cv[:, 1], exp_120cv[:, 2], 120 * column_volume) 52 | 53 | experiments = [experiment_1, experiment_2, experiment_3] 54 | 55 | for experiment in experiments: 56 | experiment.plot() 57 | 58 | yamamoto_results = fit_parameters(experiments, column) 59 | 60 | print('yamamoto_results.characteristic_charge =', yamamoto_results.characteristic_charge) 61 | print('yamamoto_results.k_eq =', yamamoto_results.k_eq) 62 | 63 | yamamoto_results.plot() 64 | -------------------------------------------------------------------------------- /CADETProcess/numerics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def round_to_significant_digits( 5 | values: np.ndarray | list[float], 6 | digits: int, 7 | ) -> np.ndarray: 8 | """ 9 | Round an array of numbers to the specified number of significant digits. 10 | 11 | Parameters 12 | ---------- 13 | values : np.ndarray | list[float] 14 | Input array of floats to be rounded. Can be a NumPy array or a list of floats. 15 | digits : int 16 | Number of significant digits to retain. Must be greater than 0. 17 | 18 | Returns 19 | ------- 20 | np.ndarray 21 | Array of values rounded to the specified significant digits. 22 | The shape matches the input. 23 | 24 | Notes 25 | ----- 26 | - The function handles zero values by returning 0 directly, avoiding issues 27 | with logarithms. 28 | - Input arrays are automatically converted to `ndarray` if not already. 29 | 30 | Examples 31 | -------- 32 | >>> import numpy as np 33 | >>> values = np.array([123.456, 0.001234, 98765, 0, -45.6789]) 34 | >>> round_to_significant_digits(values, 3) 35 | array([ 123. , 0.00123, 98700. , 0. , -45.7 ]) 36 | 37 | >>> values = [1.2345e-5, 6.789e3, 0.0] 38 | >>> round_to_significant_digits(values, 2) 39 | array([ 1.2e-05, 6.8e+03, 0.0e+00]) 40 | """ 41 | if digits is None: 42 | return values 43 | 44 | input_type = type(values) 45 | 46 | values = np.asarray(values) # Ensure input is a NumPy array 47 | 48 | if digits <= 0: 49 | raise ValueError("Number of significant digits must be greater than 0.") 50 | 51 | # Mask for non-zero values 52 | nonzero_mask = values != 0 53 | nan_mask = ~np.isnan(values) 54 | combined_mask = np.logical_and(nonzero_mask, nan_mask) 55 | result = np.zeros_like(values) # Initialize result array 56 | 57 | # For non-zero elements, calculate the scaling and apply rounding 58 | if np.any(combined_mask): # Check if there are any non-zero values 59 | nonzero_values = values[combined_mask] 60 | scales = digits - np.floor(np.log10(np.abs(nonzero_values))).astype(int) - 1 61 | 62 | # Round each non-zero value individually 63 | rounded_nonzero = [ 64 | round(v, int(scale)) for v, scale in zip(nonzero_values, scales) 65 | ] 66 | 67 | result[combined_mask] = rounded_nonzero # Assign the rounded values back 68 | 69 | result[~nan_mask] = np.nan 70 | if input_type is not np.ndarray: 71 | result = input_type(result) 72 | 73 | return result 74 | -------------------------------------------------------------------------------- /examples/load_wash_elute/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | formats: md:myst,py:percent 4 | text_representation: 5 | extension: .md 6 | format_name: myst 7 | format_version: 0.13 8 | jupytext_version: 1.14.5 9 | kernelspec: 10 | display_name: Python 3 11 | language: python 12 | name: python3 13 | --- 14 | 15 | (lwe_example)= 16 | # Load-Wash-Elute 17 | tags: single column, steric mass action law, sma, gradient 18 | 19 | A typical process to separate a mixture of components using ion exchange chromatography is the load-wash-elute process (LWE). 20 | The first step is to load the sample onto the stationary phase. 21 | The column is then washed with a solvent that removes any impurities or unwanted components that may be present. 22 | Finally, the bound compound is eluted using a solvent that displaces the compound from the stationary phase (usually with high salt concentration). 23 | 24 | For this purpose, often gradients are employed. 25 | In gradient chromatography, the concentration of one or more components of the mobile phase is systematically changed over time. 26 | The gradient can be linear or non-linear, and the change in solvent strength can be accomplished by changing the proportion of different solvents in the mobile phase, by adjusting the pH or ionic strength of the mobile phase, or by other means. 27 | Gradient chromatography is particularly useful when separating complex mixtures of compounds with similar physical and chemical properties. 28 | By gradually changing the mobile phase, the separation can be optimized to separate the various components of the mixture, leading to better resolution and higher quality separation. 29 | 30 | For example, the following shows a typical concentration profile for a linear gradient. 31 | 32 | ```{code-cell} 33 | :tags: [remove-input] 34 | 35 | from lwe_concentration import process 36 | 37 | from CADETProcess.simulator import Cadet 38 | process_simulator = Cadet() 39 | 40 | simulation_results = process_simulator.simulate(process) 41 | 42 | from CADETProcess.plotting import SecondaryAxis 43 | sec = SecondaryAxis() 44 | sec.components = ['Salt'] 45 | sec.y_label = '$c_{salt}$' 46 | 47 | _ = simulation_results.solution.column.inlet.plot(secondary_axis=sec) 48 | ``` 49 | 50 | In **CADET-Process**, gradients can be used by either changing the concentration profile of an {class}`~CADETProcess.processModel.Inlet` or by adding multiple inlets and dynamically adjusting their flow rates. 51 | In the following, both approaches are presented. 52 | 53 | 54 | ```{toctree} 55 | :maxdepth: 1 56 | 57 | lwe_concentration 58 | lwe_flow_rate 59 | ``` 60 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: pipeline 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | - test_ci 9 | pull_request: 10 | 11 | jobs: 12 | test-job: 13 | runs-on: ${{ matrix.os }} 14 | 15 | defaults: 16 | run: 17 | shell: bash -l {0} 18 | 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest] 22 | python-version: ["3.10", "3.11", "3.12"] 23 | include: 24 | - os: windows-latest 25 | python-version: "3.12" 26 | - os: macos-13 27 | python-version: "3.12" 28 | 29 | env: 30 | CONDA_FILE: environment.yml 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Get Date 36 | id: get-date 37 | run: echo "today=$(/bin/date -u '+%Y%m%d')" >> $GITHUB_OUTPUT 38 | shell: bash 39 | 40 | - name: Setup Conda Environment 41 | uses: conda-incubator/setup-miniconda@v3 42 | with: 43 | miniforge-version: latest 44 | activate-environment: cadet-process 45 | channels: conda-forge 46 | 47 | - name: Cache conda 48 | uses: actions/cache@v4 49 | env: 50 | # Increase this value to reset cache if environment.yml has not changed 51 | CACHE_NUMBER: 0 52 | with: 53 | path: ${{ env.CONDA }}/envs 54 | key: ${{ matrix.os }}-python_${{ matrix.python-version }}-${{ steps.get-date.outputs.today }}-${{ hashFiles(env.CONDA_FILE) }}-${{ env.CACHE_NUMBER }} 55 | id: cache 56 | 57 | - name: Update environment 58 | run: | 59 | conda install "setuptools>=69" "pip>=24" 60 | conda install python=${{ matrix.python-version }} 61 | echo "python=${{ matrix.python-version }}.*" > $CONDA_PREFIX/conda-meta/pinned 62 | conda env update -n cadet-process -f ${{ env.CONDA_FILE }} 63 | if: steps.cache.outputs.cache-hit != 'true' 64 | 65 | - name: Install 66 | run: | 67 | conda run pip install -e ./[all] --group testing 68 | 69 | - name: Test 70 | run: | 71 | pytest tests -m "not slow" --durations=0 72 | 73 | - name: Install pypa/build 74 | run: | 75 | conda run python -m pip install build --user 76 | 77 | - name: Build binary wheel and source tarball 78 | run: | 79 | conda run python -m build --sdist --wheel --outdir dist/ . 80 | 81 | - name: Test Wheel install and import 82 | run: | 83 | conda run python -c "import CADETProcess; print(CADETProcess.__version__)" 84 | -------------------------------------------------------------------------------- /CADETProcess/dataStructure/deprecation.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import warnings 3 | from typing import Any, Callable, Dict 4 | 5 | __all__ = ["deprecated_alias", "rename_kwargs"] 6 | 7 | 8 | def deprecated_alias(**aliases: str) -> Callable: 9 | """ 10 | Add alias for deprecated function arguments. 11 | 12 | Parameters 13 | ---------- 14 | **aliases : str 15 | Keyword arguments where keys are old parameter names and values are new parameter names 16 | 17 | Returns 18 | ------- 19 | Callable 20 | A decorator function that wraps the original function 21 | 22 | Examples 23 | -------- 24 | @deprecated_alias(old_name='new_name') 25 | def example_function(new_name): 26 | return new_name 27 | """ 28 | 29 | # Decorator function: takes the f and returns a f with the new argument names 30 | def decorator(f: Callable) -> Callable: 31 | @functools.wraps(f) 32 | def wrap_decorated_argument(*args: Any, **kwargs: Any) -> Any: 33 | rename_kwargs(f.__name__, kwargs, aliases) 34 | return f(*args, **kwargs) 35 | 36 | return wrap_decorated_argument 37 | 38 | return decorator 39 | 40 | 41 | def rename_kwargs( 42 | func_name: str, kwargs: Dict[str, Any], aliases: Dict[str, str] 43 | ) -> None: 44 | """ 45 | Automatically rename deprecated function arguments. 46 | 47 | Parameters 48 | ---------- 49 | func_name : str 50 | Name of the function being decorated 51 | kwargs : Dict[str, Any] 52 | Dictionary of keyword arguments passed to the function 53 | aliases : Dict[str, str] 54 | Dictionary mapping old parameter names to new ones 55 | 56 | Returns 57 | ------- 58 | None 59 | 60 | Raises 61 | ------ 62 | TypeError 63 | If both old and new parameter names are used simultaneously 64 | 65 | Examples 66 | -------- 67 | rename_kwargs('example_function', {'old_name': 'value'}, {'old_name': 'new_name'}) 68 | """ 69 | for alias, new in aliases.items(): 70 | if alias in kwargs: 71 | if new in kwargs: 72 | raise TypeError( 73 | f"{func_name} received both {alias} and {new} as arguments!" 74 | f" {alias} is deprecated, use {new} instead." 75 | ) 76 | warnings.warn( 77 | message=( 78 | f"`{alias}` is deprecated as an argument to `{func_name}`; use `{new}` instead." 79 | ), 80 | category=DeprecationWarning, 81 | stacklevel=2, 82 | ) 83 | kwargs[new] = kwargs.pop(alias) 84 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | - test_ci 9 | workflow_dispatch: 10 | inputs: 11 | slow: 12 | type: boolean 13 | description: Run with slow tests 14 | default: false 15 | 16 | jobs: 17 | coverage-job: 18 | runs-on: ${{ matrix.os }} 19 | 20 | defaults: 21 | run: 22 | shell: bash -l {0} 23 | 24 | strategy: 25 | matrix: 26 | os: [ubuntu-latest] 27 | python-version: ["3.12"] 28 | 29 | env: 30 | CONDA_FILE: environment.yml 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Get Date 36 | id: get-date 37 | run: echo "today=$(/bin/date -u '+%Y%m%d')" >> $GITHUB_OUTPUT 38 | shell: bash 39 | 40 | - name: Setup Conda Environment 41 | uses: conda-incubator/setup-miniconda@v3 42 | with: 43 | miniforge-version: latest 44 | use-mamba: true 45 | activate-environment: cadet-process 46 | channels: conda-forge, 47 | 48 | - name: Cache conda 49 | uses: actions/cache@v4 50 | env: 51 | # Increase this value to reset cache if environment.yml has not changed 52 | CACHE_NUMBER: 0 53 | with: 54 | path: ${{ env.CONDA }}/envs 55 | key: ${{ matrix.os }}-python_${{ matrix.python-version }}-${{ steps.get-date.outputs.today }}-${{ hashFiles(env.CONDA_FILE) }}-${{ env.CACHE_NUMBER }} 56 | 57 | - name: Update environment 58 | run: | 59 | mamba install "setuptools>=69" "pip>=24" 60 | mamba install python=${{ matrix.python-version }} 61 | echo "python=${{ matrix.python-version }}.*" > $CONDA_PREFIX/conda-meta/pinned 62 | mamba env update -n cadet-process -f ${{ env.CONDA_FILE }} 63 | if: steps.cache.outputs.cache-hit != 'true' 64 | 65 | - name: Install 66 | run: | 67 | pip install -e ./[all] --group testing 68 | 69 | # Push event doesn't have input context => inputs.notslows is empty => false 70 | - name: Coverage Run 71 | run: | 72 | if [ ${{ github.event.inputs.slow }} ]; then 73 | pytest --cov=./CADETProcess --cov=./tests --cov-branch --cov-report term-missing --cov-report xml tests 74 | else 75 | pytest -m "not slow" --cov=./CADETProcess --cov=./tests --cov-branch --cov-report term-missing --cov-report xml tests 76 | fi 77 | - name: Upload coverage reports to Codecov 78 | uses: codecov/codecov-action@v5 79 | with: 80 | token: ${{ secrets.CODECOV_TOKEN }} 81 | -------------------------------------------------------------------------------- /docs/source/user_guide/optimization/variable_normalization.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | kernelspec: 6 | display_name: Python 3 7 | name: python3 8 | --- 9 | 10 | ```{code-cell} ipython3 11 | :tags: [remove-cell] 12 | 13 | import sys 14 | sys.path.append('../../../../') 15 | ``` 16 | 17 | (variable_normalization_guide)= 18 | # Variable Normalization 19 | Most optimization algorithms struggle when optimization variables spread over multiple orders of magnitude. 20 | This is important because the magnitude or range of the parameters can impact the optimization process, and the relative importance of each parameter can be difficult to determine without normalization. 21 | Normalizing parameters makes the optimization process more efficient and helps ensure that the results are more accurate and reliable. 22 | Additionally, normalization can prevent the optimization process from becoming biased towards certain parameters, which could lead to suboptimal or inefficient solutions. 23 | 24 | **CADET-Process** provides several transformation methods which can help to soften these challenges. 25 | 26 | In the following `x` will refer to the value exposed to the optimizer, whereas `variable` refers to the actual parameter value. 27 | 28 | ```{figure} ./figures/transform.svg 29 | :name: transform 30 | ``` 31 | 32 | ## Linear Normalization 33 | The linear normalization maps the variable space from the lower and upper bound to a range between $0$ and $1$ by applying the following transformation: 34 | 35 | $$ 36 | x^\prime = \frac{x - x_{lb}}{x_{ub} - x_{lb}} 37 | $$ 38 | 39 | ```{code-cell} ipython3 40 | :tags: [hide-cell] 41 | 42 | from CADETProcess.optimization import OptimizationProblem 43 | optimization_problem = OptimizationProblem('normalization_demo') 44 | ``` 45 | 46 | ```{code-cell} ipython3 47 | optimization_problem.add_variable('var_norm_lin', lb=-100, ub=100, transform='linear') 48 | ``` 49 | 50 | ## Log Normalization 51 | The log normalization maps the variable space from the lower and upper bound to a range between $0$ and $1$ by applying the following transformation: 52 | 53 | $$ 54 | x^\prime = \frac{log \left( \frac{x}{x_{lb}} \right) }{log \left( \frac{x_{ub} }{x_{lb}} \right) } 55 | $$ 56 | 57 | ```{code-cell} ipython3 58 | optimization_problem.add_variable('var_norm_log', lb=-100, ub=100, transform='log') 59 | ``` 60 | 61 | ## Auto Transform 62 | This transform will automatically switch between a linear and a log transform if the ratio of upper and lower bounds is larger than some value ($1000$ by default). 63 | 64 | 65 | ```{code-cell} ipython3 66 | optimization_problem.add_variable('var_norm_auto', lb=-100, ub=100, transform='auto') 67 | ``` 68 | -------------------------------------------------------------------------------- /CADETProcess/dataStructure/cache.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | from .dataStructure import Structure 4 | from .parameter import Bool 5 | 6 | __all__ = [ 7 | "CachedPropertiesMixin", 8 | "cached_property_if_locked", 9 | ] 10 | 11 | 12 | class cached_property_if_locked(property): 13 | """ 14 | A property that caches its value if the instance is locked. 15 | 16 | This property extends the built-in property to cache its value when the instance 17 | is locked. The cached value is stored in the instance's `cached_properties` dictionary. 18 | """ 19 | 20 | def __get__(self, instance: Any, cls: Optional[type] = None) -> Any: 21 | """ 22 | Get the value of the property, using the cache if the instance is locked. 23 | 24 | Parameters 25 | ---------- 26 | instance : Any 27 | The instance from which to retrieve the property value. 28 | cls : Optional[type], optional 29 | The class of the instance, by default None. 30 | 31 | Returns 32 | ------- 33 | Any 34 | The value of the property. 35 | """ 36 | if instance.lock: 37 | try: 38 | return instance.cached_properties[self.name] 39 | except KeyError: 40 | pass 41 | 42 | value = super().__get__(instance, cls) 43 | 44 | if instance.lock: 45 | instance.cached_properties[self.name] = value 46 | 47 | return value 48 | 49 | @property 50 | def name(self) -> str: 51 | """str: name of the property.""" 52 | return self.fget.__name__ 53 | 54 | 55 | class CachedPropertiesMixin(Structure): 56 | """ 57 | Mixin class for caching properties in a structured object. 58 | 59 | This class is designed to be used as a mixin in conjunction with other classes 60 | inheriting from `Structure`. It provides functionality for caching properties and 61 | managing a lock state to control the caching behavior. 62 | 63 | Notes 64 | ----- 65 | - To prevent the return of outdated state, the cache is cleared whenever the `lock` 66 | state is changed. 67 | """ 68 | 69 | _lock = Bool(default=False) 70 | 71 | def __init__(self, *args: Any, **kwargs: Any) -> None: 72 | """Initialize Cached Properties Mixin Object.""" 73 | super().__init__(*args, **kwargs) 74 | self.cached_properties = {} 75 | 76 | @property 77 | def lock(self) -> bool: 78 | """bool: If True, properties are cached. False otherwise.""" 79 | return self._lock 80 | 81 | @lock.setter 82 | def lock(self, lock: bool) -> None: 83 | self._lock = lock 84 | self.cached_properties = {} 85 | -------------------------------------------------------------------------------- /CADETProcess/processModel/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ================================================ 3 | Process Model (:mod:`CADETProcess.processModel`) 4 | ================================================ 5 | 6 | .. currentmodule:: CADETProcess.processModel 7 | 8 | Classes for modelling processes. 9 | 10 | ComponentSystem 11 | =============== 12 | 13 | .. autosummary:: 14 | :toctree: generated/ 15 | 16 | Species 17 | Component 18 | ComponentSystem 19 | 20 | Reaction Models 21 | =============== 22 | 23 | .. autosummary:: 24 | :toctree: generated/ 25 | 26 | Reaction 27 | CrossPhaseReaction 28 | ReactionBaseClass 29 | NoReaction 30 | MassActionLaw 31 | MassActionLawParticle 32 | 33 | Binding Models 34 | ============== 35 | 36 | .. autosummary:: 37 | :toctree: generated/ 38 | 39 | BindingBaseClass 40 | NoBinding 41 | Linear 42 | Langmuir 43 | LangmuirLDF 44 | LangmuirLDFLiquidPhase 45 | BiLangmuir 46 | BiLangmuirLDF 47 | FreundlichLDF 48 | StericMassAction 49 | AntiLangmuir 50 | Spreading 51 | MobilePhaseModulator 52 | ExtendedMobilePhaseModulator 53 | SelfAssociation 54 | BiStericMassAction 55 | MultistateStericMassAction 56 | SimplifiedMultistateStericMassAction 57 | Saska 58 | GeneralizedIonExchange 59 | HICConstantWaterActivity 60 | HICWaterOnHydrophobicSurfaces 61 | MultiComponentColloidal 62 | 63 | Unit Operation Models 64 | ===================== 65 | 66 | .. autosummary:: 67 | :toctree: generated/ 68 | 69 | UnitBaseClass 70 | Inlet 71 | Outlet 72 | Cstr 73 | TubularReactor 74 | LumpedRateModelWithoutPores 75 | LumpedRateModelWithPores 76 | GeneralRateModel 77 | MCT 78 | 79 | Discretization 80 | -------------- 81 | 82 | .. autosummary:: 83 | :toctree: generated/ 84 | 85 | discretization 86 | 87 | Solution Recorder 88 | ----------------- 89 | 90 | .. autosummary:: 91 | :toctree: generated/ 92 | 93 | solutionRecorder 94 | 95 | Flow Sheet 96 | ========== 97 | 98 | .. autosummary:: 99 | :toctree: generated/ 100 | 101 | FlowSheet 102 | 103 | Process 104 | ======= 105 | 106 | .. autosummary:: 107 | :toctree: generated/ 108 | 109 | Process 110 | 111 | """ 112 | 113 | from . import componentSystem 114 | from .componentSystem import * 115 | 116 | from . import reaction 117 | from .reaction import * 118 | 119 | from . import binding 120 | from .binding import * 121 | 122 | from . import discretization 123 | from .discretization import * 124 | 125 | from . import unitOperation 126 | from .unitOperation import * 127 | 128 | from . import flowSheet 129 | from .flowSheet import * 130 | 131 | from . import process 132 | from .process import * 133 | 134 | __all__ = [s for s in dir()] 135 | -------------------------------------------------------------------------------- /docs/source/user_guide/optimization/index.md: -------------------------------------------------------------------------------- 1 | (optimization_guide)= 2 | # Optimization 3 | One of the main applications of **CADET-Process** is performing optimization studies. 4 | Optimization refers to the selection of a solution with regard to some criterion. 5 | In the simplest case, an optimization problem consists of minimizing some function $f(x)$ by systematically varying the input values $x$ and computing the value of that function. 6 | 7 | $$ 8 | \min_x f(x) 9 | $$ 10 | 11 | In the context of physico-chemical processes, examples for the application of optimization studies include scenarios such as process optimization (see {ref}`batch_elution_optimization_single`) and parameter estimation (see {ref}`fit_column_transport`). 12 | Here, often many variables are subject to optimization, multiple criteria have to be balanced, and additional linear and nonlinear constraints need to be considered. 13 | 14 | $$ 15 | \min_x f(x) \\ 16 | 17 | s.t. \\ 18 | &g(x) \le 0, \\ 19 | &h(x) = 0, \\ 20 | &x \in \mathbb{R}^n \\ 21 | $$ 22 | 23 | where $g$ summarizes all inequality constraint functions, and $h$ equality constraints. 24 | 25 | 26 | In the following, the optimization module of CADET-Process is introduced. 27 | To decouple the problem formulation from the problem solution, two classes are provided: 28 | An {class}`~CADETProcess.optimization.OptimizationProblem` class to specify optimization variables, objectives and constraints. 29 | And an {class}`~CADETProcess.optimization.OptimizerBase` class which allows interfacing different external optimizers to solve the problem. 30 | 31 | ```{toctree} 32 | :maxdepth: 2 33 | 34 | optimization_problem 35 | optimizer 36 | ``` 37 | 38 | ## Installation of different Optimizers 39 | To maintain the manageability and efficiency of CADET-Process, some optimizers that come with a substantial number of dependencies are made optional. 40 | This approach ensures that the core package remains lightweight, while providing users the flexibility to install additional optimizers if needed. 41 | By default, scipy and pymoo are installed. 42 | Below, we provide instructions on how to install these optional dependencies. 43 | 44 | ### Ax/BoTorch 45 | Ax is an adaptable machine learning optimization library developed by Facebook. 46 | At its core, it uses BoTorch, a Bayesian optimization framework also developed by Facebook. 47 | Ax/BoTorch leverages Gaussian Processes to model the objective function and applies Bayesian optimization techniques to find the optimal parameters. 48 | 49 | To install Ax as an optional dependency of CADET-Process, use the following command: 50 | ```bash 51 | pip install cadet-process[ax] 52 | ``` 53 | 54 | ## Advanced Configuration 55 | ```{toctree} 56 | :maxdepth: 2 57 | 58 | parallel_evaluation 59 | evaluator 60 | multi_objective_optimization 61 | variable_indices 62 | variable_normalization 63 | variable_dependencies 64 | ``` 65 | -------------------------------------------------------------------------------- /tests/test_deprecation.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | # Import the functions to test 4 | from CADETProcess.dataStructure import deprecated_alias, rename_kwargs 5 | 6 | 7 | class TestDeprecatedAlias(unittest.TestCase): 8 | def test_basic_functionality(self): 9 | @deprecated_alias(old_arg="new_arg") 10 | def example_func(new_arg): 11 | return new_arg 12 | 13 | # Test with new argument name 14 | self.assertEqual(example_func(new_arg=42), 42) 15 | 16 | # Test with old argument name 17 | with self.assertWarns(DeprecationWarning) as warning_context: 18 | result = example_func(old_arg=42) 19 | self.assertEqual(result, 42) 20 | 21 | # Verify warning message 22 | warning_message = str(warning_context.warning) 23 | self.assertIn("`old_arg` is deprecated", warning_message) 24 | self.assertIn("use `new_arg` instead", warning_message) 25 | 26 | def test_argument_conflict(self): 27 | """Test error when both old and new argument names are used.""" 28 | 29 | @deprecated_alias(old_arg="new_arg") 30 | def example_func(new_arg): 31 | return new_arg 32 | 33 | # Should raise TypeError when both old and new names are used 34 | with self.assertRaises(TypeError) as context: 35 | example_func(old_arg=1, new_arg=2) 36 | 37 | self.assertIn( 38 | "received both old_arg and new_arg as arguments", str(context.exception) 39 | ) 40 | 41 | def test_rename_kwargs_direct(self): 42 | """Test rename_kwargs function directly.""" 43 | kwargs = {"old_arg": 42} 44 | aliases = {"old_arg": "new_arg"} 45 | 46 | with self.assertWarns(DeprecationWarning): 47 | rename_kwargs("test_func", kwargs, aliases) 48 | 49 | self.assertIn("new_arg", kwargs) 50 | self.assertNotIn("old_arg", kwargs) 51 | self.assertEqual(kwargs["new_arg"], 42) 52 | 53 | def test_positional_args(self): 54 | """Test that positional arguments work normally with the decorator.""" 55 | 56 | @deprecated_alias(old_kwarg="new_kwarg") 57 | def example_func(pos_arg, new_kwarg=None): 58 | return f"{pos_arg}-{new_kwarg}" 59 | 60 | # Test with positional argument 61 | self.assertEqual(example_func("test", new_kwarg=42), "test-42") 62 | 63 | # Test with old keyword argument 64 | with self.assertWarns(DeprecationWarning): 65 | self.assertEqual(example_func("test", old_kwarg=42), "test-42") 66 | 67 | def test_no_arguments(self): 68 | """Test function with no arguments still works with decorator.""" 69 | 70 | @deprecated_alias(old_arg="new_arg") 71 | def example_func(): 72 | return "success" 73 | 74 | self.assertEqual(example_func(), "success") 75 | 76 | 77 | if __name__ == "__main__": 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /tests/test_numerics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from CADETProcess.numerics import round_to_significant_digits 4 | 5 | 6 | def test_basic_functionality(): 7 | values = np.array([123.456, 0.001234, 98765]) 8 | expected = np.array([123.0, 0.00123, 98800.0]) 9 | result = round_to_significant_digits(values, 3) 10 | np.testing.assert_allclose(result, expected, rtol=1e-6) 11 | 12 | 13 | def test_handling_zeros(): 14 | values = np.array([0, 0.0, -0.0]) 15 | expected = np.array([0.0, 0.0, -0.0]) 16 | result = round_to_significant_digits(values, 3) 17 | np.testing.assert_array_equal(result, expected) 18 | 19 | 20 | def test_negative_numbers(): 21 | values = np.array([-123.456, -0.001234, -98765]) 22 | expected = np.array([-123.0, -0.00123, -98800.0]) 23 | result = round_to_significant_digits(values, 3) 24 | np.testing.assert_allclose(result, expected, rtol=1e-6) 25 | 26 | 27 | def test_large_numbers(): 28 | values = np.array([1.23e10, 9.87e15]) 29 | expected = np.array([1.23e10, 9.87e15]) 30 | result = round_to_significant_digits(values, 3) 31 | np.testing.assert_allclose(result, expected, rtol=1e-6) 32 | 33 | 34 | def test_small_numbers(): 35 | values = np.array([1.23e-10, 9.87e-15]) 36 | expected = np.array([1.23e-10, 9.87e-15]) 37 | result = round_to_significant_digits(values, 3) 38 | np.testing.assert_allclose(result, expected, rtol=1e-6) 39 | 40 | 41 | def test_mixed_values(): 42 | values = np.array([123.456, 0, -0.001234, 9.8765e-5]) 43 | expected = np.array([123.0, 0.0, -0.00123, 9.88e-5]) 44 | result = round_to_significant_digits(values, 3) 45 | np.testing.assert_allclose(result, expected, rtol=1e-6) 46 | 47 | 48 | def test_invalid_digits(): 49 | with pytest.raises( 50 | ValueError, match="Number of significant digits must be greater than 0." 51 | ): 52 | round_to_significant_digits(np.array([123.456]), 0) 53 | 54 | 55 | def test_empty_array(): 56 | values = np.array([]) 57 | expected = np.array([]) 58 | result = round_to_significant_digits(values, 3) 59 | np.testing.assert_array_equal(result, expected) 60 | 61 | 62 | def test_non_array_input(): 63 | values = [123.456, 0.001234, 98765] # List input 64 | expected = np.array([123.0, 0.00123, 98800.0]) 65 | result = round_to_significant_digits(values, 3) 66 | np.testing.assert_allclose(result, expected, rtol=1e-6) 67 | 68 | 69 | def test_none_digits(): 70 | values = [123.456, 0.001234, 98765] # List input 71 | expected = np.array([123.456, 0.001234, 98765]) 72 | result = round_to_significant_digits(values, None) 73 | np.testing.assert_allclose(result, expected, rtol=1e-6) 74 | 75 | 76 | def test_nan_digits(): 77 | values = np.array([123.456, np.nan, 98765]) 78 | expected = np.array([123.0, np.nan, 98800.0]) 79 | result = round_to_significant_digits(values, 3) 80 | np.testing.assert_allclose(result, expected, rtol=1e-6) 81 | -------------------------------------------------------------------------------- /docs/source/user_guide/optimization/figures/no_transform.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Variable
Variable
x
x
Text is not SVG - cannot display
4 | -------------------------------------------------------------------------------- /docs/source/release_notes/v0.8.0.md: -------------------------------------------------------------------------------- 1 | # v0.8.0 2 | 3 | **CADET-Process** v0.8.0 is the culmination of the last 6 months of development. 4 | All users are encouraged to upgrade to this release. 5 | This release requires Python 3.9+ 6 | 7 | 8 | ## Highlights of this release 9 | - Overhaul of parameter descriptors: Cleaner structure, new validation methods and improved testing. 10 | - Improved indexing for multidimensional parameters for events and optimization variables. 11 | - Improved parallelization: Selector for backend allows more flexibility when running optimization on multiple cores. 12 | - Overhaul of `reactions` module: Individual reaction parameters can now be accessed using the standard `parameters` interface (required for optimization). 13 | - Improved handling of variable transforms for optimization problems with linear constraints. 14 | 15 | ## Breaking changes 16 | 17 | ### Adsorption rates and reference concentrations 18 | The parameter behavior for adsorption rates in CADET-Core and CADET-Process has been a point of discussion. 19 | This parameter can represent either the "real" $k_a$ or the transformed $\tilde{k}_{a}$, depending on whether a reference concentration is used in binding models such as SMA. 20 | 21 | To clarify this, CADET-Process initially introduced the `adsorption_rate` parameter to always signify the "real" $k_a$. 22 | An additional internal property, `adsorption_rate_transformed`, was introduced to handle the transformation automatically before passing the value to CADET-Core. 23 | This aimed to simplify the transfer of values to different reference concentrations or adsorption models. 24 | 25 | Despite these efforts, this inconsistency between CADET-Core and CADET-Process lead to some confusion among users. 26 | 27 | Starting from CADET-Process v0.8.0, a more harmonized approach has been adopted. 28 | Now, `adsorption_rate` and `desorption_rate` in the SMA model (and similar models) directly map to $k_a$/$k_d$ of CADET-Core, thus representing the transformed parameters. 29 | For users who still need access to the "real" parameter values, for example, when transferring parameters between different systems, the attributes `adsorption_rate_untransformed` and `desorption_rate_untransformed` have been introduced into CADET-Process binding model classes. 30 | 31 | ## Pull requests for 0.8.0 32 | - [38](https://github.com/fau-advanced-separations/CADET-Process/pull/38): Fix parallelization error `Cadet` object has no attribute `_is_file_class` 33 | - [40](https://github.com/fau-advanced-separations/CADET-Process/pull/40): Add selector for parallelization backend 34 | - [46](https://github.com/fau-advanced-separations/CADET-Process/pull/46): Fix linear constraints error in Scipy 35 | - [54](https://github.com/fau-advanced-separations/CADET-Process/pull/54): Fix indexing for Events and OptimizationVariables 36 | - [55](https://github.com/fau-advanced-separations/CADET-Process/pull/55): Rework interface regarding reference concentrations 37 | - [59](https://github.com/fau-advanced-separations/CADET-Process/pull/59): Fix/change addict version 38 | -------------------------------------------------------------------------------- /examples/batch_elution/optimization_single.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | formats: md:myst,py:percent 4 | text_representation: 5 | extension: .md 6 | format_name: myst 7 | format_version: 0.13 8 | jupytext_version: 1.17.1 9 | main_language: python 10 | kernelspec: 11 | display_name: Python 3 12 | name: python3 13 | --- 14 | 15 | (batch_elution_optimization_single)= 16 | # Optimize Batch Elution Process (Single Objective) 17 | 18 | ## Setup Optimization Problem 19 | 20 | ```{code-cell} 21 | from CADETProcess.optimization import OptimizationProblem 22 | optimization_problem = OptimizationProblem('batch_elution_single') 23 | 24 | from examples.batch_elution.process import process 25 | optimization_problem.add_evaluation_object(process) 26 | 27 | optimization_problem.add_variable('cycle_time', lb=10, ub=600) 28 | optimization_problem.add_variable('feed_duration.time', lb=10, ub=300) 29 | 30 | optimization_problem.add_linear_constraint( 31 | ['feed_duration.time', 'cycle_time'], [1, -1] 32 | ) 33 | ``` 34 | 35 | ## Setup Simulator 36 | 37 | ```{code-cell} 38 | from CADETProcess.simulator import Cadet 39 | process_simulator = Cadet() 40 | process_simulator.evaluate_stationarity = True 41 | 42 | optimization_problem.add_evaluator(process_simulator) 43 | ``` 44 | 45 | ## Setup Fractionator 46 | 47 | ```{code-cell} 48 | from CADETProcess.fractionation import FractionationOptimizer 49 | frac_opt = FractionationOptimizer() 50 | 51 | optimization_problem.add_evaluator( 52 | frac_opt, 53 | kwargs={ 54 | 'purity_required': [0.95, 0.95], 55 | 'ignore_failed': False, 56 | 'allow_empty_fractions': False, 57 | } 58 | ) 59 | ``` 60 | 61 | ## Add callback for post-processing 62 | 63 | ```{code-cell} 64 | def callback(fractionation, individual, evaluation_object, callbacks_dir): 65 | fractionation.plot_fraction_signal( 66 | file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_fractionation.png', 67 | show=False 68 | ) 69 | 70 | 71 | optimization_problem.add_callback( 72 | callback, requires=[process_simulator, frac_opt] 73 | ) 74 | ``` 75 | 76 | ## Setup Objectives 77 | 78 | ```{code-cell} 79 | from CADETProcess.performance import PerformanceProduct 80 | ranking = [1, 1] 81 | performance = PerformanceProduct(ranking=ranking) 82 | 83 | optimization_problem.add_objective( 84 | performance, requires=[process_simulator, frac_opt], minimize=False, 85 | ) 86 | ``` 87 | 88 | ## Configure Optimizer 89 | 90 | ```{code-cell} 91 | from CADETProcess.optimization import U_NSGA3 92 | optimizer = U_NSGA3() 93 | ``` 94 | 95 | ## Run Optimization 96 | 97 | ```{note} 98 | For performance reasons, the optimization is currently not run when building the documentation. 99 | In future, we will try to sideload pre-computed results to also discuss them here. 100 | ``` 101 | 102 | ``` 103 | results = optimizer.optimize( 104 | optimization_problem, 105 | use_checkpoint=True, 106 | ) 107 | ``` 108 | -------------------------------------------------------------------------------- /docs/source/user_guide/overview.md: -------------------------------------------------------------------------------- 1 | (overview)= 2 | # Framework Overview 3 | 4 | **CADET-Process** is written in Python, a free and open-source programming language that gained a lot of popularity in the scientific community in recent years. 5 | One of the main advantages of Python is the easy integration of other scientific and numerical packages. 6 | This makes it especially useful for a modular approach such as the one presented. 7 | The following figure gives a general overview of the program structure and workflow. 8 | 9 | ```{figure} ./figures/framework_overview.svg 10 | :name: framework_overview 11 | 12 | Overview of the framework modules and their relations. 13 | White boxes represent input configurations and solution objects, blue boxes internal tools and procedures, and green boxes external tools. 14 | For a detailed explanation, see text. 15 | ``` 16 | 17 | The {class}`~CADETProcess.processModel.Process` is an abstract representation of the chromatographic process configuration including the operational and design parameters. 18 | Processes can be simulated using a {class}`Simulator ` which solves the underlying equations. 19 | The {class}`Simulator ` adapter acts as an abstract interface to external solvers (e.g. **CADET**) and translates the internal configuration to the corresponding format of the solver. 20 | After the computation is finished, the {class}`~CADETProcess.simulationResults.SimulationResults` are returned and can be further evaluated (see {ref}`simulation_guide`). 21 | If a {class}`~CADETProcess.stationarity.StationarityEvaluator` is configured to test for cyclic stationarity, more chromatographic cycles are be simulated until stationarity is reached (see {ref}`stationarity_guide`). 22 | 23 | For processing the {class}`~CADETProcess.simulationResults.SimulationResults`, different modules are provided. 24 | For example, they can be compared to experimental data (or other simulations) using a {class}`~CADETProcess.comparison.Comparator` which computes residuals such as the sum of squared errors (see also {ref}`comparison_guide`). 25 | Moreover, the {class}`~CADETProcess.fractionation.Fractionator` module automatically determines fractionation times of the simulated chromatograms and determines process performance indicators such as purity, yield, and productivity (see {ref}`fractionation_guide`). 26 | 27 | These metrics can be used as objectives in an {class}`~CADETProcess.optimization.OptimizationProblem` class which serves to configure optimization studies. 28 | Here, any process parameter can be added as optimization variable and the evaluation methods can be used to construct objectives and constraint functions. 29 | This enables many different scenarios such as {ref}`process optimization ` and {ref}`parameter estimation `. 30 | Again, an abstract {class}`Optimizer ` provides an interface to external optimization algorithms such as {class}`U-NSGA-3 ` (see {ref}`optimization_guide`). 31 | -------------------------------------------------------------------------------- /docs/source/user_guide/process_model/binding_model.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | kernelspec: 6 | display_name: Python 3 7 | name: python3 8 | --- 9 | 10 | ```{code-cell} ipython3 11 | :tags: [remove-cell] 12 | 13 | import sys 14 | sys.path.append('../../../../') 15 | ``` 16 | 17 | (binding_models_guide)= 18 | # Binding Models 19 | 20 | In **CADET-Process**, a number of binding models can be used to describe the interaction of molecules with a stationary phase. 21 | For an overview of all models in **CADET-Process**, see {mod}`CADETProcess.processModel`. 22 | It's important to note that the adsorption model is defined independently of the unit operation. 23 | This facilitates reusing the same configuration in different unit operations or processes. 24 | 25 | To instantiate a binding model, the corresponding class needs to be imported from the {mod}`CADETProcess.processModel` module. 26 | For example, consider the {class}`~CADETProcess.processModel.Langmuir` isotherm with a two-component system. 27 | 28 | ```{code-cell} ipython3 29 | :tags: [hide-cell] 30 | 31 | from CADETProcess.processModel import ComponentSystem 32 | component_system = ComponentSystem(2) 33 | ``` 34 | 35 | ```{code-cell} ipython3 36 | from CADETProcess.processModel import Langmuir 37 | binding_model = Langmuir(component_system) 38 | ``` 39 | 40 | All model parameters can be listed using the `parameters` attribute. 41 | 42 | ```{code-cell} ipython3 43 | print(binding_model.parameters) 44 | ``` 45 | 46 | Note that some parameters might have default values. 47 | To only show required parameters, inspect `required_parameters`. 48 | 49 | ```{code-cell} ipython3 50 | print(binding_model.required_parameters) 51 | ``` 52 | 53 | ## Multi-State Binding 54 | Some binding models support multiple binding sites, for example, the {class}`~CADETProcess.processModel.BiLangmuir` model. 55 | Note that originally, the Bi-Langmuir model is limited to two different binding site types. 56 | In **CADET**, the model has been extended to arbitrary many binding site types. 57 | 58 | ```{code-cell} ipython3 59 | from CADETProcess.processModel import BiLangmuir 60 | 61 | binding_model = BiLangmuir(component_system, n_binding_sites=2, name='langmuir') 62 | ``` 63 | 64 | {attr}`~CADETProcess.processModel.BindingBaseClass.n_binding_sites` denotes the total number of binding sites whereas {attr}`~CADETProcess.processModel.BindingBaseClass.n_bound_states` the total number of bound states. 65 | 66 | ```{code-cell} ipython3 67 | print(binding_model.n_binding_sites) 68 | print(binding_model.n_bound_states) 69 | ``` 70 | 71 | This means that parameters like `adsorption_rate` now have $4$ entries. 72 | 73 | ```{code-cell} ipython3 74 | binding_model.adsorption_rate = [1,2,3,4] 75 | ``` 76 | The order of this linear array is in `state-major` ordering. 77 | 78 | *I.e.*, the bound state index changes the slowest and the component index the fastest: 79 | ``` 80 | comp0bnd0, comp1bnd0, comp0bnd1, comp1bnd1 81 | ``` 82 | 83 | Note that this also effects the number of entries for the initial state of the stationary phase `q` (see {ref}`unit_operation_guide`). 84 | -------------------------------------------------------------------------------- /CADETProcess/optimization/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | =============================================== 3 | Optimization (:mod:`CADETProcess.optimization`) 4 | =============================================== 5 | 6 | .. currentmodule:: CADETProcess.optimization 7 | 8 | The ``optimization`` module provides functionality for minimizing (or maximizing) 9 | objective functions, possibly subject to constraints. It includes interfaces to several 10 | optimization suites, notably, ``scipy.optimize`` and ``pymoo``. 11 | 12 | OptimizationProblem 13 | =================== 14 | .. autosummary:: 15 | :toctree: generated/ 16 | 17 | OptimizationProblem 18 | 19 | 20 | Optimizer 21 | ========= 22 | 23 | Base 24 | ---- 25 | 26 | .. autosummary:: 27 | :toctree: generated/ 28 | 29 | OptimizerBase 30 | 31 | Scipy 32 | ----- 33 | 34 | .. autosummary:: 35 | :toctree: generated/ 36 | 37 | TrustConstr 38 | COBYLA 39 | COBYQA 40 | NelderMead 41 | SLSQP 42 | 43 | Pymoo 44 | ----- 45 | 46 | .. autosummary:: 47 | :toctree: generated/ 48 | 49 | NSGA2 50 | U_NSGA3 51 | 52 | Ax 53 | -- 54 | 55 | .. autosummary:: 56 | :toctree: generated/ 57 | 58 | BotorchModular 59 | GPEI 60 | NEHVI 61 | 62 | Population 63 | ========== 64 | .. autosummary:: 65 | :toctree: generated/ 66 | 67 | Individual 68 | Population 69 | 70 | 71 | Results 72 | ======= 73 | .. autosummary:: 74 | :toctree: generated/ 75 | 76 | OptimizationResults 77 | 78 | 79 | Cache 80 | ===== 81 | .. autosummary:: 82 | :toctree: generated/ 83 | 84 | ResultsCache 85 | 86 | ParallelizationBackend 87 | ====================== 88 | .. autosummary:: 89 | :toctree: generated/ 90 | 91 | ParallelizationBackendBase 92 | SequentialBackend 93 | Joblib 94 | Pathos 95 | 96 | """ 97 | 98 | from .individual import * 99 | from .population import * 100 | from .cache import * 101 | from .results import * 102 | from .optimizationProblem import OptimizationProblem, OptimizationVariable 103 | from .parallelizationBackend import * 104 | from .optimizer import * 105 | from .scipyAdapter import COBYLA, COBYQA, TrustConstr, NelderMead, SLSQP 106 | from .pymooAdapter import NSGA2, U_NSGA3 107 | 108 | import importlib 109 | 110 | try: 111 | from .axAdapater import BotorchModular, GPEI, NEHVI, qNParEGO 112 | 113 | ax_imported = True 114 | except ImportError: 115 | ax_imported = False 116 | 117 | 118 | def __getattr__(name): 119 | if name in ("BotorchModular", "GPEI", "NEHVI", "qNParEGO"): 120 | if ax_imported: 121 | module = importlib.import_module("axAdapter", package=__name__) 122 | return getattr(module, name) 123 | else: 124 | raise ImportError( 125 | "The AxInterface class could not be imported. " 126 | "This may be because the 'ax' package, which is an optional dependency, is not installed. " 127 | "To install it, run 'pip install CADET-Process[ax]'" 128 | ) 129 | raise AttributeError(f"module {__name__} has no attribute {name}") 130 | -------------------------------------------------------------------------------- /examples/batch_elution/optimization_single.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: md:myst,py:percent 5 | # text_representation: 6 | # extension: .py 7 | # format_name: percent 8 | # format_version: '1.3' 9 | # jupytext_version: 1.17.1 10 | # kernelspec: 11 | # display_name: Python 3 12 | # name: python3 13 | # --- 14 | 15 | # %% [markdown] 16 | # (batch_elution_optimization_single)= 17 | # # Optimize Batch Elution Process (Single Objective) 18 | # 19 | # ## Setup Optimization Problem 20 | 21 | # %% 22 | from CADETProcess.optimization import OptimizationProblem 23 | optimization_problem = OptimizationProblem('batch_elution_single') 24 | 25 | from examples.batch_elution.process import process 26 | optimization_problem.add_evaluation_object(process) 27 | 28 | optimization_problem.add_variable('cycle_time', lb=10, ub=600) 29 | optimization_problem.add_variable('feed_duration.time', lb=10, ub=300) 30 | 31 | optimization_problem.add_linear_constraint( 32 | ['feed_duration.time', 'cycle_time'], [1, -1] 33 | ) 34 | 35 | # %% [markdown] 36 | # ## Setup Simulator 37 | 38 | # %% 39 | from CADETProcess.simulator import Cadet 40 | process_simulator = Cadet() 41 | process_simulator.evaluate_stationarity = True 42 | 43 | optimization_problem.add_evaluator(process_simulator) 44 | 45 | # %% [markdown] 46 | # ## Setup Fractionator 47 | 48 | # %% 49 | from CADETProcess.fractionation import FractionationOptimizer 50 | frac_opt = FractionationOptimizer() 51 | 52 | optimization_problem.add_evaluator( 53 | frac_opt, 54 | kwargs={ 55 | 'purity_required': [0.95, 0.95], 56 | 'ignore_failed': False, 57 | 'allow_empty_fractions': False, 58 | } 59 | ) 60 | 61 | 62 | # %% [markdown] 63 | # ## Add callback for post-processing 64 | 65 | # %% 66 | def callback(fractionation, individual, evaluation_object, callbacks_dir): 67 | fractionation.plot_fraction_signal( 68 | file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_fractionation.png', 69 | show=False 70 | ) 71 | 72 | 73 | optimization_problem.add_callback( 74 | callback, requires=[process_simulator, frac_opt] 75 | ) 76 | 77 | # %% [markdown] 78 | # ## Setup Objectives 79 | 80 | # %% 81 | from CADETProcess.performance import PerformanceProduct 82 | ranking = [1, 1] 83 | performance = PerformanceProduct(ranking=ranking) 84 | 85 | optimization_problem.add_objective( 86 | performance, requires=[process_simulator, frac_opt], minimize=False, 87 | ) 88 | 89 | # %% [markdown] 90 | # ## Configure Optimizer 91 | 92 | # %% 93 | from CADETProcess.optimization import U_NSGA3 94 | optimizer = U_NSGA3() 95 | 96 | # %% [markdown] 97 | # ## Run Optimization 98 | # 99 | # ```{note} 100 | # For performance reasons, the optimization is currently not run when building the documentation. 101 | # In future, we will try to sideload pre-computed results to also discuss them here. 102 | # ``` 103 | # 104 | # ``` 105 | # results = optimizer.optimize( 106 | # optimization_problem, 107 | # use_checkpoint=True, 108 | # ) 109 | # ``` 110 | -------------------------------------------------------------------------------- /CADETProcess/comparison/peaks.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import numpy as np 4 | import scipy.signal 5 | 6 | from CADETProcess.solution import SolutionIO 7 | 8 | 9 | def find_peaks( 10 | solution: SolutionIO, 11 | normalize: bool = True, 12 | prominence: float = 0.5, 13 | find_minima: bool = False, 14 | ) -> list[list[tuple[float, float]]]: 15 | """ 16 | Find peaks in solution. 17 | 18 | Parameters 19 | ---------- 20 | solution : SolutionIO 21 | Solution object. 22 | normalize : bool, optional 23 | If true, normalize data to maximum value (for each component). 24 | The default is True. 25 | prominence : float, optional 26 | Required prominence to detekt peak. The default is 0.5. 27 | find_minima : bool, optional 28 | Find negative peaks/minima of solution. The default is False. 29 | 30 | Returns 31 | ------- 32 | peaks : list 33 | List with list of (time, height) for each peak for every component. 34 | Regardless of normalization, the actual peak height is returned. 35 | """ 36 | solution_original = solution 37 | solution = copy.deepcopy(solution) 38 | 39 | if normalize: 40 | solution = solution.normalize() 41 | 42 | peaks = [] 43 | for i in range(solution.component_system.n_comp): 44 | sol = solution.solution[:, i].copy() 45 | 46 | if find_minima: 47 | sol *= -1 48 | 49 | peak_indices, _ = scipy.signal.find_peaks(sol, prominence=prominence) 50 | if len(peak_indices) == 0: 51 | peak_indices = [np.argmax(sol)] 52 | time = solution.time[peak_indices] 53 | peak_heights = solution_original.solution[peak_indices, i] 54 | 55 | peaks.append([(t, h) for t, h in zip(time, peak_heights)]) 56 | 57 | return peaks 58 | 59 | 60 | def find_breakthroughs( 61 | solution: SolutionIO, 62 | normalize: bool = True, 63 | threshold: float = 0.95, 64 | ) -> list[(float, float)]: 65 | """ 66 | Find breakthroughs in solution. 67 | 68 | Parameters 69 | ---------- 70 | solution : SolutionIO 71 | Solution object. 72 | normalize : bool, optional 73 | If true, normalize data to maximum value (for each component). 74 | The default is True. 75 | threshold : float, optional 76 | Percentage of maximum concentration that needs to be reached to be 77 | considered as breakthrough. The default is 0.95. 78 | 79 | Returns 80 | ------- 81 | breakthrough : list 82 | List with (time, height) for breakthrough of every component. 83 | Regardless of normalization, the actual breakthroug height is returned. 84 | """ 85 | solution_original = solution 86 | solution = copy.deepcopy(solution) 87 | 88 | if normalize: 89 | solution = solution.normalize() 90 | 91 | breakthrough = [] 92 | for i in range(solution.component_system.n_comp): 93 | sol = solution.solution[:, i].copy() 94 | 95 | breakthrough_indices = np.where(sol > threshold * np.max(sol))[0][0] 96 | if len(breakthrough_indices) == 0: 97 | breakthrough_indices = [np.argmax(sol)] 98 | time = solution.time[breakthrough_indices] 99 | breakthrough_height = solution_original.solution[breakthrough_indices, i] 100 | 101 | breakthrough.append((time, breakthrough_height)) 102 | 103 | return breakthrough 104 | -------------------------------------------------------------------------------- /docs/source/user_guide/process_model/reaction.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | kernelspec: 6 | display_name: Python 3 7 | name: python3 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | 15 | ```{code-cell} ipython3 16 | :tags: [remove-cell] 17 | 18 | import sys 19 | sys.path.append('../../../../') 20 | ``` 21 | 22 | $$\require{mhchem}$$ 23 | 24 | (reaction_models_guide)= 25 | # Chemical Reactions 26 | Since version 4, it is possible to model chemical reactions with CADET using mass action law type reactions (see {ref}`reaction_models`). 27 | The mass action law states that the speed of a reaction is proportional to the product of the concentrations of their reactants. 28 | 29 | In CADET-Process, a reaction module was implemented to facilitate the setup of these reactions. 30 | There are two different classes: the {class}`~CADETProcess.processModel.MassActionLaw` which is used for bulk phase reactions, as well as {class}`~CADETProcess.processModel.MassActionLawParticle` which is specifically designed to model reactions in particle pore phase. 31 | 32 | ## Forward Reactions 33 | As a simple example, consider the following system: 34 | 35 | $$ 36 | \ce{1 A ->[k_{AB}] 1 B} 37 | $$ 38 | 39 | Assuming a {class}`~CADETProcess.processModel.ComponentSystem` with components `A` and `B`, configure the {class}`~CADETProcess.processModel.MassActionLaw` reaction model. 40 | 41 | ```{code-cell} ipython3 42 | :tags: [hide-cell] 43 | 44 | from CADETProcess.processModel import ComponentSystem 45 | component_system = ComponentSystem(['A', 'B']) 46 | ``` 47 | 48 | To instantiate it, pass the {class}`~CADETProcess.processModel.ComponentSystem`. 49 | Then, add the reaction using the {meth}`~CADETProcess.processModel.MassActionLaw.add_reaction` method. 50 | The following arguments are expected: 51 | - components: The components names that take part in the reaction (useful for bigger systems) 52 | - stoichiometric coefficients in the order of components 53 | - forward reaction rate 54 | - backward reaction rate 55 | 56 | ```{code-cell} ipython3 57 | from CADETProcess.processModel import MassActionLaw 58 | reaction_system = MassActionLaw(component_system) 59 | reaction_system.add_reaction( 60 | components=['A', 'B'], 61 | coefficients=[-1, 1], 62 | k_fwd=0.1, 63 | k_bwd=0 64 | ) 65 | ``` 66 | 67 | To demonstrate this reaction, a {class}`~CADETProcess.processModel.Cstr` is instantiated and the reaction is added to the tank. 68 | Moreover, the initial conditions are set. 69 | In principle, the {class}`~CADETProcess.processModel.Cstr` supports reactions in bulk and particle pore phase. 70 | Since the porosity is $1$ by default, only the bulk phase is considered. 71 | 72 | ```{code-cell} ipython3 73 | from CADETProcess.processModel import Cstr 74 | 75 | reactor = Cstr(component_system, 'reactor') 76 | reactor.bulk_reaction_model = reaction_system 77 | reactor.V = 1e-6 78 | reactor.c = [1.0, 0.0] 79 | ``` 80 | 81 | ## Equilibrium Reactions 82 | It is also possible to consider equilibrium reactions where the product can react back to the educts. 83 | 84 | $$ 85 | \ce{ 2 A <=>[k_{AB}][k_{BA}] B} 86 | $$ 87 | 88 | ```{code-cell} ipython3 89 | reaction_system = MassActionLaw(component_system) 90 | reaction_system.add_reaction( 91 | components=['A', 'B'], 92 | coefficients=[-2, 1], 93 | k_fwd=0.2, 94 | k_bwd=0.1 95 | ) 96 | 97 | reactor.bulk_reaction_model = reaction_system 98 | ``` 99 | -------------------------------------------------------------------------------- /examples/batch_elution/optimization_multi.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | formats: md:myst,py:percent 4 | text_representation: 5 | extension: .md 6 | format_name: myst 7 | format_version: 0.13 8 | jupytext_version: 1.17.1 9 | kernelspec: 10 | display_name: Python 3 11 | language: python 12 | name: python3 13 | --- 14 | 15 | (batch_elution_optimization_multi)= 16 | # Optimize Batch Elution Process (Multi-Objective) 17 | 18 | ## Setup Optimization Problem 19 | 20 | ```{code-cell} 21 | from CADETProcess.optimization import OptimizationProblem 22 | optimization_problem = OptimizationProblem('batch_elution_multi') 23 | 24 | from examples.batch_elution.process import process 25 | optimization_problem.add_evaluation_object(process) 26 | 27 | optimization_problem.add_variable('cycle_time', lb=10, ub=600) 28 | optimization_problem.add_variable('feed_duration.time', lb=10, ub=300) 29 | 30 | optimization_problem.add_linear_constraint( 31 | ['feed_duration.time', 'cycle_time'], [1, -1] 32 | ) 33 | ``` 34 | 35 | ## Setup Simulator 36 | 37 | ```{code-cell} 38 | from CADETProcess.simulator import Cadet 39 | process_simulator = Cadet() 40 | process_simulator.evaluate_stationarity = True 41 | 42 | optimization_problem.add_evaluator(process_simulator) 43 | ``` 44 | 45 | ## Setup Fractionator 46 | 47 | ```{code-cell} 48 | from CADETProcess.fractionation import FractionationOptimizer 49 | frac_opt = FractionationOptimizer() 50 | 51 | optimization_problem.add_evaluator( 52 | frac_opt, 53 | kwargs={ 54 | 'purity_required': [0.95, 0.95], 55 | 'ignore_failed': False, 56 | 'allow_empty_fractions': False, 57 | } 58 | ) 59 | ``` 60 | 61 | ## Setup Objectives 62 | 63 | ```{code-cell} 64 | from CADETProcess.performance import Productivity, Recovery, EluentConsumption 65 | 66 | productivity = Productivity() 67 | optimization_problem.add_objective( 68 | productivity, 69 | n_objectives=2, 70 | requires=[process_simulator, frac_opt], 71 | minimize=False, 72 | ) 73 | 74 | recovery = Recovery() 75 | optimization_problem.add_objective( 76 | recovery, 77 | n_objectives=2, 78 | requires=[process_simulator, frac_opt], 79 | minimize=False, 80 | ) 81 | 82 | eluent_consumption = EluentConsumption() 83 | optimization_problem.add_objective( 84 | eluent_consumption, 85 | n_objectives=2, 86 | requires=[process_simulator, frac_opt], 87 | minimize=False, 88 | ) 89 | ``` 90 | 91 | ## Add callback for post-processing 92 | 93 | ```{code-cell} 94 | def callback(fractionation, individual, evaluation_object, callbacks_dir): 95 | fractionation.plot_fraction_signal( 96 | file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_fractionation.png', 97 | show=False 98 | ) 99 | 100 | 101 | optimization_problem.add_callback( 102 | callback, requires=[process_simulator, frac_opt] 103 | ) 104 | ``` 105 | 106 | ## Configure Optimizer 107 | 108 | ```{code-cell} 109 | from CADETProcess.optimization import U_NSGA3 110 | optimizer = U_NSGA3() 111 | ``` 112 | 113 | ## Run Optimization 114 | 115 | ```{note} 116 | For performance reasons, the optimization is currently not run when building the documentation. 117 | In future, we will try to sideload pre-computed results to also discuss them here. 118 | ``` 119 | 120 | ``` 121 | if __name__ == '__main__': 122 | results = optimizer.optimize( 123 | optimization_problem, 124 | use_checkpoint=True, 125 | ) 126 | ``` 127 | -------------------------------------------------------------------------------- /docs/source/user_guide/optimization/parallel_evaluation.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | text_representation: 4 | format_name: myst 5 | kernelspec: 6 | display_name: Python 3 7 | name: python3 8 | --- 9 | 10 | ```{code-cell} ipython3 11 | :tags: [remove-cell] 12 | 13 | import sys 14 | sys.path.append('../../../../') 15 | ``` 16 | 17 | (parallel_evaluation_guide)= 18 | # Parallel Evaluation 19 | **CADET-Process** provides a versatile and user-friendly framework for solving optimization problems. 20 | One of the key features of **CADET-Process** is its ability to leverage parallelization, allowing users to exploit the full potential of their multi-core processors and significantly speed up the optimization process. 21 | 22 | In this tutorial, the parallelization backend module of **CADET-Process** is introduced. 23 | This module allows choosing between different parallelization strategies, each tailored to suit different hardware configuration and optimization needs. 24 | By selecting an appropriate backend, the workload can efficiently be distributed across multiple cores, reducing the time required for function evaluations and improving overall optimization performance. 25 | 26 | The parallelization backend module consists of four classes: 27 | - {class}`~CADETProcess.optimization.ParallelizationBackendBase`: This class serves as the base for all parallelization backend adapters. 28 | It provides common attributes and methods that any parallelization backend must implement, such as {attr}`~CADETProcess.optimization.ParallelizationBase.n_cores`, the number of cores to be used for parallelization. 29 | - {class}`~CADETProcess.optimization.SequentialBackend`: This backend is designed for cases where parallel execution is not desired or not possible. 30 | It evaluates the target function sequentially, one individual at a time, making it useful for single-core processors or when parallelization is not beneficial. 31 | - {class}`~CADETProcess.optimization.Joblib`, and {class}`~CADETProcess.optimization.Pathos`: Parallel backends allowing users to leverage multiple cores efficiently for function evaluations. 32 | 33 | All backends implement a {meth}`~CADETProcess.optimization.ParallelizationBackendBase.evaluate` method which takes a function (callable) and a population (Iterable) as input. 34 | This method maps the provided function over the elements of the population array and returns a list containing the results of the function evaluations for each element. 35 | 36 | To demonstrate how to use parallel backends, consider the following example. 37 | 38 | 39 | ```{code-cell} ipython3 40 | # Import the available backends 41 | from CADETProcess.optimization import SequentialBackend, Joblib 42 | 43 | def square(x): 44 | """Simple function that returns the square of a given number.""" 45 | return x ** 2 46 | 47 | # Example 1: Using SequentialBackend 48 | print("Example 1: Using SequentialBackend") 49 | backend = SequentialBackend() 50 | result = backend.evaluate(square, [1, 2, 3, 4]) 51 | print(result) # Output: [1, 4, 9, 16] 52 | 53 | # Example 2: Using Joblib Backend 54 | print("Example 2: Using Joblib Backend") 55 | backend = Joblib(n_cores=2) # Specify the number of cores to be used 56 | result = backend.evaluate(square, [1, 2, 3, 4]) 57 | print(result) # Output: [1, 4, 9, 16] 58 | ``` 59 | 60 | All optimzers can be associated with a parallel backend. 61 | For example, {class}`~CADETProcess.optimization.U_NSGA3` can be configured to use a parallel backend as shown below: 62 | 63 | ```{code-cell} ipython3 64 | # Import the available backends 65 | from CADETProcess.optimization import U_NSGA3 66 | 67 | optimizer = U_NSGA3() 68 | optimizer.backend = Joblib(n_cores=2) 69 | ``` 70 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=69", 4 | "wheel", 5 | "build" 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "CADET-Process" 11 | dynamic = ["version"] 12 | authors = [ 13 | { name = "Johannes Schmölder", email = "j.schmoelder@fz-juelich.de" }, 14 | ] 15 | description = "A Framework for Modelling and Optimizing Advanced Chromatographic Processes" 16 | readme = "README.md" 17 | requires-python = ">=3.10" 18 | keywords = ["process modeling", "process optimization", "chromatography"] 19 | license = { text = "GPLv3" } 20 | classifiers = [ 21 | "Programming Language :: Python :: 3", 22 | "Operating System :: OS Independent", 23 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 24 | "Intended Audience :: Science/Research", 25 | ] 26 | dependencies = [ 27 | "addict==2.3", 28 | "cadet-python>=1.0.4", 29 | "corner>=2.2.1", 30 | "diskcache>=5.4.0", 31 | "hopsy>=1.5.3", 32 | "joblib>=1.3.0", 33 | "numpy>=1.21", 34 | "matplotlib>=3.4", 35 | "numba>=0.55.1", 36 | "pathos>=0.2.8", 37 | "psutil>=5.9.8", 38 | "pymoo>=0.6", 39 | "scipy>=1.14", 40 | ] 41 | 42 | [dependency-groups] 43 | testing = [ 44 | "coverage", 45 | "pytest", 46 | "pytest-cov" 47 | ] 48 | docs = [ 49 | "myst-nb>=0.17.1", 50 | "numpydoc>=1.5.0", 51 | "sphinx>=5.3.0", 52 | "sphinxcontrib-bibtex>=2.5.0", 53 | "sphinx_book_theme>=1.0.0", 54 | "sphinx_copybutton>=0.5.1", 55 | "sphinx-sitemap>=2.5.0", 56 | ] 57 | dev = [ 58 | {include-group = "docs"}, 59 | {include-group = "testing"}, 60 | "certifi", # tries to prevent certificate problems on windows 61 | "pre-commit", 62 | "ruff<=0.14.7", 63 | ] 64 | 65 | [project.optional-dependencies] 66 | ax = [ 67 | "ax-platform >=0.3.5,<0.5.0" 68 | ] 69 | all = [ 70 | "CADET-Process[ax]", 71 | ] 72 | 73 | [project.urls] 74 | homepage = "https://github.com/fau-advanced-separations/CADET-Process" 75 | documentation = "https://cadet-process.readthedocs.io" 76 | "Bug Tracker" = "https://github.com/fau-advanced-separations/CADET-Process/issues" 77 | 78 | [tool.setuptools.packages.find] 79 | include = [ 80 | "CADETProcess*", 81 | "examples*", 82 | "tests*" 83 | ] 84 | 85 | [tool.setuptools.dynamic] 86 | version = { attr = "CADETProcess.__version__" } 87 | 88 | [tool.ruff] 89 | src = ["CADETProcess", "tests"] 90 | exclude = ["examples", "docs", "__init__.py"] 91 | line-length = 88 92 | fix = true 93 | 94 | [tool.ruff.lint] 95 | preview = true 96 | select = [ 97 | "ANN", # type annotations 98 | "D", # docstrings 99 | "E", # pycodestyle errors 100 | "F", # pyflakes 101 | "W", # pycodestyle warnings 102 | "I", # isort 103 | ] 104 | ignore = [ 105 | "ANN401", # Allow dynamically typed expressions like `Any` 106 | "D100", # Allow missing docstring in public module 107 | ] 108 | 109 | [tool.ruff.format] 110 | docstring-code-format = true 111 | 112 | [tool.ruff.lint.pycodestyle] 113 | max-line-length = 100 114 | 115 | [tool.ruff.lint.pydocstyle] 116 | convention = "numpy" 117 | 118 | [tool.ruff.lint.per-file-ignores] 119 | "tests/*.py" = [ 120 | "ANN", # type annotations 121 | "D", # docstrings 122 | "E402", # imports on top of module 123 | "E731", # lamba assignments 124 | "F841", # unused variables 125 | ] 126 | 127 | 128 | [tool.pytest.ini_options] 129 | testpaths = [ 130 | "tests", 131 | ] 132 | pythonpath = [ 133 | "tests", 134 | ] 135 | markers = [ 136 | "slow: marks tests as slow (deselect with '-m \"not slow\"')", 137 | ] 138 | -------------------------------------------------------------------------------- /examples/batch_elution/optimization_multi.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: md:myst,py:percent 5 | # text_representation: 6 | # extension: .py 7 | # format_name: percent 8 | # format_version: '1.3' 9 | # jupytext_version: 1.17.1 10 | # kernelspec: 11 | # display_name: Python 3 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # %% [markdown] 17 | # (batch_elution_optimization_multi)= 18 | # # Optimize Batch Elution Process (Multi-Objective) 19 | # 20 | # ## Setup Optimization Problem 21 | 22 | # %% 23 | from CADETProcess.optimization import OptimizationProblem 24 | optimization_problem = OptimizationProblem('batch_elution_multi') 25 | 26 | from examples.batch_elution.process import process 27 | optimization_problem.add_evaluation_object(process) 28 | 29 | optimization_problem.add_variable('cycle_time', lb=10, ub=600) 30 | optimization_problem.add_variable('feed_duration.time', lb=10, ub=300) 31 | 32 | optimization_problem.add_linear_constraint( 33 | ['feed_duration.time', 'cycle_time'], [1, -1] 34 | ) 35 | 36 | # %% [markdown] 37 | # ## Setup Simulator 38 | 39 | # %% 40 | from CADETProcess.simulator import Cadet 41 | process_simulator = Cadet() 42 | process_simulator.evaluate_stationarity = True 43 | 44 | optimization_problem.add_evaluator(process_simulator) 45 | 46 | # %% [markdown] 47 | # ## Setup Fractionator 48 | 49 | # %% 50 | from CADETProcess.fractionation import FractionationOptimizer 51 | frac_opt = FractionationOptimizer() 52 | 53 | optimization_problem.add_evaluator( 54 | frac_opt, 55 | kwargs={ 56 | 'purity_required': [0.95, 0.95], 57 | 'ignore_failed': False, 58 | 'allow_empty_fractions': False, 59 | } 60 | ) 61 | 62 | # %% [markdown] 63 | # ## Setup Objectives 64 | 65 | # %% 66 | from CADETProcess.performance import Productivity, Recovery, EluentConsumption 67 | 68 | productivity = Productivity() 69 | optimization_problem.add_objective( 70 | productivity, 71 | n_objectives=2, 72 | requires=[process_simulator, frac_opt], 73 | minimize=False, 74 | ) 75 | 76 | recovery = Recovery() 77 | optimization_problem.add_objective( 78 | recovery, 79 | n_objectives=2, 80 | requires=[process_simulator, frac_opt], 81 | minimize=False, 82 | ) 83 | 84 | eluent_consumption = EluentConsumption() 85 | optimization_problem.add_objective( 86 | eluent_consumption, 87 | n_objectives=2, 88 | requires=[process_simulator, frac_opt], 89 | minimize=False, 90 | ) 91 | 92 | 93 | # %% [markdown] 94 | # ## Add callback for post-processing 95 | 96 | # %% 97 | def callback(fractionation, individual, evaluation_object, callbacks_dir): 98 | fractionation.plot_fraction_signal( 99 | file_name=f'{callbacks_dir}/{individual.id}_{evaluation_object}_fractionation.png', 100 | show=False 101 | ) 102 | 103 | 104 | optimization_problem.add_callback( 105 | callback, requires=[process_simulator, frac_opt] 106 | ) 107 | 108 | # %% [markdown] 109 | # ## Configure Optimizer 110 | 111 | # %% 112 | from CADETProcess.optimization import U_NSGA3 113 | optimizer = U_NSGA3() 114 | 115 | # %% [markdown] 116 | # ## Run Optimization 117 | # 118 | # ```{note} 119 | # For performance reasons, the optimization is currently not run when building the documentation. 120 | # In future, we will try to sideload pre-computed results to also discuss them here. 121 | # ``` 122 | # 123 | # ``` 124 | # if __name__ == '__main__': 125 | # results = optimizer.optimize( 126 | # optimization_problem, 127 | # use_checkpoint=True, 128 | # ) 129 | # ``` 130 | -------------------------------------------------------------------------------- /tests/test_cache.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from CADETProcess.optimization import ResultsCache 4 | 5 | 6 | class TestCache(unittest.TestCase): 7 | def setUp(self): 8 | self.cache_dict = ResultsCache(use_diskcache=False) 9 | self.cache_disk = ResultsCache(use_diskcache=True) 10 | 11 | key = (None, "objective", str([1, 2, 3])) 12 | result = "important result" 13 | self.cache_dict.set(key, result) 14 | self.cache_disk.set(key, result) 15 | 16 | key = (None, "intermediate", str([1, 2, 3])) 17 | result = "temporary result" 18 | self.cache_dict.set(key, result, "temp") 19 | self.cache_disk.set(key, result, "temp") 20 | 21 | def tearDown(self): 22 | self.cache_disk.delete_database() 23 | 24 | def test_set(self): 25 | new_result = "new" 26 | key = (None, "other", str([1, 2, 3])) 27 | 28 | self.cache_dict.set(key, new_result) 29 | cached_result = self.cache_dict.get(key) 30 | self.assertEqual(cached_result, new_result) 31 | 32 | self.cache_disk.set(key, new_result) 33 | cached_result = self.cache_disk.get(key) 34 | self.assertEqual(cached_result, new_result) 35 | 36 | def test_get(self): 37 | key = (None, "intermediate", str([1, 2, 3])) 38 | result_expected = "temporary result" 39 | cached_result = self.cache_dict.get(key) 40 | self.assertEqual(result_expected, cached_result) 41 | 42 | key = (None, "intermediate", str([1, 2, 3])) 43 | result_expected = "temporary result" 44 | cached_result = self.cache_disk.get(key) 45 | self.assertEqual(result_expected, cached_result) 46 | 47 | key = (None, "false", str([1, 2, 3])) 48 | with self.assertRaises(KeyError): 49 | cached_result = self.cache_dict.get(key) 50 | 51 | with self.assertRaises(KeyError): 52 | cached_result = self.cache_disk.get(key) 53 | 54 | def test_delete(self): 55 | key = (None, "intermediate", str([1, 2, 3])) 56 | self.cache_dict.delete(key) 57 | with self.assertRaises(KeyError): 58 | cached_result = self.cache_dict.get(key) 59 | 60 | self.cache_disk.delete(key) 61 | with self.assertRaises(KeyError): 62 | cached_result = self.cache_disk.get(key) 63 | 64 | def test_tags(self): 65 | tags_expected = ["temp"] 66 | tags = list(self.cache_dict.tags.keys()) 67 | self.assertEqual(tags_expected, tags) 68 | 69 | key = (None, "other", str([1, 2, 4])) 70 | self.cache_dict.set(key, "new", "foo") 71 | tags_expected = ["temp", "foo"] 72 | tags = list(self.cache_dict.tags.keys()) 73 | self.assertEqual(tags_expected, tags) 74 | 75 | def test_prune(self): 76 | key = (None, "intermediate", str([1, 2, 3])) 77 | self.cache_dict.prune("temp") 78 | with self.assertRaises(KeyError): 79 | cached_result = self.cache_dict.get(key) 80 | 81 | key = (None, "objective", str([1, 2, 3])) 82 | result_expected = "important result" 83 | cached_result = self.cache_dict.get(key) 84 | self.assertEqual(result_expected, cached_result) 85 | 86 | key = (None, "intermediate", str([1, 2, 3])) 87 | self.cache_disk.prune("temp") 88 | with self.assertRaises(KeyError): 89 | cached_result = self.cache_disk.get(key) 90 | 91 | key = (None, "objective", str([1, 2, 3])) 92 | result_expected = "important result" 93 | cached_result = self.cache_disk.get(key) 94 | self.assertEqual(result_expected, cached_result) 95 | 96 | def test_delete_database(self): 97 | pass 98 | 99 | 100 | if __name__ == "__main__": 101 | unittest.main() 102 | -------------------------------------------------------------------------------- /examples/load_wash_elute/lwe_concentration.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | formats: md:myst,py:percent 4 | text_representation: 5 | extension: .md 6 | format_name: myst 7 | format_version: 0.13 8 | jupytext_version: 1.17.1 9 | kernelspec: 10 | display_name: Python 3 11 | language: python 12 | name: python3 13 | --- 14 | 15 | (lwe_example_concentration)= 16 | # Concentration Gradients 17 | 18 | ```{figure} ./figures/flow_sheet_concentration.svg 19 | Flow sheet for load-wash-elute process using a single inlet. 20 | ``` 21 | 22 | ```{code-cell} 23 | import numpy as np 24 | 25 | from CADETProcess.processModel import ComponentSystem 26 | from CADETProcess.processModel import StericMassAction 27 | from CADETProcess.processModel import Inlet, GeneralRateModel, Outlet 28 | from CADETProcess.processModel import FlowSheet 29 | from CADETProcess.processModel import Process 30 | 31 | # Component System 32 | component_system = ComponentSystem() 33 | component_system.add_component('Salt') 34 | component_system.add_component('A') 35 | component_system.add_component('B') 36 | component_system.add_component('C') 37 | 38 | # Binding Model 39 | binding_model = StericMassAction(component_system, name='SMA') 40 | binding_model.is_kinetic = True 41 | binding_model.adsorption_rate = [0.0, 35.5, 1.59, 7.7] 42 | binding_model.desorption_rate = [0.0, 1000, 1000, 1000] 43 | binding_model.characteristic_charge = [0.0, 4.7, 5.29, 3.7] 44 | binding_model.steric_factor = [0.0, 11.83, 10.6, 10] 45 | binding_model.capacity = 1200.0 46 | 47 | # Unit Operations 48 | inlet = Inlet(component_system, name='inlet') 49 | inlet.flow_rate = 6.683738370512285e-8 50 | 51 | column = GeneralRateModel(component_system, name='column') 52 | column.binding_model = binding_model 53 | 54 | column.length = 0.014 55 | column.diameter = 0.02 56 | column.bed_porosity = 0.37 57 | column.particle_radius = 4.5e-5 58 | column.particle_porosity = 0.75 59 | column.axial_dispersion = 5.75e-8 60 | column.film_diffusion = column.n_comp * [6.9e-6] 61 | column.pore_diffusion = [7e-10, 6.07e-11, 6.07e-11, 6.07e-11] 62 | column.surface_diffusion = column.n_bound_states * [0.0] 63 | 64 | column.c = [50, 0, 0, 0] 65 | column.cp = [50, 0, 0, 0] 66 | column.q = [binding_model.capacity, 0, 0, 0] 67 | 68 | outlet = Outlet(component_system, name='outlet') 69 | 70 | # Flow Sheet 71 | flow_sheet = FlowSheet(component_system) 72 | 73 | flow_sheet.add_unit(inlet) 74 | flow_sheet.add_unit(column) 75 | flow_sheet.add_unit(outlet, product_outlet=True) 76 | 77 | flow_sheet.add_connection(inlet, column) 78 | flow_sheet.add_connection(column, outlet) 79 | ``` 80 | 81 | ```{figure} ./figures/events_concentration.svg 82 | Events of load-wash-elute process using a single inlet and modifying its concentration. 83 | ``` 84 | 85 | ```{code-cell} 86 | # Process 87 | process = Process(flow_sheet, 'lwe') 88 | process.cycle_time = 2000.0 89 | 90 | load_duration = 9 91 | t_gradient_start = 90.0 92 | gradient_duration = process.cycle_time - t_gradient_start 93 | 94 | c_load = np.array([50.0, 1.0, 1.0, 1.0]) 95 | c_wash = np.array([50.0, 0.0, 0.0, 0.0]) 96 | c_elute = np.array([500.0, 0.0, 0.0, 0.0]) 97 | gradient_slope = (c_elute - c_wash) / gradient_duration 98 | c_gradient_poly = np.array(list(zip(c_wash, gradient_slope))) 99 | 100 | process.add_event('load', 'flow_sheet.inlet.c', c_load) 101 | process.add_event('wash', 'flow_sheet.inlet.c', c_wash, load_duration) 102 | process.add_event('grad_start', 'flow_sheet.inlet.c', c_gradient_poly, t_gradient_start) 103 | ``` 104 | 105 | ```{code-cell} 106 | if __name__ == '__main__': 107 | from CADETProcess.simulator import Cadet 108 | process_simulator = Cadet() 109 | 110 | simulation_results = process_simulator.simulate(process) 111 | 112 | from CADETProcess.plotting import SecondaryAxis 113 | sec = SecondaryAxis() 114 | sec.components = ['Salt'] 115 | sec.y_label = '$c_{salt}$' 116 | 117 | simulation_results.solution.column.outlet.plot(secondary_axis=sec) 118 | ``` 119 | -------------------------------------------------------------------------------- /examples/load_wash_elute/lwe_concentration.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: md:myst,py:percent 5 | # text_representation: 6 | # extension: .py 7 | # format_name: percent 8 | # format_version: '1.3' 9 | # jupytext_version: 1.17.1 10 | # kernelspec: 11 | # display_name: Python 3 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # %% [markdown] 17 | # (lwe_example_concentration)= 18 | # # Concentration Gradients 19 | # 20 | # ```{figure} ./figures/flow_sheet_concentration.svg 21 | # Flow sheet for load-wash-elute process using a single inlet. 22 | # ``` 23 | 24 | # %% 25 | import numpy as np 26 | 27 | from CADETProcess.processModel import ComponentSystem 28 | from CADETProcess.processModel import StericMassAction 29 | from CADETProcess.processModel import Inlet, GeneralRateModel, Outlet 30 | from CADETProcess.processModel import FlowSheet 31 | from CADETProcess.processModel import Process 32 | 33 | # Component System 34 | component_system = ComponentSystem() 35 | component_system.add_component('Salt') 36 | component_system.add_component('A') 37 | component_system.add_component('B') 38 | component_system.add_component('C') 39 | 40 | # Binding Model 41 | binding_model = StericMassAction(component_system, name='SMA') 42 | binding_model.is_kinetic = True 43 | binding_model.adsorption_rate = [0.0, 35.5, 1.59, 7.7] 44 | binding_model.desorption_rate = [0.0, 1000, 1000, 1000] 45 | binding_model.characteristic_charge = [0.0, 4.7, 5.29, 3.7] 46 | binding_model.steric_factor = [0.0, 11.83, 10.6, 10] 47 | binding_model.capacity = 1200.0 48 | 49 | # Unit Operations 50 | inlet = Inlet(component_system, name='inlet') 51 | inlet.flow_rate = 6.683738370512285e-8 52 | 53 | column = GeneralRateModel(component_system, name='column') 54 | column.binding_model = binding_model 55 | 56 | column.length = 0.014 57 | column.diameter = 0.02 58 | column.bed_porosity = 0.37 59 | column.particle_radius = 4.5e-5 60 | column.particle_porosity = 0.75 61 | column.axial_dispersion = 5.75e-8 62 | column.film_diffusion = column.n_comp * [6.9e-6] 63 | column.pore_diffusion = [7e-10, 6.07e-11, 6.07e-11, 6.07e-11] 64 | column.surface_diffusion = column.n_bound_states * [0.0] 65 | 66 | column.c = [50, 0, 0, 0] 67 | column.cp = [50, 0, 0, 0] 68 | column.q = [binding_model.capacity, 0, 0, 0] 69 | 70 | outlet = Outlet(component_system, name='outlet') 71 | 72 | # Flow Sheet 73 | flow_sheet = FlowSheet(component_system) 74 | 75 | flow_sheet.add_unit(inlet) 76 | flow_sheet.add_unit(column) 77 | flow_sheet.add_unit(outlet, product_outlet=True) 78 | 79 | flow_sheet.add_connection(inlet, column) 80 | flow_sheet.add_connection(column, outlet) 81 | 82 | # %% [markdown] 83 | # ```{figure} ./figures/events_concentration.svg 84 | # Events of load-wash-elute process using a single inlet and modifying its concentration. 85 | # ``` 86 | 87 | # %% 88 | # Process 89 | process = Process(flow_sheet, 'lwe') 90 | process.cycle_time = 2000.0 91 | 92 | load_duration = 9 93 | t_gradient_start = 90.0 94 | gradient_duration = process.cycle_time - t_gradient_start 95 | 96 | c_load = np.array([50.0, 1.0, 1.0, 1.0]) 97 | c_wash = np.array([50.0, 0.0, 0.0, 0.0]) 98 | c_elute = np.array([500.0, 0.0, 0.0, 0.0]) 99 | gradient_slope = (c_elute - c_wash) / gradient_duration 100 | c_gradient_poly = np.array(list(zip(c_wash, gradient_slope))) 101 | 102 | process.add_event('load', 'flow_sheet.inlet.c', c_load) 103 | process.add_event('wash', 'flow_sheet.inlet.c', c_wash, load_duration) 104 | process.add_event('grad_start', 'flow_sheet.inlet.c', c_gradient_poly, t_gradient_start) 105 | 106 | # %% 107 | if __name__ == '__main__': 108 | from CADETProcess.simulator import Cadet 109 | process_simulator = Cadet() 110 | 111 | simulation_results = process_simulator.simulate(process) 112 | 113 | from CADETProcess.plotting import SecondaryAxis 114 | sec = SecondaryAxis() 115 | sec.components = ['Salt'] 116 | sec.y_label = '$c_{salt}$' 117 | 118 | simulation_results.solution.column.outlet.plot(secondary_axis=sec) 119 | -------------------------------------------------------------------------------- /CADETProcess/modelBuilder/batchElutionBuilder.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from CADETProcess.processModel import ( 4 | ChromatographicColumnBase, 5 | FlowSheet, 6 | Inlet, 7 | Outlet, 8 | Process, 9 | ) 10 | 11 | 12 | class BatchElution(Process): 13 | """ 14 | Batch elution process. 15 | 16 | The flowsheet is configured with the following unit operations: 17 | - feed: Inlet 18 | - eluent: Inlet 19 | - column: ChromatographicColumnBase 20 | - outlet: Outlet 21 | 22 | The following durations/events are configured: 23 | - `feed_duration`: The length of the feed injection. 24 | - `feed_on`: Set `feed.flow_rate` to `flow_rate`. 25 | - `eluent_off`: Set `eluent.flow_rate` to 0.0; triggered by `feed_on`. 26 | - `feed_off`: Set `feed.flow_rate` to 0.0; triggered by `feed_on` and `feed_duration`. 27 | - `eluent_on`: Set `eluent.flow_rate` to `flow_rate`; triggered by `feed_off`. 28 | """ 29 | 30 | def __init__( 31 | self, 32 | column: ChromatographicColumnBase, 33 | c_feed: list[float], 34 | flow_rate: float, 35 | feed_duration: float, 36 | cycle_time: float, 37 | c_eluent: Optional[list[float] | float] = 0.0, 38 | ) -> None: 39 | """ 40 | Initialize batch elution process. 41 | 42 | Parameters 43 | ---------- 44 | column : ChromatographicColumnBase 45 | Chromatographic column object to be used in process. 46 | c_feed : list[float] 47 | Feed concentration. 48 | flow_rate : float 49 | Flow rate. 50 | feed_duration : float 51 | Feed duration. 52 | cycle_time : float 53 | Cycle time. 54 | c_eluent : list[float] | float, optional 55 | Eluent concentration. The default is 0.0. 56 | """ 57 | if not isinstance(column, ChromatographicColumnBase): 58 | raise TypeError("Expected ChromatographicColumnBase.") 59 | 60 | flow_sheet = self._build_flow_sheet( 61 | column, 62 | c_feed, 63 | c_eluent, 64 | ) 65 | 66 | super().__init__(flow_sheet, "Batch Elution") 67 | self.cycle_time = cycle_time 68 | 69 | # Durations 70 | self.add_duration("feed_duration", feed_duration) 71 | 72 | # Injection 73 | self.add_event("feed_on", "flow_sheet.feed.flow_rate", flow_rate) 74 | self.add_event("eluent_off", "flow_sheet.eluent.flow_rate", 0.0) 75 | self.add_event_dependency("eluent_off", ["feed_on"]) 76 | 77 | # Elution 78 | self.add_event("feed_off", "flow_sheet.feed.flow_rate", 0.0) 79 | self.add_event_dependency("feed_off", ["feed_on", "feed_duration"], [1, 1]) 80 | self.add_event("eluent_on", "flow_sheet.eluent.flow_rate", flow_rate) 81 | self.add_event_dependency("eluent_on", ["feed_off"]) 82 | 83 | def _build_flow_sheet( 84 | self, 85 | column: ChromatographicColumnBase, 86 | c_feed: list[float], 87 | c_eluent: list[float] | float | None = 0.0, 88 | ) -> FlowSheet: 89 | """Build and return the flow sheet for batch elution process.""" 90 | component_system = column.component_system 91 | 92 | # Unit Operations 93 | feed = Inlet(component_system, name="feed") 94 | feed.c = c_feed 95 | 96 | eluent = Inlet(component_system, name="eluent") 97 | eluent.c = c_eluent 98 | 99 | outlet = Outlet(component_system, name="outlet") 100 | 101 | # Flow Sheet 102 | flow_sheet = FlowSheet(component_system) 103 | 104 | flow_sheet.add_unit(feed, feed_inlet=True) 105 | flow_sheet.add_unit(eluent, eluent_inlet=True) 106 | flow_sheet.add_unit(column) 107 | flow_sheet.add_unit(outlet, product_outlet=True) 108 | 109 | flow_sheet.add_connection(feed, column) 110 | flow_sheet.add_connection(eluent, column) 111 | flow_sheet.add_connection(column, outlet) 112 | 113 | return flow_sheet 114 | -------------------------------------------------------------------------------- /examples/load_wash_elute/lwe_hic.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | formats: md:myst,py:percent 4 | text_representation: 5 | extension: .md 6 | format_name: myst 7 | format_version: 0.13 8 | jupytext_version: 1.17.1 9 | kernelspec: 10 | display_name: Python 3 11 | language: python 12 | name: python3 13 | --- 14 | 15 | (lwe_example_concentration)= 16 | # Concentration Gradients 17 | 18 | ```{figure} ./figures/flow_sheet_concentration.svg 19 | Flow sheet for load-wash-elute process using a single inlet. 20 | ``` 21 | 22 | ```{code-cell} 23 | import numpy as np 24 | 25 | import matplotlib 26 | matplotlib.use("TkAgg") 27 | 28 | from CADETProcess.processModel import ComponentSystem, HICConstantWaterActivity 29 | from CADETProcess.processModel import Inlet, GeneralRateModel, Outlet 30 | from CADETProcess.processModel import FlowSheet 31 | from CADETProcess.processModel import Process 32 | 33 | # Component System 34 | component_system = ComponentSystem() 35 | component_system.add_component('Salt') 36 | component_system.add_component('A') 37 | component_system.add_component('B') 38 | component_system.add_component('C') 39 | 40 | # Binding Model 41 | binding_model = HICConstantWaterActivity(component_system, name='HIC_SWA') 42 | # binding_model = HICWaterOnHydrophobicSurfaces(component_system, name='HIC_WHS') 43 | binding_model.is_kinetic = True 44 | binding_model.adsorption_rate = [0.0, 0.7, 1, 200] 45 | binding_model.desorption_rate = [0.0, 1000, 1000, 1000] 46 | binding_model.hic_characteristic = [0.0, 10, 13, 4] 47 | binding_model.capacity = [0.0, 10000000, 10000000, 10000000] 48 | binding_model.beta_0 = 10. ** -0.5 49 | binding_model.beta_1 = 10. ** -3.65 50 | binding_model.bound_states = [0, 1, 1, 1] 51 | 52 | # Unit Operations 53 | inlet = Inlet(component_system, name='inlet') 54 | inlet.flow_rate = 6.683738370512285e-8 55 | 56 | column = GeneralRateModel(component_system, name='column') 57 | column.binding_model = binding_model 58 | 59 | column.length = 0.014 60 | column.diameter = 0.02 61 | column.bed_porosity = 0.37 62 | column.particle_radius = 4.5e-5 63 | column.particle_porosity = 0.75 64 | column.axial_dispersion = 5.75e-8 65 | column.film_diffusion = column.n_comp * [6.9e-6] 66 | column.pore_diffusion = [7e-10, 6.07e-11, 6.07e-11, 6.07e-11] 67 | column.surface_diffusion = column.n_bound_states * [0.0] 68 | 69 | salt_gradient_start_concentration = 3000 70 | salt_gradient_end_concentration = 50 71 | 72 | column.c = [salt_gradient_start_concentration, 0, 0, 0] 73 | column.cp = [salt_gradient_start_concentration, 0, 0, 0] 74 | column.q = [0, 0, 0] 75 | 76 | outlet = Outlet(component_system, name='outlet') 77 | 78 | # Flow Sheet 79 | flow_sheet = FlowSheet(component_system) 80 | 81 | flow_sheet.add_unit(inlet) 82 | flow_sheet.add_unit(column) 83 | flow_sheet.add_unit(outlet, product_outlet=True) 84 | 85 | flow_sheet.add_connection(inlet, column) 86 | flow_sheet.add_connection(column, outlet) 87 | ``` 88 | 89 | ```{figure} ./figures/events_concentration.svg 90 | Events of load-wash-elute process using a single inlet and modifying its concentration. 91 | ``` 92 | 93 | ```{code-cell} 94 | # Process 95 | process = Process(flow_sheet, 'lwe') 96 | process.cycle_time = 2000.0 97 | 98 | load_duration = 9 99 | t_gradient_start = 90.0 100 | gradient_duration = process.cycle_time - t_gradient_start 101 | 102 | c_load = np.array([salt_gradient_start_concentration, 1.0, 1.0, 1.0]) 103 | c_wash = np.array([salt_gradient_start_concentration, 0.0, 0.0, 0.0]) 104 | c_elute = np.array([salt_gradient_end_concentration, 0.0, 0.0, 0.0]) 105 | gradient_slope = (c_elute - c_wash) / gradient_duration 106 | c_gradient_poly = np.array(list(zip(c_wash, gradient_slope))) 107 | 108 | process.add_event('load', 'flow_sheet.inlet.c', c_load) 109 | process.add_event('wash', 'flow_sheet.inlet.c', c_wash, load_duration) 110 | process.add_event('grad_start', 'flow_sheet.inlet.c', c_gradient_poly, t_gradient_start) 111 | ``` 112 | 113 | ```{code-cell} 114 | if __name__ == '__main__': 115 | from CADETProcess.simulator import Cadet 116 | 117 | process_simulator = Cadet() 118 | 119 | simulation_results = process_simulator.simulate(process) 120 | 121 | from CADETProcess.plotting import SecondaryAxis 122 | 123 | sec = SecondaryAxis() 124 | sec.components = ['Salt'] 125 | sec.y_label = '$c_{salt}$' 126 | 127 | simulation_results.solution.column.outlet.plot(secondary_axis=sec) 128 | ``` 129 | -------------------------------------------------------------------------------- /examples/load_wash_elute/lwe_hic.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: md:myst,py:percent 5 | # text_representation: 6 | # extension: .py 7 | # format_name: percent 8 | # format_version: '1.3' 9 | # jupytext_version: 1.17.1 10 | # kernelspec: 11 | # display_name: Python 3 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # %% [markdown] 17 | # (lwe_example_concentration)= 18 | # # Concentration Gradients 19 | # 20 | # ```{figure} ./figures/flow_sheet_concentration.svg 21 | # Flow sheet for load-wash-elute process using a single inlet. 22 | # ``` 23 | 24 | # %% 25 | import numpy as np 26 | 27 | import matplotlib 28 | matplotlib.use("TkAgg") 29 | 30 | from CADETProcess.processModel import ComponentSystem, HICConstantWaterActivity 31 | from CADETProcess.processModel import Inlet, GeneralRateModel, Outlet 32 | from CADETProcess.processModel import FlowSheet 33 | from CADETProcess.processModel import Process 34 | 35 | # Component System 36 | component_system = ComponentSystem() 37 | component_system.add_component('Salt') 38 | component_system.add_component('A') 39 | component_system.add_component('B') 40 | component_system.add_component('C') 41 | 42 | # Binding Model 43 | binding_model = HICConstantWaterActivity(component_system, name='HIC_SWA') 44 | # binding_model = HICWaterOnHydrophobicSurfaces(component_system, name='HIC_WHS') 45 | binding_model.is_kinetic = True 46 | binding_model.adsorption_rate = [0.0, 0.7, 1, 200] 47 | binding_model.desorption_rate = [0.0, 1000, 1000, 1000] 48 | binding_model.hic_characteristic = [0.0, 10, 13, 4] 49 | binding_model.capacity = [0.0, 10000000, 10000000, 10000000] 50 | binding_model.beta_0 = 10. ** -0.5 51 | binding_model.beta_1 = 10. ** -3.65 52 | binding_model.bound_states = [0, 1, 1, 1] 53 | 54 | # Unit Operations 55 | inlet = Inlet(component_system, name='inlet') 56 | inlet.flow_rate = 6.683738370512285e-8 57 | 58 | column = GeneralRateModel(component_system, name='column') 59 | column.binding_model = binding_model 60 | 61 | column.length = 0.014 62 | column.diameter = 0.02 63 | column.bed_porosity = 0.37 64 | column.particle_radius = 4.5e-5 65 | column.particle_porosity = 0.75 66 | column.axial_dispersion = 5.75e-8 67 | column.film_diffusion = column.n_comp * [6.9e-6] 68 | column.pore_diffusion = [7e-10, 6.07e-11, 6.07e-11, 6.07e-11] 69 | column.surface_diffusion = column.n_bound_states * [0.0] 70 | 71 | salt_gradient_start_concentration = 3000 72 | salt_gradient_end_concentration = 50 73 | 74 | column.c = [salt_gradient_start_concentration, 0, 0, 0] 75 | column.cp = [salt_gradient_start_concentration, 0, 0, 0] 76 | column.q = [0, 0, 0] 77 | 78 | outlet = Outlet(component_system, name='outlet') 79 | 80 | # Flow Sheet 81 | flow_sheet = FlowSheet(component_system) 82 | 83 | flow_sheet.add_unit(inlet) 84 | flow_sheet.add_unit(column) 85 | flow_sheet.add_unit(outlet, product_outlet=True) 86 | 87 | flow_sheet.add_connection(inlet, column) 88 | flow_sheet.add_connection(column, outlet) 89 | 90 | # %% [markdown] 91 | # ```{figure} ./figures/events_concentration.svg 92 | # Events of load-wash-elute process using a single inlet and modifying its concentration. 93 | # ``` 94 | 95 | # %% 96 | # Process 97 | process = Process(flow_sheet, 'lwe') 98 | process.cycle_time = 2000.0 99 | 100 | load_duration = 9 101 | t_gradient_start = 90.0 102 | gradient_duration = process.cycle_time - t_gradient_start 103 | 104 | c_load = np.array([salt_gradient_start_concentration, 1.0, 1.0, 1.0]) 105 | c_wash = np.array([salt_gradient_start_concentration, 0.0, 0.0, 0.0]) 106 | c_elute = np.array([salt_gradient_end_concentration, 0.0, 0.0, 0.0]) 107 | gradient_slope = (c_elute - c_wash) / gradient_duration 108 | c_gradient_poly = np.array(list(zip(c_wash, gradient_slope))) 109 | 110 | process.add_event('load', 'flow_sheet.inlet.c', c_load) 111 | process.add_event('wash', 'flow_sheet.inlet.c', c_wash, load_duration) 112 | process.add_event('grad_start', 'flow_sheet.inlet.c', c_gradient_poly, t_gradient_start) 113 | 114 | # %% 115 | if __name__ == '__main__': 116 | from CADETProcess.simulator import Cadet 117 | 118 | process_simulator = Cadet() 119 | 120 | simulation_results = process_simulator.simulate(process) 121 | 122 | from CADETProcess.plotting import SecondaryAxis 123 | 124 | sec = SecondaryAxis() 125 | sec.components = ['Salt'] 126 | sec.y_label = '$c_{salt}$' 127 | 128 | simulation_results.solution.column.outlet.plot(secondary_axis=sec) 129 | -------------------------------------------------------------------------------- /tests/test_transform.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from CADETProcess.transform import ( 4 | AutoTransformer, 5 | NormLinearTransformer, 6 | NormLogTransformer, 7 | NullTransformer, 8 | ) 9 | 10 | 11 | class Test_Transformer(unittest.TestCase): 12 | def test_input_range(self): 13 | transform = NormLinearTransformer(0, 100) 14 | 15 | with self.assertRaises(ValueError): 16 | in_ = -10 17 | out = transform.transform(in_) 18 | 19 | with self.assertRaises(ValueError): 20 | in_ = 1000 21 | out = transform.transform(in_) 22 | 23 | def test_output_range(self): 24 | transform = NormLinearTransformer(0, 100) 25 | 26 | with self.assertRaises(ValueError): 27 | in_ = -1 28 | out = transform.untransform(in_) 29 | 30 | with self.assertRaises(ValueError): 31 | in_ = 2 32 | out = transform.untransform(in_) 33 | 34 | def test_no_transform(self): 35 | transform = NullTransformer(0, 100) 36 | self.assertAlmostEqual(transform.lb, 0) 37 | self.assertAlmostEqual(transform.ub, 100) 38 | 39 | in_ = 0 40 | out_expected = 0 41 | out = transform.transform(in_) 42 | self.assertAlmostEqual(out_expected, out) 43 | 44 | in_ = 0 45 | out_expected = 0 46 | out = transform.untransform(in_) 47 | self.assertAlmostEqual(out_expected, out) 48 | 49 | def test_linear(self): 50 | transform = NormLinearTransformer(0, 100) 51 | self.assertAlmostEqual(transform.lb, 0) 52 | self.assertAlmostEqual(transform.ub, 1) 53 | 54 | in_ = 0 55 | out_expected = 0 56 | out = transform.transform(in_) 57 | self.assertAlmostEqual(out_expected, out) 58 | 59 | in_ = 0 60 | out_expected = 0 61 | out = transform.untransform(in_) 62 | self.assertAlmostEqual(out_expected, out) 63 | 64 | in_ = 10 65 | out_expected = 0.1 66 | out = transform.transform(in_) 67 | self.assertAlmostEqual(out_expected, out) 68 | 69 | in_ = 0.1 70 | out_expected = 10 71 | out = transform.untransform(in_) 72 | self.assertAlmostEqual(out_expected, out) 73 | 74 | in_ = 100 75 | out_expected = 1 76 | out = transform.transform(in_) 77 | self.assertAlmostEqual(out_expected, out) 78 | 79 | in_ = 1 80 | out_expected = 100 81 | out = transform.untransform(in_) 82 | self.assertAlmostEqual(out_expected, out) 83 | 84 | def test_log(self): 85 | """Missing: Special case for lb_input <= 0""" 86 | transform = NormLogTransformer(1, 1000) 87 | self.assertAlmostEqual(transform.lb, 0) 88 | self.assertAlmostEqual(transform.ub, 1) 89 | 90 | in_ = 1 91 | out_expected = 0 92 | out = transform.transform(in_) 93 | self.assertAlmostEqual(out_expected, out) 94 | 95 | in_ = 0 96 | out_expected = 1 97 | out = transform.untransform(in_) 98 | self.assertAlmostEqual(out_expected, out) 99 | 100 | in_ = 10 101 | out_expected = 1 / 3 102 | out = transform.transform(in_) 103 | self.assertAlmostEqual(out_expected, out) 104 | 105 | in_ = 1 / 3 106 | out_expected = 10 107 | out = transform.untransform(in_) 108 | self.assertAlmostEqual(out_expected, out) 109 | 110 | in_ = 100 111 | out_expected = 2 / 3 112 | out = transform.transform(in_) 113 | self.assertAlmostEqual(out_expected, out) 114 | 115 | in_ = 2 / 3 116 | out_expected = 100 117 | out = transform.untransform(in_) 118 | self.assertAlmostEqual(out_expected, out) 119 | 120 | in_ = 1000 121 | out_expected = 1 122 | out = transform.transform(in_) 123 | self.assertAlmostEqual(out_expected, out) 124 | 125 | in_ = 1 126 | out_expected = 1000 127 | out = transform.untransform(in_) 128 | self.assertAlmostEqual(out_expected, out) 129 | 130 | def test_auto(self): 131 | threshold = 1000 132 | 133 | transform = AutoTransformer(1, 100, threshold=threshold) 134 | self.assertTrue(transform.use_linear) 135 | self.assertFalse(transform.use_log) 136 | 137 | # Expect Log behaviour 138 | transform = AutoTransformer(1, 1001, threshold=threshold) 139 | self.assertFalse(transform.use_linear) 140 | self.assertTrue(transform.use_log) 141 | 142 | 143 | if __name__ == "__main__": 144 | unittest.main() 145 | -------------------------------------------------------------------------------- /tests/dataStructure/test_nested_dict.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import pytest 4 | from CADETProcess.dataStructure.nested_dict import ( 5 | check_nested, 6 | generate_nested_dict, 7 | get_leaves, 8 | get_nested_attribute, 9 | get_nested_list_value, 10 | get_nested_value, 11 | insert_path, 12 | set_nested_attribute, 13 | set_nested_list_value, 14 | set_nested_value, 15 | update_dict_recursively, 16 | ) 17 | 18 | 19 | @pytest.fixture 20 | def sample_dict(): 21 | """Fixture providing a sample nested dictionary.""" 22 | return { 23 | "a": { 24 | "b": { 25 | "c": 42, 26 | }, 27 | }, 28 | "x": { 29 | "y": { 30 | "z": 99, 31 | }, 32 | }, 33 | } 34 | 35 | 36 | @pytest.fixture 37 | def sample_list(): 38 | """Fixture providing a sample nested list.""" 39 | return [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] 40 | 41 | 42 | # --- TESTS FOR NESTED DICTIONARY FUNCTIONS --- # 43 | 44 | 45 | def test_check_nested(sample_dict): 46 | assert check_nested(sample_dict, "a.b.c") is True 47 | assert check_nested(sample_dict, "a.b") is False 48 | assert check_nested(sample_dict, "x.y.z") is True 49 | assert check_nested(sample_dict, "missing.path") is False 50 | 51 | 52 | def test_generate_nested_dict(): 53 | expected = {"a": {"b": {"c": 10}}} 54 | assert generate_nested_dict("a.b.c", 10) == expected 55 | 56 | 57 | def test_insert_path(sample_dict): 58 | insert_path(sample_dict, "a.b.d", 100) 59 | assert sample_dict["a"]["b"]["d"] == 100 60 | 61 | insert_path(sample_dict, "new.path.here", 50) 62 | assert sample_dict["new"]["path"]["here"] == 50 63 | 64 | 65 | def test_get_leaves(sample_dict): 66 | leaves = set(get_leaves(sample_dict)) 67 | assert leaves == {"a.b.c", "x.y.z"} 68 | 69 | 70 | def test_set_nested_value(sample_dict): 71 | set_nested_value(sample_dict, "a.b.c", 77) 72 | assert sample_dict["a"]["b"]["c"] == 77 73 | 74 | set_nested_value(sample_dict, "x.y.z", "test") 75 | assert sample_dict["x"]["y"]["z"] == "test" 76 | 77 | 78 | def test_get_nested_value(sample_dict): 79 | assert get_nested_value(sample_dict, "a.b.c") == 42 80 | assert get_nested_value(sample_dict, "x.y.z") == 99 81 | 82 | with pytest.raises(KeyError): 83 | get_nested_value(sample_dict, "missing.path") 84 | 85 | 86 | # --- TESTS FOR DICTIONARY UPDATING --- # 87 | 88 | 89 | def test_update_dict_recursively(sample_dict): 90 | target = {"a": {"b": 1}, "c": 3} 91 | updates = {"a": {"b": 2, "d": 4}, "c": 30, "e": 5} 92 | 93 | updated = update_dict_recursively(copy.deepcopy(target), updates) 94 | assert updated == {"a": {"b": 2, "d": 4}, "c": 30, "e": 5} 95 | 96 | updated_existing = update_dict_recursively( 97 | copy.deepcopy(target), updates, only_existing_keys=True 98 | ) 99 | assert updated_existing == {"a": {"b": 2}, "c": 30} 100 | 101 | 102 | # --- TESTS FOR OBJECT ATTRIBUTE FUNCTIONS --- # 103 | 104 | 105 | class SampleObject: 106 | def __init__(self): 107 | self.a = SampleSubObject() 108 | 109 | 110 | class SampleSubObject: 111 | def __init__(self): 112 | self.b = SampleInnerObject() 113 | 114 | 115 | class SampleInnerObject: 116 | def __init__(self): 117 | self.c = 42 118 | 119 | 120 | @pytest.fixture 121 | def sample_obj(): 122 | return SampleObject() 123 | 124 | 125 | def test_get_nested_attribute(sample_obj): 126 | assert get_nested_attribute(sample_obj, "a.b.c") == 42 127 | 128 | with pytest.raises(AttributeError): 129 | get_nested_attribute(sample_obj, "a.b.d") 130 | 131 | 132 | def test_set_nested_attribute(sample_obj): 133 | set_nested_attribute(sample_obj, "a.b.c", 99) 134 | assert sample_obj.a.b.c == 99 135 | 136 | with pytest.raises(AttributeError): 137 | set_nested_attribute(sample_obj, "a.c.b", 50) 138 | 139 | 140 | # --- TESTS FOR NESTED LIST FUNCTIONS --- # 141 | 142 | 143 | def test_get_nested_list_value(sample_list): 144 | assert get_nested_list_value(sample_list, (0, 1, 1)) == 4 145 | assert get_nested_list_value(sample_list, (1, 0, 1)) == 6 146 | 147 | with pytest.raises(IndexError): 148 | get_nested_list_value(sample_list, (3, 2, 1)) 149 | 150 | 151 | def test_set_nested_list_value(sample_list): 152 | set_nested_list_value(sample_list, (0, 1, 1), 99) 153 | assert sample_list[0][1][1] == 99 154 | 155 | with pytest.raises(IndexError): 156 | set_nested_list_value(sample_list, (3, 2, 1), 100) 157 | 158 | 159 | if __name__ == "__main__": 160 | pytest.main([__file__]) 161 | -------------------------------------------------------------------------------- /CADETProcess/equilibria/initial_conditions.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import numpy as np 4 | 5 | from CADETProcess import CADETProcessError 6 | from CADETProcess.processModel import ( 7 | BindingBaseClass, 8 | Cstr, 9 | FlowSheet, 10 | Inlet, 11 | LumpedRateModelWithoutPores, 12 | Outlet, 13 | Process, 14 | ) 15 | from CADETProcess.simulator import Cadet 16 | 17 | __all__ = ["simulate_solid_equilibria"] 18 | 19 | 20 | def simulate_solid_equilibria( 21 | binding_model: BindingBaseClass, 22 | buffer: list, 23 | unit_model: Optional[str] = "cstr", 24 | flush: Optional[list] = None, 25 | cadet_install_path: Optional[str] = None, 26 | ) -> list: 27 | """ 28 | Simulate initial conditions for solid phase for given buffer. 29 | 30 | Parameters 31 | ---------- 32 | binding_model : BindingBase 33 | Binding model describing relation between bulk and solif phase. 34 | buffer : list 35 | Buffer concentration in mM. 36 | unit_model : {'cstr', 'column'}, optional 37 | Unit model to be used in simulation. The default is 'cstr'. 38 | flush : list, optional 39 | Additional buffer for flushing column after loading. 40 | The default is None. 41 | cadet_install_path : str, optional 42 | Path to CADET installation. If None, the default CADET path is used. 43 | 44 | Raises 45 | ------ 46 | CADETProcessError 47 | DESCRIPTION. 48 | 49 | Returns 50 | ------- 51 | list 52 | Initial conditions for solid phase. 53 | """ 54 | process_name = flow_sheet_name = "initial_conditions" 55 | component_system = binding_model.component_system 56 | 57 | # Unit Operations 58 | buffer_source = Inlet(component_system, name="buffer") 59 | buffer_source.c = buffer 60 | 61 | if flush is None: 62 | flush = buffer 63 | flush_source = Inlet(component_system, "flush") 64 | flush_source.c = flush 65 | 66 | if unit_model == "cstr": 67 | unit = Cstr(component_system, "cstr") 68 | unit.init_liquid_volume = 5e-7 69 | unit.const_solid_volume = 5e-7 70 | 71 | Q = 1e-6 72 | cycle_time = np.round(1000 * unit.volume / Q) 73 | unit.flow_rate = Q 74 | elif unit_model == "column": 75 | unit = LumpedRateModelWithoutPores(component_system, name="column") 76 | unit.length = 0.1 77 | unit.diameter = 0.01 78 | unit.axial_dispersion = 1e-6 79 | unit.total_porosity = 0.7 80 | 81 | Q = 60 / (60 * 1e6) 82 | cycle_time = np.round(10 * unit.volume / Q) 83 | else: 84 | raise CADETProcessError("Unknown unit model") 85 | 86 | try: 87 | q = binding_model.n_comp * binding_model.n_states * [0] 88 | capacity = binding_model.capacity 89 | if isinstance(capacity, list): 90 | capacity = capacity[0] 91 | 92 | q[0] = capacity 93 | unit.q = q 94 | c = binding_model.n_comp * [0] 95 | c[0] = buffer[0] 96 | unit.c = c 97 | except AttributeError: 98 | pass 99 | 100 | unit.binding_model = binding_model 101 | 102 | unit.solution_recorder.write_solution_bulk = True 103 | unit.solution_recorder.write_solution_solid = True 104 | 105 | outlet = Outlet(component_system, name="outlet") 106 | 107 | # flow sheet 108 | fs = FlowSheet(component_system, name=flow_sheet_name) 109 | 110 | fs.add_unit(buffer_source) 111 | fs.add_unit(flush_source) 112 | fs.add_unit(unit) 113 | fs.add_unit(outlet, product_outlet=True) 114 | 115 | fs.add_connection(buffer_source, unit) 116 | fs.add_connection(flush_source, unit) 117 | fs.add_connection(unit, outlet) 118 | 119 | # Process 120 | proc = Process(fs, name=process_name) 121 | proc.cycle_time = cycle_time 122 | 123 | # Create Events and Durations 124 | proc.add_event("buffer_on", "flow_sheet.buffer.flow_rate", Q) 125 | proc.add_event("buffer_off", "flow_sheet.buffer.flow_rate", 0, 0.9 * cycle_time) 126 | 127 | proc.add_event("eluent_off", "flow_sheet.flush.flow_rate", 0.0, 0.0) 128 | proc.add_event("eluent_on", "flow_sheet.flush.flow_rate", Q, 0.9 * cycle_time) 129 | 130 | # Simulator 131 | process_simulator = Cadet(cadet_install_path) 132 | proc_results = process_simulator.simulate(proc) 133 | 134 | if unit_model == "cstr": 135 | init_q = proc_results.solution[unit.name].solid.solution[-1, :] 136 | elif unit_model == "column": 137 | init_q = proc_results.solution[unit.name].solid.solution[-1, 0, :] 138 | 139 | init_q = np.round(init_q, 14) 140 | return init_q.tolist() 141 | -------------------------------------------------------------------------------- /docs/source/user_guide/optimization/figures/single_evaluation_object.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
EvaluationObject
EvaluationObject
Variable
Variable
*v
*v
*e
*e
Text is not SVG - cannot display
4 | -------------------------------------------------------------------------------- /tests/test_fractions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import CADETProcess 4 | import numpy as np 5 | 6 | 7 | class Test_Fractions(unittest.TestCase): 8 | def create_fractions(self): 9 | m_0 = np.array([0, 0]) 10 | frac0 = CADETProcess.fractionation.Fraction(m_0, 1) 11 | m_1 = np.array([3, 0]) 12 | frac1 = CADETProcess.fractionation.Fraction(m_1, 2) 13 | m_2 = np.array([1, 2]) 14 | frac2 = CADETProcess.fractionation.Fraction(m_2, 3) 15 | m_3 = np.array([0, 2]) 16 | frac3 = CADETProcess.fractionation.Fraction(m_3, 3) 17 | m_4 = np.array([0, 0]) 18 | frac4 = CADETProcess.fractionation.Fraction(m_4, 0) 19 | 20 | return frac0, frac1, frac2, frac3, frac4 21 | 22 | def create_pools(self): 23 | fractions = self.create_fractions() 24 | 25 | pool_waste = CADETProcess.fractionation.FractionPool(n_comp=2) 26 | pool_waste.add_fraction(fractions[0]) 27 | pool_waste.add_fraction(fractions[1]) 28 | pool_waste.add_fraction(fractions[2]) 29 | 30 | pool_1 = CADETProcess.fractionation.FractionPool(n_comp=2) 31 | pool_1.add_fraction(fractions[3]) 32 | 33 | pool_2 = CADETProcess.fractionation.FractionPool(n_comp=2) 34 | pool_2.add_fraction(fractions[4]) 35 | 36 | return pool_waste, pool_1, pool_2 37 | 38 | def test_fraction_mass(self): 39 | fractions = self.create_fractions() 40 | 41 | self.assertEqual(fractions[0].fraction_mass, 0) 42 | self.assertEqual(fractions[1].fraction_mass, 3) 43 | self.assertEqual(fractions[2].fraction_mass, 3) 44 | self.assertEqual(fractions[3].fraction_mass, 2) 45 | self.assertEqual(fractions[4].fraction_mass, 0) 46 | 47 | def test_fraction_concentration(self): 48 | fractions = self.create_fractions() 49 | 50 | np.testing.assert_equal(fractions[0].concentration, np.array([0.0, 0.0])) 51 | np.testing.assert_equal(fractions[1].concentration, np.array([1.5, 0])) 52 | np.testing.assert_equal(fractions[2].concentration, np.array([1 / 3, 2 / 3])) 53 | np.testing.assert_equal(fractions[3].concentration, np.array([0, 2 / 3])) 54 | np.testing.assert_equal(fractions[4].concentration, np.array([0, 0])) 55 | 56 | def test_fraction_purity(self): 57 | fractions = self.create_fractions() 58 | 59 | np.testing.assert_equal(fractions[0].purity, np.array([0.0, 0.0])) 60 | np.testing.assert_equal(fractions[1].purity, np.array([1, 0])) 61 | np.testing.assert_equal(fractions[2].purity, np.array([1 / 3, 2 / 3])) 62 | np.testing.assert_equal(fractions[3].purity, np.array([0, 1])) 63 | np.testing.assert_equal(fractions[4].purity, np.array([0, 0])) 64 | 65 | def test_n_comp(self): 66 | pools = self.create_pools() 67 | 68 | self.assertEqual(pools[0].n_comp, 2) 69 | self.assertEqual(pools[0].fractions[0].n_comp, 2) 70 | 71 | m_wrong_n_comp = np.array([0, 0, 0]) 72 | frac_wrong_n_comp = CADETProcess.fractionation.Fraction(m_wrong_n_comp, 1) 73 | 74 | with self.assertRaises(CADETProcess.CADETProcessError): 75 | pools[0].add_fraction(frac_wrong_n_comp) 76 | 77 | def test_pool_mass(self): 78 | pools = self.create_pools() 79 | 80 | np.testing.assert_equal(pools[0].mass, np.array([4.0, 2.0])) 81 | np.testing.assert_equal(pools[1].mass, np.array([0, 2])) 82 | np.testing.assert_equal(pools[2].mass, np.array([0, 0])) 83 | 84 | def test_pool_pool_mass(self): 85 | pools = self.create_pools() 86 | 87 | self.assertEqual(pools[0].pool_mass, 6) 88 | self.assertEqual(pools[1].pool_mass, 2) 89 | self.assertEqual(pools[2].pool_mass, 0) 90 | 91 | def test_pool_volume(self): 92 | pools = self.create_pools() 93 | 94 | self.assertEqual(pools[0].volume, 6) 95 | self.assertEqual(pools[1].volume, 3) 96 | self.assertEqual(pools[2].volume, 0) 97 | 98 | def test_pool_concentration(self): 99 | pools = self.create_pools() 100 | 101 | np.testing.assert_equal(pools[0].concentration, np.array([2 / 3, 1 / 3])) 102 | np.testing.assert_equal(pools[1].concentration, np.array([0, 2 / 3])) 103 | np.testing.assert_equal(pools[2].concentration, np.array([0, 0])) 104 | 105 | def test_pool_purity(self): 106 | pools = self.create_pools() 107 | 108 | np.testing.assert_equal(pools[0].purity, np.array([2 / 3, 1 / 3])) 109 | np.testing.assert_equal(pools[1].purity, np.array([0, 1])) 110 | np.testing.assert_equal(pools[2].purity, np.array([0, 0])) 111 | 112 | 113 | if __name__ == "__main__": 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /docs/source/user_guide/optimization/figures/callbacks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Callback
Callback
XPareto
XPareto
*p
*p
*c
*c
Text is not SVG - cannot display
4 | -------------------------------------------------------------------------------- /CADETProcess/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | ======================================= 3 | Settings (:mod:`CADETProcess.settings`) 4 | ======================================= 5 | 6 | .. currentmodule:: CADETProcess.settings 7 | 8 | This module provides functionality for general settings. 9 | 10 | .. autosummary:: 11 | :toctree: generated/ 12 | 13 | Settings 14 | 15 | """ # noqa 16 | 17 | import os 18 | import shutil 19 | import tempfile 20 | from pathlib import Path 21 | from warnings import warn 22 | 23 | from CADETProcess.dataStructure import Bool, Structure, Switch 24 | 25 | __all__ = ["Settings"] 26 | 27 | 28 | class Settings(Structure): 29 | """ 30 | A class for managing general settings. 31 | 32 | Attributes 33 | ---------- 34 | working_directory : str or None 35 | The path of the working directory. If None, the current directory is used. 36 | save_log : bool 37 | Whether to save log files or not. 38 | temp_dir : str or None 39 | The path of the temporary directory. 40 | If None, a directory named "tmp" is created in the working directory. 41 | debug_mode : bool 42 | Whether to enable debug mode or not. 43 | LOG_LEVEL : str 44 | The log level to use. 45 | Must be one of 'DEBUG', 'INFO', 'WARNING', 'ERROR', or 'CRITICAL'. 46 | 47 | Methods 48 | ------- 49 | delete_temporary_files() 50 | Deletes the temporary simulation files. 51 | """ 52 | 53 | _save_log = Bool(default=False) 54 | debug_mode = Bool(default=False) 55 | LOG_LEVEL = Switch( 56 | valid=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], 57 | default="INFO", 58 | ) 59 | 60 | def __init__(self) -> None: 61 | """Initialize Settings Object.""" 62 | self._temp_dir = None 63 | self.working_directory = None 64 | 65 | @property 66 | def working_directory(self) -> str: 67 | """ 68 | The path of the working directory. 69 | 70 | If the working directory is not set, the current directory is used. 71 | 72 | Raises 73 | ------ 74 | TypeError 75 | If the working directory is not a string or None. 76 | 77 | Returns 78 | ------- 79 | pathlib.Path 80 | The absolute path of the working directory. 81 | """ 82 | if self._working_directory is None: 83 | _working_directory = Path("./") 84 | else: 85 | _working_directory = Path(self._working_directory) 86 | 87 | _working_directory = _working_directory.absolute() 88 | 89 | _working_directory.mkdir(exist_ok=True, parents=True) 90 | 91 | return _working_directory 92 | 93 | @working_directory.setter 94 | def working_directory(self, working_directory: str) -> None: 95 | self._working_directory = working_directory 96 | 97 | def set_working_directory(self, working_directory: str) -> None: 98 | """Set working directory.""" 99 | warn( 100 | "This function is deprecated, use working_directory property.", 101 | DeprecationWarning, 102 | stacklevel=2, 103 | ) 104 | self.working_directory = working_directory 105 | 106 | @property 107 | def save_log(self) -> bool: 108 | """bool: If True, save log files.""" 109 | return self._save_log 110 | 111 | @save_log.setter 112 | def save_log(self, save_log: bool) -> None: 113 | from CADETProcess import log 114 | 115 | log.update_loggers(self.log_directory, save_log) 116 | 117 | self._save_log = save_log 118 | 119 | @property 120 | def log_directory(self) -> str: 121 | """pathlib.Path: Log directory.""" 122 | return self.working_directory / "log" 123 | 124 | @property 125 | def temp_dir(self) -> str: 126 | """pathlib.Path: Directory for temporary files.""" 127 | if self._temp_dir is None: 128 | if "XDG_RUNTIME_DIR" in os.environ: 129 | _temp_dir = Path(os.environ["XDG_RUNTIME_DIR"]) / "CADET-Process" 130 | else: 131 | _temp_dir = self.working_directory / "tmp" 132 | else: 133 | _temp_dir = Path(self._temp_dir).absolute() 134 | 135 | _temp_dir.mkdir(exist_ok=True, parents=True) 136 | tempfile.tempdir = _temp_dir.as_posix() 137 | 138 | return Path(tempfile.gettempdir()) 139 | 140 | @temp_dir.setter 141 | def temp_dir(self, temp_dir: str) -> None: 142 | self._temp_dir = temp_dir 143 | 144 | def delete_temporary_files(self) -> None: 145 | """Delete the temporary files directory.""" 146 | shutil.rmtree(self.temp_dir / "simulation_files", ignore_errors=True) 147 | self.temp_dir = self._temp_dir 148 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../..')) 16 | from datetime import date 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'CADET-Process' 21 | copyright = f'2019-{date.today().year}' 22 | author = 'Johannes Schmölder' 23 | 24 | import CADETProcess 25 | version = CADETProcess.__version__ 26 | release = CADETProcess.__version__.replace("_", "") 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | 34 | # Extensions 35 | extensions = [] 36 | 37 | ## MyST-NB 38 | extensions.append("myst_nb") 39 | nb_execution_mode = "cache" 40 | source_suffix = { 41 | '.rst': 'restructuredtext', 42 | '.ipynb': 'myst-nb', 43 | '.myst': 'myst-nb', 44 | '.md': 'myst-nb', 45 | } 46 | 47 | ## Numpydoc 48 | extensions.append("numpydoc") 49 | numpydoc_class_members_toctree = False 50 | 51 | ## Autodoc 52 | extensions.append("sphinx.ext.autodoc") 53 | 54 | ## Autosummary 55 | extensions.append("sphinx.ext.autosummary") 56 | autosummary_generate = True 57 | 58 | ## Intersphinx mapping 59 | extensions.append("sphinx.ext.intersphinx") 60 | intersphinx_mapping = { 61 | "python": ("https://docs.python.org/3/", None), 62 | "numpy": ("https://numpy.org/doc/stable/", None), 63 | "matplotlib": ("https://matplotlib.org/stable", None), 64 | "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), 65 | "cadet": ("https://cadet.github.io/master/", None), 66 | } 67 | 68 | ## To do 69 | extensions.append("sphinx.ext.todo") 70 | todo_include_todos = True 71 | 72 | ## Viewcode 73 | extensions.append("sphinx.ext.viewcode") 74 | 75 | ## Copy Button 76 | extensions.append("sphinx_copybutton") 77 | 78 | ## BibTeX 79 | extensions.append("sphinxcontrib.bibtex") 80 | bibtex_bibfiles = ['references.bib'] 81 | 82 | # -- Internationalization ------------------------------------------------ 83 | # specifying the natural language populates some key tags 84 | language = "en" 85 | 86 | # ReadTheDocs has its own way of generating sitemaps, etc. 87 | if not os.environ.get("READTHEDOCS"): 88 | extensions += ["sphinx_sitemap"] 89 | 90 | # -- Sitemap ------------------------------------------------------------- 91 | html_baseurl = os.environ.get("SITEMAP_URL_BASE", "http://127.0.0.1:8000/") 92 | sitemap_locales = [None] 93 | sitemap_url_scheme = "{link}" 94 | 95 | # Add any paths that contain templates here, relative to this directory. 96 | templates_path = ["_templates"] 97 | 98 | # List of patterns, relative to source directory, that match files and 99 | # directories to ignore when looking for source files. 100 | # This pattern also affects html_static_path and html_extra_path. 101 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 102 | 103 | # -- Extension options ------------------------------------------------------- 104 | 105 | myst_enable_extensions = [ 106 | "dollarmath", 107 | "amsmath", 108 | "colon_fence", 109 | ] 110 | 111 | # -- Options for HTML output ------------------------------------------------- 112 | 113 | # The theme to use for HTML and HTML Help pages. 114 | html_theme = "sphinx_book_theme" 115 | html_logo = "_static/logo.png" 116 | 117 | html_theme_options = { 118 | "show_toc_level": 2, 119 | "use_download_button": True, 120 | "repository_url": "https://github.com/fau-advanced-separations/CADET-Process", 121 | "use_repository_button": True, 122 | "use_issues_button": True, 123 | } 124 | 125 | html_sidebars = { 126 | "**": ["navbar-logo.html", "search-field.html", "sbt-sidebar-nav.html"] 127 | } 128 | 129 | # Add any paths that contain custom static files (such as style sheets) here, 130 | # relative to this directory. They are copied after the builtin static files, 131 | # so a file named "default.css" will overwrite the builtin "default.css". 132 | html_static_path = ["_static"] 133 | 134 | # Copy examples 135 | import shutil 136 | shutil.rmtree('./examples', ignore_errors=True) 137 | shutil.copytree('../../examples', './examples/') 138 | -------------------------------------------------------------------------------- /examples/load_wash_elute/lwe_flow_rate.md: -------------------------------------------------------------------------------- 1 | --- 2 | jupytext: 3 | formats: md:myst,py:percent 4 | text_representation: 5 | extension: .md 6 | format_name: myst 7 | format_version: 0.13 8 | jupytext_version: 1.17.1 9 | kernelspec: 10 | display_name: Python 3 11 | language: python 12 | name: python3 13 | --- 14 | 15 | (lwe_example_flow_rate)= 16 | # Flow Rate Gradients 17 | 18 | 19 | ```{figure} ./figures/flow_sheet_flow_rate.svg 20 | Flow sheet for load-wash-elute process using a separate inlets for buffers. 21 | ``` 22 | 23 | ```{code-cell} 24 | from CADETProcess.processModel import ComponentSystem 25 | from CADETProcess.processModel import StericMassAction 26 | from CADETProcess.processModel import Inlet, GeneralRateModel, Outlet 27 | from CADETProcess.processModel import FlowSheet 28 | from CADETProcess.processModel import Process 29 | 30 | # Component System 31 | component_system = ComponentSystem() 32 | component_system.add_component('Salt') 33 | component_system.add_component('A') 34 | component_system.add_component('B') 35 | component_system.add_component('C') 36 | 37 | # Binding Model 38 | binding_model = StericMassAction(component_system, name='SMA') 39 | binding_model.is_kinetic = True 40 | binding_model.adsorption_rate = [0.0, 35.5, 1.59, 7.7] 41 | binding_model.desorption_rate = [0.0, 1000, 1000, 1000] 42 | binding_model.characteristic_charge = [0.0, 4.7, 5.29, 3.7] 43 | binding_model.steric_factor = [0.0, 11.83, 10.6, 10] 44 | binding_model.capacity = 1200.0 45 | 46 | # Unit Operations 47 | load = Inlet(component_system, name='load') 48 | load.c = [50, 1.0, 1.0, 1.0] 49 | 50 | wash = Inlet(component_system, name='wash') 51 | wash.c = [50.0, 0.0, 0.0, 0.0] 52 | 53 | elute = Inlet(component_system, name='elute') 54 | elute.c = [500.0, 0.0, 0.0, 0.0] 55 | 56 | column = GeneralRateModel(component_system, name='column') 57 | column.binding_model = binding_model 58 | 59 | column.length = 0.014 60 | column.diameter = 0.02 61 | column.bed_porosity = 0.37 62 | column.particle_radius = 4.5e-5 63 | column.particle_porosity = 0.75 64 | column.axial_dispersion = 5.75e-8 65 | column.film_diffusion = column.n_comp * [6.9e-6] 66 | column.pore_diffusion = [7e-10, 6.07e-11, 6.07e-11, 6.07e-11] 67 | column.surface_diffusion = column.n_bound_states * [0.0] 68 | 69 | column.c = [50, 0, 0, 0] 70 | column.cp = [50, 0, 0, 0] 71 | column.q = [binding_model.capacity, 0, 0, 0] 72 | 73 | outlet = Outlet(component_system, name='outlet') 74 | 75 | # Flow Sheet 76 | flow_sheet = FlowSheet(component_system) 77 | 78 | flow_sheet.add_unit(load, feed_inlet=True) 79 | flow_sheet.add_unit(wash, eluent_inlet=True) 80 | flow_sheet.add_unit(elute, eluent_inlet=True) 81 | flow_sheet.add_unit(column) 82 | flow_sheet.add_unit(outlet, product_outlet=True) 83 | 84 | flow_sheet.add_connection(load, column) 85 | flow_sheet.add_connection(wash, column) 86 | flow_sheet.add_connection(elute, column) 87 | flow_sheet.add_connection(column, outlet) 88 | ``` 89 | 90 | ```{figure} ./figures/events_flow_rate.svg 91 | Events of load-wash-elute process using multiple inlets and mofifying their flow rates. 92 | ``` 93 | 94 | ```{code-cell} 95 | # Process 96 | process = Process(flow_sheet, 'lwe') 97 | process.cycle_time = 2000.0 98 | 99 | load_duration = 10.0 100 | t_gradient_start = 90.0 101 | gradient_duration = process.cycle_time - t_gradient_start 102 | 103 | Q = 6.683738370512285e-8 104 | gradient_slope = Q / (process.cycle_time - t_gradient_start) 105 | 106 | process.add_event('load_on', 'flow_sheet.load.flow_rate', Q) 107 | process.add_event('load_off', 'flow_sheet.load.flow_rate', 0.0) 108 | process.add_duration('load_duration', time=load_duration) 109 | process.add_event_dependency('load_off', ['load_on', 'load_duration'], [1, 1]) 110 | 111 | process.add_event('wash_off', 'flow_sheet.wash.flow_rate', 0) 112 | process.add_event( 113 | 'elute_off', 'flow_sheet.elute.flow_rate', 0 114 | ) 115 | 116 | process.add_event( 117 | 'wash_on', 'flow_sheet.wash.flow_rate', Q, time=load_duration 118 | ) 119 | process.add_event_dependency('wash_on', ['load_off']) 120 | 121 | process.add_event( 122 | 'wash_gradient', 'flow_sheet.wash.flow_rate', 123 | [Q, -gradient_slope], t_gradient_start 124 | ) 125 | process.add_event( 126 | 'elute_gradient', 'flow_sheet.elute.flow_rate', [0, gradient_slope] 127 | ) 128 | process.add_event_dependency('elute_gradient', ['wash_gradient']) 129 | ``` 130 | 131 | ```{code-cell} 132 | if __name__ == '__main__': 133 | from CADETProcess.simulator import Cadet 134 | process_simulator = Cadet() 135 | 136 | simulation_results = process_simulator.simulate(process) 137 | 138 | from CADETProcess.plotting import SecondaryAxis 139 | sec = SecondaryAxis() 140 | sec.components = ['Salt'] 141 | sec.y_label = '$c_{salt}$' 142 | 143 | simulation_results.solution.column.outlet.plot(secondary_axis=sec) 144 | ``` 145 | -------------------------------------------------------------------------------- /CADETProcess/comparison/shape.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing import Optional 3 | 4 | import numpy as np 5 | import scipy 6 | import scipy.optimize as optimize 7 | from scipy.interpolate import PchipInterpolator 8 | 9 | 10 | def pearson( 11 | time: np.ndarray, 12 | reference_spline: PchipInterpolator, 13 | simulation_spline: PchipInterpolator, 14 | offset: Optional[float] = 0, 15 | ) -> float: 16 | """ 17 | Calculate Pearson correlation. 18 | 19 | Optionally, an offset can be specified that shifts the signal in time. This can be 20 | a helpful metric in parameter estimation. 21 | 22 | Parameters 23 | ---------- 24 | time: np.ndarray 25 | The time array 26 | reference_spline: PchipInterpolator 27 | The reference data. 28 | simulation_spline: PchipInterpolator 29 | The simulated data. 30 | offset: float, optional 31 | A time offset to be applied to the simulation spline. 32 | 33 | Returns 34 | ------- 35 | float 36 | The pearson correlation given the current offset. 37 | """ 38 | shifted_time = time - offset 39 | 40 | # restrict to the valid domain of reference_spline 41 | t_min, t_max = time[0], time[-1] 42 | valid_mask = (shifted_time >= t_min) & (shifted_time <= t_max) 43 | 44 | time_eval = time[valid_mask] 45 | shifted_eval = shifted_time[valid_mask] 46 | 47 | ref_vals = reference_spline(time_eval) 48 | sim_vals = simulation_spline(shifted_eval) 49 | 50 | try: 51 | pear = scipy.stats.pearsonr(ref_vals, sim_vals)[0] 52 | except ValueError: 53 | warnings.warn( 54 | f"Pearson correlation failed due to NaN or Inf in array reference: " 55 | f"{reference_spline.x}, simulation: {simulation_spline.x}" 56 | ) 57 | pear = -1 58 | 59 | return pear 60 | 61 | 62 | def flip_pearson(pear: float) -> float: 63 | """Flip value s.t. 0 is best and 1 is worst.""" 64 | return 0.5 * (1 - pear) 65 | 66 | 67 | def shape( 68 | time: np.ndarray, 69 | reference_spline: PchipInterpolator, 70 | simulation_spline: PchipInterpolator, 71 | offset: Optional[float] = 0, 72 | flip: Optional[bool] = True, 73 | ) -> float: 74 | """ 75 | Calculate shape metric. 76 | 77 | Parameters 78 | ---------- 79 | time: np.ndarray 80 | The time array 81 | reference_spline: PchipInterpolator 82 | The reference data. 83 | simulation_spline: PchipInterpolator 84 | The simulated data. 85 | offset: float, optional 86 | A time offset to be applied to the simulation spline. 87 | flip: bool, optional 88 | If True, flip value s.t. 0 is best and 1 is worst. 89 | The default is True. 90 | 91 | Returns 92 | ------- 93 | float 94 | The pearson correlation given the current offset. 95 | """ 96 | pear = pearson(time, reference_spline, simulation_spline, offset) 97 | if flip: 98 | pear = flip_pearson(pear) 99 | 100 | return pear 101 | 102 | 103 | def determine_optimal_offset( 104 | time: np.ndarray, 105 | reference_spline: PchipInterpolator, 106 | simulation_spline: PchipInterpolator, 107 | ) -> tuple[float, float]: 108 | """ 109 | Determine the optimal time offset s.t. Pearson correlation is maximixed. 110 | 111 | Uses scipy.optimize.minimize to find the optimal time offset. 112 | 113 | Parameters 114 | ---------- 115 | time: np.ndarray 116 | The time array 117 | reference_spline: PchipInterpolator 118 | The reference data. 119 | simulation_spline: PchipInterpolator 120 | The simulated data. 121 | 122 | Returns 123 | ------- 124 | tuple[float, float] 125 | The maximum pearson correlation and the corresponding time offset. 126 | """ 127 | x0 = 0 128 | window = 0.05 * (time[-1] - time[0]) 129 | bounds = (-time[-1] + window, time[-1] - window) # Limit offset to avoid issues 130 | 131 | # Brute force for screening 132 | _, _, offsets, scores = optimize.brute( 133 | lambda x: shape(time, reference_spline, simulation_spline, x, True), 134 | [bounds], 135 | Ns=101, 136 | full_output=True, 137 | finish=None, 138 | ) 139 | ind = np.nanargmin(scores) 140 | f = scores[ind] 141 | x = offsets[ind] 142 | 143 | # Update for refinement 144 | x0 = x 145 | window = 0.01 * (time[-1] - time[0]) 146 | bounds = (max(bounds[0], x0 - window), min(bounds[1], x0 + window)) 147 | 148 | # Refinement 149 | result = optimize.minimize( 150 | lambda x: shape(time, reference_spline, simulation_spline, x, True), 151 | x0=x0, 152 | bounds=[bounds], 153 | method="Powell", 154 | options={"xtol": 1e-12}, 155 | ) 156 | 157 | f = result.fun 158 | x = result.x[0] 159 | 160 | return f, x 161 | -------------------------------------------------------------------------------- /examples/load_wash_elute/lwe_flow_rate.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: md:myst,py:percent 5 | # text_representation: 6 | # extension: .py 7 | # format_name: percent 8 | # format_version: '1.3' 9 | # jupytext_version: 1.17.1 10 | # kernelspec: 11 | # display_name: Python 3 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # %% [markdown] 17 | # (lwe_example_flow_rate)= 18 | # # Flow Rate Gradients 19 | # 20 | # 21 | # ```{figure} ./figures/flow_sheet_flow_rate.svg 22 | # Flow sheet for load-wash-elute process using a separate inlets for buffers. 23 | # ``` 24 | 25 | # %% 26 | from CADETProcess.processModel import ComponentSystem 27 | from CADETProcess.processModel import StericMassAction 28 | from CADETProcess.processModel import Inlet, GeneralRateModel, Outlet 29 | from CADETProcess.processModel import FlowSheet 30 | from CADETProcess.processModel import Process 31 | 32 | # Component System 33 | component_system = ComponentSystem() 34 | component_system.add_component('Salt') 35 | component_system.add_component('A') 36 | component_system.add_component('B') 37 | component_system.add_component('C') 38 | 39 | # Binding Model 40 | binding_model = StericMassAction(component_system, name='SMA') 41 | binding_model.is_kinetic = True 42 | binding_model.adsorption_rate = [0.0, 35.5, 1.59, 7.7] 43 | binding_model.desorption_rate = [0.0, 1000, 1000, 1000] 44 | binding_model.characteristic_charge = [0.0, 4.7, 5.29, 3.7] 45 | binding_model.steric_factor = [0.0, 11.83, 10.6, 10] 46 | binding_model.capacity = 1200.0 47 | 48 | # Unit Operations 49 | load = Inlet(component_system, name='load') 50 | load.c = [50, 1.0, 1.0, 1.0] 51 | 52 | wash = Inlet(component_system, name='wash') 53 | wash.c = [50.0, 0.0, 0.0, 0.0] 54 | 55 | elute = Inlet(component_system, name='elute') 56 | elute.c = [500.0, 0.0, 0.0, 0.0] 57 | 58 | column = GeneralRateModel(component_system, name='column') 59 | column.binding_model = binding_model 60 | 61 | column.length = 0.014 62 | column.diameter = 0.02 63 | column.bed_porosity = 0.37 64 | column.particle_radius = 4.5e-5 65 | column.particle_porosity = 0.75 66 | column.axial_dispersion = 5.75e-8 67 | column.film_diffusion = column.n_comp * [6.9e-6] 68 | column.pore_diffusion = [7e-10, 6.07e-11, 6.07e-11, 6.07e-11] 69 | column.surface_diffusion = column.n_bound_states * [0.0] 70 | 71 | column.c = [50, 0, 0, 0] 72 | column.cp = [50, 0, 0, 0] 73 | column.q = [binding_model.capacity, 0, 0, 0] 74 | 75 | outlet = Outlet(component_system, name='outlet') 76 | 77 | # Flow Sheet 78 | flow_sheet = FlowSheet(component_system) 79 | 80 | flow_sheet.add_unit(load, feed_inlet=True) 81 | flow_sheet.add_unit(wash, eluent_inlet=True) 82 | flow_sheet.add_unit(elute, eluent_inlet=True) 83 | flow_sheet.add_unit(column) 84 | flow_sheet.add_unit(outlet, product_outlet=True) 85 | 86 | flow_sheet.add_connection(load, column) 87 | flow_sheet.add_connection(wash, column) 88 | flow_sheet.add_connection(elute, column) 89 | flow_sheet.add_connection(column, outlet) 90 | 91 | # %% [markdown] 92 | # ```{figure} ./figures/events_flow_rate.svg 93 | # Events of load-wash-elute process using multiple inlets and mofifying their flow rates. 94 | # ``` 95 | 96 | # %% 97 | # Process 98 | process = Process(flow_sheet, 'lwe') 99 | process.cycle_time = 2000.0 100 | 101 | load_duration = 10.0 102 | t_gradient_start = 90.0 103 | gradient_duration = process.cycle_time - t_gradient_start 104 | 105 | Q = 6.683738370512285e-8 106 | gradient_slope = Q / (process.cycle_time - t_gradient_start) 107 | 108 | process.add_event('load_on', 'flow_sheet.load.flow_rate', Q) 109 | process.add_event('load_off', 'flow_sheet.load.flow_rate', 0.0) 110 | process.add_duration('load_duration', time=load_duration) 111 | process.add_event_dependency('load_off', ['load_on', 'load_duration'], [1, 1]) 112 | 113 | process.add_event('wash_off', 'flow_sheet.wash.flow_rate', 0) 114 | process.add_event( 115 | 'elute_off', 'flow_sheet.elute.flow_rate', 0 116 | ) 117 | 118 | process.add_event( 119 | 'wash_on', 'flow_sheet.wash.flow_rate', Q, time=load_duration 120 | ) 121 | process.add_event_dependency('wash_on', ['load_off']) 122 | 123 | process.add_event( 124 | 'wash_gradient', 'flow_sheet.wash.flow_rate', 125 | [Q, -gradient_slope], t_gradient_start 126 | ) 127 | process.add_event( 128 | 'elute_gradient', 'flow_sheet.elute.flow_rate', [0, gradient_slope] 129 | ) 130 | process.add_event_dependency('elute_gradient', ['wash_gradient']) 131 | 132 | # %% 133 | if __name__ == '__main__': 134 | from CADETProcess.simulator import Cadet 135 | process_simulator = Cadet() 136 | 137 | simulation_results = process_simulator.simulate(process) 138 | 139 | from CADETProcess.plotting import SecondaryAxis 140 | sec = SecondaryAxis() 141 | sec.components = ['Salt'] 142 | sec.y_label = '$c_{salt}$' 143 | 144 | simulation_results.solution.column.outlet.plot(secondary_axis=sec) 145 | --------------------------------------------------------------------------------