diff --git a/.flake8 b/.flake8
index 45105cf..f35b80a 100644
--- a/.flake8
+++ b/.flake8
@@ -14,6 +14,7 @@ exclude =
dist,
.conda,
tests/autogenerated_*,
+ docs/source/_autogenerated
.venv,
venv
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index af5beb7..b74661d 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -17,17 +17,20 @@ jobs:
uses: actions/setup-python@v3
with:
python-version: "3.x"
- - name: Install dependencies
- run: pip install sphinx sphinx_rtd_theme sphinx-autodoc-typehints myst-parser
- - name: Generate Class List
+ - name: Install gaspype and dependencies
run: |
- echo "Create a dummy file to ensure the class list generation works"
+ echo "Create a dummy file to ensure gaspype does't crash"
mkdir -p src/gaspype/data
printf 'gapy\x00\x00\x00\x00' > src/gaspype/data/therm_data.bin
- pip install .
- python ./docs/source/generate_class_list.py
+ pip install .[doc_build]
+ python -m ipykernel install --user --name temp_kernel --display-name "Python (temp_kernel)"
+ - name: Generate Docs
+ run: python ./docs/source/generate_class_list.py
+ - name: Generate Examples
+ run: python ./docs/source/render_examples.py
- name: Build Docs
run: |
+ cp LICENSE docs/source/LICENSE.md
cd docs
sphinx-apidoc -o ./source/ ../src/ -M --no-toc
rm ./source/*.rst
diff --git a/.gitignore b/.gitignore
index d588a6b..f74d387 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,7 @@ __pycache__
.pytest_cache
tests/autogenerated_*.py
docs/build/
-docs/source/modules.md
+docs/source/_autogenerated/
venv/
.venv/
thermo_data/combined_data.yaml
diff --git a/README.md b/README.md
index 2367860..421721d 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,8 @@ fl.get_density(t=t_range, p=1e5)
```
array([0.10122906, 0.09574625, 0.09082685, 0.08638827, 0.08236328])
```
-A ```fluid``` object can have multiple compositions. A multidimensional ```fluid``` object can be created for example by multiplication with a numpy array:
+A ```fluid``` object can have multiple compositions. A multidimensional ```fluid``` object
+can be created for example by multiplication with a numpy array:
``` python
fl2 = gp.fluid({'H2O': 1, 'N2': 2}) + \
@@ -125,7 +126,8 @@ array([[[0. , 0.5 , 0.5 ],
```
### Elements
-In some cases not the molecular but the atomic composition is of interest. The ```elements``` class can be used for atom based balances and works similar:
+In some cases not the molecular but the atomic composition is of interest.
+The ```elements``` class can be used for atom based balances and works similar:
``` python
el = gp.elements({'N': 1, 'Cl': 2})
@@ -134,7 +136,9 @@ el.get_mass()
```
np.float64(0.08490700000000001)
```
-A ```elements``` object can be as well instantiated from a ```fluid``` object. Arithmetic operations between ```elements``` and ```fluid``` result in an ```elements``` object:
+A ```elements``` object can be as well instantiated from a ```fluid``` object.
+Arithmetic operations between ```elements``` and ```fluid``` result in
+an ```elements``` object:
``` python
el2 = gp.elements(fl) + el - 0.3 * fl
el2
@@ -146,7 +150,8 @@ N 1.000e+00 mol
O 7.000e-01 mol
```
-Going from an atomic composition to an molecular composition is a little bit less straight forward, since there is no universal approach. One way is to calculate the thermodynamic equilibrium for a mixture:
+Going from an atomic composition to an molecular composition is possible as well.
+One way is to calculate the thermodynamic equilibrium for a mixture:
``` python
fs = gp.fluid_system('CH4, H2, CO, CO2, O2')
@@ -163,7 +168,13 @@ CO2 33.07 %
O2 0.00 %
```
-The ```equilibrium``` function can be called with a ```fluid``` or ```elements``` object as first argument. ```fluid``` and ```elements``` referencing a ```fluid_system``` object witch can be be set as shown above during the object instantiation. If not provided, a new one will be created automatically. Providing a ```fluid_system``` gives more control over which molecular species are included in derived ```fluid``` objects. Furthermore arithmetic operations between objects with the same ```fluid_system``` are potentially faster:
+The ```equilibrium``` function can be called with a ```fluid``` or ```elements``` object
+as first argument. ```fluid``` and ```elements``` referencing a ```fluid_system``` object
+witch can be be set as shown above during the object instantiation. If not provided,
+a new one will be created automatically. Providing a ```fluid_system``` gives more
+control over which molecular species are included in derived ```fluid``` objects.
+Furthermore arithmetic operations between objects with the same ```fluid_system```
+are potentially faster:
``` python
fl3 + gp.fluid({'CH4': 1}, fs)
@@ -177,7 +188,9 @@ CO2 18.07 %
O2 0.00 %
```
-Especially if the ```fluid_system``` of one of the operants has not a subset of molecular species of the other ```fluid_system``` a new ```fluid_system``` will be created for the operation which might degrade performance:
+Especially if the ```fluid_system``` of one of the operants has not a subset of
+molecular species of the other ```fluid_system``` a new ```fluid_system``` will
+be created for the operation which might degrade performance:
``` python
fl3 + gp.fluid({'NH3': 1})
diff --git a/docs/media/soc_inverted.svg b/docs/media/soc_inverted.svg
new file mode 100644
index 0000000..6a5bf2d
--- /dev/null
+++ b/docs/media/soc_inverted.svg
@@ -0,0 +1,193 @@
+
+
+
+
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 4187ecf..35ab3d2 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -17,7 +17,7 @@ author = 'Nicolas Kruse'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
-extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", "myst_parser"]
+extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", "myst_parser", "sphinx.ext.autosummary"]
templates_path = ['_templates']
exclude_patterns = []
@@ -26,7 +26,8 @@ exclude_patterns = []
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
# html_theme = 'alabaster'
-html_theme = 'sphinx_rtd_theme'
+html_theme = 'pydata_sphinx_theme'
html_static_path = ['_static']
autodoc_inherit_docstrings = True
+autoclass_content = 'both'
diff --git a/docs/source/floatarray.md b/docs/source/floatarray.md
new file mode 100644
index 0000000..98c5e3c
--- /dev/null
+++ b/docs/source/floatarray.md
@@ -0,0 +1,5 @@
+# gaspype.FloatArray
+
+```{eval-rst}
+.. autoclass:: gaspype.FloatArray
+```
\ No newline at end of file
diff --git a/docs/source/generate_class_list.py b/docs/source/generate_class_list.py
index 8fa77cf..41a941f 100644
--- a/docs/source/generate_class_list.py
+++ b/docs/source/generate_class_list.py
@@ -1,11 +1,17 @@
+# This script generates the source md-files for all classes and functions for the docs
+
import importlib
import inspect
import fnmatch
from io import TextIOWrapper
-def write_classes(f: TextIOWrapper, patterns: list[str], module_name: str, title: str, description: str = '', exclude: list[str] = []) -> None:
+def write_manual(f: TextIOWrapper, doc_files: list[str], title: str) -> None:
+ write_dochtree(f, title, doc_files)
+
+def write_classes(f: TextIOWrapper, patterns: list[str], module_name: str, title: str, description: str = '', exclude: list[str] = []) -> None:
+ """Write the classes to the file."""
module = importlib.import_module(module_name)
classes = [
@@ -15,26 +21,25 @@ def write_classes(f: TextIOWrapper, patterns: list[str], module_name: str, title
obj.__doc__ and '(Automatic generated stub)' not in obj.__doc__)
]
- """Write the classes to the file."""
- f.write(f'## {title}\n\n')
if description:
f.write(f'{description}\n\n')
+ write_dochtree(f, title, classes)
+
for cls in classes:
- f.write('```{eval-rst}\n')
- f.write(f'.. autoclass:: {module_name}.{cls}\n')
- f.write(' :members:\n')
- f.write(' :class-doc-from: both\n')
- f.write(' :undoc-members:\n')
- f.write(' :show-inheritance:\n')
- f.write(' :inherited-members:\n')
- if title != 'Base classes':
- f.write(' :exclude-members: select\n')
- f.write('```\n\n')
+ with open(f'docs/source/_autogenerated/{cls}.md', 'w') as f2:
+ f2.write(f'# {module_name}.{cls}\n')
+ f2.write('```{eval-rst}\n')
+ f2.write(f'.. autoclass:: {module_name}.{cls}\n')
+ f2.write(' :members:\n')
+ f2.write(' :undoc-members:\n')
+ f2.write(' :show-inheritance:\n')
+ f2.write(' :inherited-members:\n')
+ f2.write('```\n\n')
def write_functions(f: TextIOWrapper, patterns: list[str], module_name: str, title: str, description: str = '', exclude: list[str] = []) -> None:
-
+ """Write the classes to the file."""
module = importlib.import_module(module_name)
functions = [
@@ -43,19 +48,37 @@ def write_functions(f: TextIOWrapper, patterns: list[str], module_name: str, tit
any(fnmatch.fnmatch(name, pat) for pat in patterns if pat not in exclude))
]
- """Write the classes to the file."""
- f.write(f'## {title}\n\n')
if description:
f.write(f'{description}\n\n')
+ write_dochtree(f, title, functions)
+
for func in functions:
if not func.startswith('_'):
- f.write('```{eval-rst}\n')
- f.write(f'.. autofunction:: {module_name}.{func}\n')
- f.write('```\n\n')
+ with open(f'docs/source/_autogenerated/{func}.md', 'w') as f2:
+ f2.write(f'# {module_name}.{func}\n')
+ f2.write('```{eval-rst}\n')
+ f2.write(f'.. autofunction:: {module_name}.{func}\n')
+ f2.write('```\n\n')
-with open('docs/source/modules.md', 'w') as f:
- f.write('# Functions and classes\n\n')
- write_classes(f, ['*'], 'gaspype', title='Classes')
- write_functions(f, ['*'], 'gaspype', title='Functions')
+def write_dochtree(f: TextIOWrapper, title: str, items: list[str]):
+ f.write('```{toctree}\n')
+ f.write(':maxdepth: 1\n')
+ f.write(f':caption: {title}:\n')
+ #f.write(':hidden:\n')
+ for text in items:
+ if not text.startswith('_'):
+ f.write(f"{text}\n")
+ f.write('```\n\n')
+
+
+if __name__ == "__main__":
+ with open('docs/source/_autogenerated/index.md', 'w') as f:
+ f.write('# Classes and functions\n\n')
+
+ write_classes(f, ['*'], 'gaspype', title='Classes')
+
+ write_functions(f, ['*'], 'gaspype', title='Functions')
+
+ write_manual(f, ['../ndfloat', '../floatarray'], title='Types')
diff --git a/docs/source/index.md b/docs/source/index.md
index 7232e27..9e284ef 100644
--- a/docs/source/index.md
+++ b/docs/source/index.md
@@ -1,9 +1,8 @@
```{toctree}
-:maxdepth: 2
-:caption: Contents:
-
-readme
-modules
+:maxdepth: 1
+:hidden:
+_autogenerated/index
+_autogenerated/examples
```
```{include} ../../README.md
diff --git a/docs/source/ndfloat.md b/docs/source/ndfloat.md
new file mode 100644
index 0000000..1ac7868
--- /dev/null
+++ b/docs/source/ndfloat.md
@@ -0,0 +1,5 @@
+# gaspype.NDFloat
+
+```{eval-rst}
+.. autoclass:: gaspype.NDFloat
+```
\ No newline at end of file
diff --git a/docs/source/readme.md b/docs/source/readme.md
deleted file mode 100644
index 060259b..0000000
--- a/docs/source/readme.md
+++ /dev/null
@@ -1,2 +0,0 @@
-```{include} ../../README.md
-```
\ No newline at end of file
diff --git a/docs/source/render_examples.py b/docs/source/render_examples.py
new file mode 100644
index 0000000..b66a25d
--- /dev/null
+++ b/docs/source/render_examples.py
@@ -0,0 +1,66 @@
+# This script converts the example md-files as jupyter notebook,
+# execute the notebook and convert the notebook back to a md-file
+# with outputs included.
+
+import subprocess
+from glob import glob
+import os
+from io import TextIOWrapper
+
+
+def run_cmd(command: list[str]):
+ result = subprocess.run(command, capture_output=True, text=True)
+
+ print('> ' + ' '.join(command))
+ print(result.stdout)
+
+ assert (not result.stderr or
+ any('RuntimeWarning: ' in line for line in result.stderr.splitlines()) or
+ any('[NbConvertApp]' in line and 'error' not in line.lower() for line in result.stderr.splitlines())), 'ERROR: ' + result.stderr
+
+
+def run_rendering(input_path: str, output_directory: str):
+ file_name = '.'.join(os.path.basename(input_path).split('.')[:-1])
+ assert file_name
+
+ print(f'- Convert {input_path} ...')
+ run_cmd(['notedown', input_path, '--to', 'notebook', '--output', f'{output_directory}/{file_name}.ipynb', '--run'])
+ run_cmd(['jupyter', 'nbconvert', '--to', 'markdown', f'{output_directory}/{file_name}.ipynb', '--output', f'{file_name}.md'])
+ run_cmd(['python', 'tests/md_to_code.py', 'script', f'{input_path}', f'{output_directory}/{file_name}.py'])
+
+
+def write_dochtree(f: TextIOWrapper, title: str, items: list[str]):
+ f.write('```{toctree}\n')
+ f.write(':maxdepth: 1\n')
+ #f.write(':hidden:\n')
+ #f.write(f':caption: {title}:\n')
+ for text in items:
+ if not text.startswith('_'):
+ f.write(f"{text}\n")
+ f.write('```\n\n')
+
+
+def render_examples(filter: str, example_file: str):
+ files = glob(filter)
+ names = ['.'.join(os.path.basename(path).split('.')[:-1]) for path in files]
+
+ with open(example_file, 'w') as f:
+ f.write('# Gaspype examples\n\n')
+ write_dochtree(f, '', [n for n in names if n.lower() != 'readme'])
+
+ f.write('## Download Jupyter Notebooks\n\n')
+ for path, name in zip(files, names):
+ if name.lower() != 'readme':
+ run_rendering(path, 'docs/source/_autogenerated')
+ notebook = name + '.ipynb'
+ f.write(f'- [{notebook}]({notebook})\n\n')
+
+ f.write('## Download plain python files\n\n')
+ for path, name in zip(files, names):
+ if name.lower() != 'readme':
+ script_name = name + '.py'
+ f.write(f'- [{script_name}]({script_name})\n\n')
+
+
+if __name__ == "__main__":
+ render_examples('examples/*.md', 'docs/source/_autogenerated/examples.md')
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..ba40e2f
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,25 @@
+# Example scripts
+
+Examples can be looked-up in the
+[documentation](https://dlr-institute-of-future-fuels.github.io/gaspype/)
+rendered with results.
+
+The gaspype examples from this directory are available in the documentation as
+downloadable Jupyter Notebooks or plain python scripts with comments.
+
+The conversion is done like the following automated by the
+[docs/source/render_examples.py](../docs/source/render_examples.py) script:
+``` bash
+# Converting markdown with code sections to Jupyter Notebook and run it:
+notedown examples/soec_methane.md --to notebook --output docs/source/_autogenerated/soec_methane.ipynb --run
+
+# Converting the Jupyter Notebook to Markdown and a folder with image
+# files placed in docs/source/_autogenerated/:
+jupyter nbconvert --to markdown docs/source/_autogenerated/soec_methane.ipynb --output soec_methane.md
+```
+
+A new example Markdown file can be created from a Jupyter Notebook running
+the following command:
+``` bash
+jupyter nbconvert --to markdown new_example.ipynb --NbConvertApp.use_output_suffix=False --ClearOutputPreprocessor.enabled=True --output-dir examples/ --output new_example.md
+```
\ No newline at end of file
diff --git a/examples/carbon_activity.md b/examples/carbon_activity.md
new file mode 100644
index 0000000..4e8c67e
--- /dev/null
+++ b/examples/carbon_activity.md
@@ -0,0 +1,68 @@
+# Carbon Activity
+
+This example shows the equilibrium calculation for solid carbon.
+
+
+```python
+import gaspype as gp
+import numpy as np
+import matplotlib.pyplot as plt
+```
+
+Setting temperatures and pressure:
+
+
+```python
+t_range = np.array([600, 700, 800, 900, 1100, 1500]) # °C
+
+p = 1e5 # Pa
+
+fs = gp.fluid_system(['H2', 'H2O', 'CO2', 'CO', 'CH4'])
+```
+
+Equilibrium calculation for methane steam mixtures:
+
+
+```python
+ratio = np.linspace(0.01, 1.5, num=128)
+
+fl = gp.fluid({'CH4': 1}, fs) + ratio * gp.fluid({'H2O': 1}, fs)
+```
+
+gaspype.carbon_activity supports currently only 0D fluids therefore we build this helper function:
+
+
+```python
+def partial_c_activity(fl: gp.fluid, t: float, p: float):
+ fls = fl.array_composition.shape
+
+ eq_fl = gp.equilibrium(fl, t, p)
+
+ ret = np.zeros(fls[0])
+ for i in range(fls[0]):
+ ret[i] = gp.carbon_activity(gp.fluid(eq_fl.array_composition[i,:], fs), t, p)
+
+ return ret
+```
+
+Now we use the helper function to calculate the carbon activitx for all
+compositions in equilibrium_h2o times all temperatures in t_range:
+
+
+```python
+carbon_activity = np.vstack([partial_c_activity(fl, tc + 273.15, p) for tc in t_range])
+```
+
+Plot carbon activities, a activity of > 1 means there is thermodynamically the formation of sold carbon favored.
+
+
+```python
+fig, ax = plt.subplots(figsize=(6, 4), dpi=120)
+ax.set_xlabel("CO2/CH4")
+ax.set_ylabel("carbon activity")
+ax.set_ylim(1e-1, 1e3)
+ax.set_yscale('log')
+ax.plot(ratio, carbon_activity.T)
+ax.hlines(1, np.min(ratio), np.max(ratio), colors='k', linestyles='dashed')
+ax.legend([f'{tc} °C' for tc in t_range])
+```
diff --git a/examples/methane_mixtures.md b/examples/methane_mixtures.md
new file mode 100644
index 0000000..a39268d
--- /dev/null
+++ b/examples/methane_mixtures.md
@@ -0,0 +1,60 @@
+# Methane Mixtures
+
+This example shows equilibria of methane mixed with steam and CO2
+
+
+```python
+import gaspype as gp
+import numpy as np
+import matplotlib.pyplot as plt
+```
+
+Setting temperature and pressure:
+
+
+```python
+t = 900 + 273.15
+p = 1e5
+
+fs = gp.fluid_system(['H2', 'H2O', 'CO2', 'CO', 'CH4', 'O2'])
+```
+
+Equilibrium calculation for methane steam mixtures:
+
+
+```python
+ratio = np.linspace(0.01, 1.5, num=64)
+
+el = gp.fluid({'CH4': 1}, fs) + ratio * gp.fluid({'H2O': 1}, fs)
+equilibrium_h2o = gp.equilibrium(el, t, p)
+```
+
+
+```python
+fig, ax = plt.subplots(figsize=(6, 4), dpi=120)
+ax.set_xlabel("H2O/CH4")
+ax.set_ylabel("molar fraction")
+ax.set_ylim(0, 1.1)
+#ax.set_xlim(0, 100)
+ax.plot(ratio, equilibrium_h2o.get_x())
+ax.legend(fs.active_species)
+```
+
+Equilibrium calculation for methane CO2 mixtures:
+
+
+```python
+el = gp.fluid({'CH4': 1}, fs) + ratio * gp.fluid({'H2O': 1}, fs)
+equilibrium_co2 = gp.equilibrium(el, t, p)
+```
+
+
+```python
+fig, ax = plt.subplots(figsize=(6, 4), dpi=120)
+ax.set_xlabel("CO2/CH4")
+ax.set_ylabel("molar fraction")
+ax.set_ylim(0, 1.1)
+#ax.set_xlim(0, 100)
+ax.plot(ratio, equilibrium_co2.get_x())
+ax.legend(fs.active_species)
+```
diff --git a/examples/soec_methane.md b/examples/soec_methane.md
new file mode 100644
index 0000000..a2814f4
--- /dev/null
+++ b/examples/soec_methane.md
@@ -0,0 +1,115 @@
+# SOEC with Methane
+
+This example shows a 1D isothermal SOEC (Solid oxide electrolyzer cell) model.
+
+The operating parameters chosen here are not necessary realistic
+
+```python
+import gaspype as gp
+from gaspype import R, F
+import numpy as np
+import matplotlib.pyplot as plt
+```
+
+Calculation of the local equilibrium compositions on the fuel and air
+side in counter flow along the fuel flow direction:
+```python
+fuel_utilization = 0.90
+air_utilization = 0.5
+t = 800 + 273.15 #K
+p = 1e5 #Pa
+
+fs = gp.fluid_system('H2, H2O, O2, CH4, CO, CO2')
+feed_fuel = gp.fluid({'CH4': 1, 'H2O': 0.1}, fs)
+
+o2_full_conv = np.sum(gp.elements(feed_fuel)[['H', 'C' ,'O']] * [1/4, 1, -1/2])
+
+feed_air = gp.fluid({'O2': 1, 'N2': 4}) * o2_full_conv / air_utilization
+
+conversion = np.linspace(0, fuel_utilization, 32)
+perm_oxygen = o2_full_conv * conversion * gp.fluid({'O2': 1})
+
+fuel_side = gp.equilibrium(feed_fuel + perm_oxygen, t, p)
+air_side = gp.equilibrium(feed_air - perm_oxygen, t, p)
+```
+
+Plot compositions of the fuel and air side:
+```python
+fig, ax = plt.subplots()
+ax.set_xlabel("Conversion")
+ax.set_ylabel("Molar fraction")
+ax.plot(conversion, fuel_side.get_x(), '-')
+ax.legend(fuel_side.species)
+
+fig, ax = plt.subplots()
+ax.set_xlabel("Conversion")
+ax.set_ylabel("Molar fraction")
+ax.plot(conversion, air_side.get_x(), '-')
+ax.legend(air_side.species)
+```
+
+Calculation of the oxygen partial pressures:
+```python
+o2_fuel_side = gp.oxygen_partial_pressure(fuel_side, t, p)
+o2_air_side = air_side.get_x('O2') * p
+```
+
+Plot oxygen partial pressures:
+```python
+fig, ax = plt.subplots()
+ax.set_xlabel("Conversion")
+ax.set_ylabel("Oxygen partial pressure / Pa")
+ax.set_yscale('log')
+ax.plot(conversion, np.stack([o2_fuel_side, o2_air_side], axis=1), '-')
+ax.legend(['o2_fuel_side', 'o2_air_side'])
+```
+
+Calculation of the local nernst potential between fuel and air side:
+```python
+z_O2 = 4
+nernst_voltage = R*t / (z_O2*F) * np.log(o2_air_side/o2_fuel_side)
+```
+
+#Plot nernst potential:
+```python
+fig, ax = plt.subplots()
+ax.set_xlabel("Conversion")
+ax.set_ylabel("Voltage / V")
+ax.plot(conversion, nernst_voltage, '-')
+print(np.min(nernst_voltage))
+```
+
+The model uses between each node a constant conversion. Because
+current density depends strongly on the position along the cell
+the constant conversion does not relate to a constant distance.
+
+
+
+To calculate the local current density (**node_current**) as well
+as the total cell current (**terminal_current**) the (relative)
+physical distance between the nodes (**dz**) must be calculated:
+```python
+cell_voltage = 0.77 #V
+ASR = 0.2 #Ohm*cm²
+
+node_current = (nernst_voltage - cell_voltage) / ASR # mA/cm² (Current density at each node)
+
+current = (node_current[1:] + node_current[:-1]) / 2 # mA/cm² (Average current density between the nodes)
+
+dz = 1/current / np.sum(1/current) # Relative distance between each node
+
+terminal_current = np.sum(current * dz) # mA/cm² (Total cell current per cell area)
+
+print(f'Terminal current: {terminal_current:.2f} A/cm²')
+```
+
+Plot the local current density:
+```python
+z_position = np.concatenate([[0], np.cumsum(dz)]) # Relative position of each node
+
+fig, ax = plt.subplots()
+ax.set_xlabel("Relative cell position")
+ax.set_ylabel("Current density / A/cm²")
+ax.plot(z_position, node_current, '-')
+```
+
diff --git a/examples/sulfur_oxygen_equalibrium.md b/examples/sulfur_oxygen_equalibrium.md
new file mode 100644
index 0000000..4831ec3
--- /dev/null
+++ b/examples/sulfur_oxygen_equalibrium.md
@@ -0,0 +1,52 @@
+# Sulfur Oxygen Equilibrium
+
+This example shows equilibrium calculations for sulfur/oxygen mixtures.
+
+```python
+import gaspype as gp
+import numpy as np
+import matplotlib.pyplot as plt
+```
+
+List possible sulfur/oxygen species:
+
+```python
+gp.species(element_names = 'S, O')
+```
+
+Or more specific by using regular expressions:
+
+
+```python
+gp.species('S?[2-3]?O?[2-5]?', use_regex=True)
+```
+
+Calculation of the molar equilibrium fractions for sulfur and oxygen depending on the oxygen to sulfur ratio:
+
+
+```python
+fs = gp.fluid_system(['S2', 'S2O', 'SO2', 'SO3', 'O2'])
+
+oxygen_ratio = np.linspace(0.5, 3, num=128)
+el = gp.elements({'S': 1}, fs) + oxygen_ratio * gp.elements({'O': 1}, fs)
+
+composition = gp.equilibrium(el, 800+273.15, 1e4)
+
+plt.plot(oxygen_ratio, composition.get_x())
+plt.legend(composition.species)
+```
+
+Calculation of the molar equilibrium fractions for sulfur and oxygen depending on temperature in °C:
+
+
+```python
+fs = gp.fluid_system(['S2', 'S2O', 'SO2', 'SO3', 'O2'])
+
+el = gp.elements({'S': 1, 'O':2.5}, fs)
+
+t_range = np.linspace(500, 1300, num=32)
+composition = gp.equilibrium(el, t_range+273.15, 1e4)
+
+plt.plot(t_range, composition.get_x())
+plt.legend(composition.species)
+```
diff --git a/pyproject.toml b/pyproject.toml
index 2e38725..f7db5c2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -41,7 +41,20 @@ dev = [
"cantera",
"pyyaml>=6.0.1",
"types-PyYAML",
- "scipy-stubs"
+ "scipy-stubs",
+ "matplotlib"
+]
+doc_build = [
+ "sphinx",
+ "pydata_sphinx_theme",
+ "sphinx-autodoc-typehints",
+ "myst-parser",
+ "pandas",
+ "matplotlib",
+ "ipykernel",
+ "jupyter",
+ "nbconvert",
+ "notedown"
]
[tool.mypy]
diff --git a/src/gaspype/__init__.py b/src/gaspype/__init__.py
index 579fbe5..e269c34 100644
--- a/src/gaspype/__init__.py
+++ b/src/gaspype/__init__.py
@@ -29,9 +29,9 @@ p_atm = 101325 # Pa
_epsy = 1e-18
-def lookup(prop_array: FloatArray,
- temperature: FloatArray | float,
- t_offset: float) -> FloatArray:
+def _lookup(prop_array: FloatArray,
+ temperature: FloatArray | float,
+ t_offset: float) -> FloatArray:
"""linear interpolates values from the given prop_array
Args:
@@ -58,9 +58,9 @@ def species(pattern: str = '*', element_names: str | list[str] = [], use_regex:
Args:
pattern: Optional filter for specific molecules
Placeholder characters:
- # A number including non written ones: 'C#H#' matches 'CH4'
- $ Arbitrary element name
- * Any sequence of characters
+ # A number including non written ones: 'C#H#' matches 'CH4';
+ $ Arbitrary element name;
+ * Any sequence of characters
element_names:
restrict results to species that contain only the specified elements.
The elements can be supplied as list of strings or as comma separated string.
@@ -95,7 +95,17 @@ def species(pattern: str = '*', element_names: str | list[str] = [], use_regex:
def set_solver(solver: Literal['gibs minimization', 'system of equations']) -> None:
- """Select a solver for chemical equilibrium.
+ """
+ Select a solver for chemical equilibrium.
+
+ Solvers:
+ - **system of equations** (default): Finds the root for a system of
+ equations covering a minimal set of equilibrium equations and elemental balance.
+ The minimal set of equilibrium equations is derived by SVD using the null_space
+ implementation of scipy.
+
+ - **gibs minimization**: Minimizes the total Gibbs Enthalpy while keeping
+ the elemental composition constant using the SLSQP implementation of scipy
Args:
solver: Name of the solver
@@ -127,10 +137,10 @@ class fluid_system:
Attributes:
species_names (list[str]): List of selected species in the fluid_system
- array_molar_mass: Array of the molar masses of the species in the fluid_system
- array_element_composition: Array of the element composition of the species in the fluid_system.
+ array_molar_mass (FloatArray): Array of the molar masses of the species in the fluid_system
+ array_element_composition (FloatArray): Array of the element composition of the species in the fluid_system.
Dimension is: (number of species, number of elements)
- array_atomic_mass: Array of the atomic masses of the elements in the fluid_system
+ array_atomic_mass (FloatArray): Array of the atomic masses of the elements in the fluid_system
"""
def __init__(self, species: list[str] | str, t_min: int = 250, t_max: int = 2000):
@@ -202,7 +212,7 @@ class fluid_system:
Returns:
Array with the enthalpies of each specie in J/mol
"""
- return lookup(self._h_array, t, self._t_offset)
+ return _lookup(self._h_array, t, self._t_offset)
def get_species_s(self, t: float | FloatArray) -> FloatArray:
"""Get the molar entropies for all species in the fluid system
@@ -213,7 +223,7 @@ class fluid_system:
Returns:
Array with the entropies of each specie in J/mol/K
"""
- return lookup(self._s_array, t, self._t_offset)
+ return _lookup(self._s_array, t, self._t_offset)
def get_species_cp(self, t: float | FloatArray) -> FloatArray:
"""Get the isobaric molar heat capacity for all species in the fluid system
@@ -224,7 +234,7 @@ class fluid_system:
Returns:
Array with the heat capacities of each specie in J/mol/K
"""
- return lookup(self._cp_array, t, self._t_offset)
+ return _lookup(self._cp_array, t, self._t_offset)
# def get_species_g(self, t: float | NDArray[_Float]) -> NDArray[_Float]:
# return lookup(self._g_array, t, self._t_offset)
@@ -239,7 +249,7 @@ class fluid_system:
Returns:
Array of gibbs free energy divided by RT (dimensionless)
"""
- return lookup(self._g_rt_array, t, self._t_offset)
+ return _lookup(self._g_rt_array, t, self._t_offset)
def get_species_references(self) -> str:
"""Get a string with the references for all fluids of the fluid system
@@ -263,13 +273,12 @@ class fluid:
one or more species.
Attributes:
- fs: Reference to the fluid_system used for this fluid
- species: List of species names in the associated fluid_system
- array_composition: Array of the molar amounts of the species in the fluid
- array_element_composition: Array of the element composition in the fluid
- array_fractions: Array of the molar fractions of the species in the fluid
- total: Array of the sums of the molar amount of all species
- fs: Reference to the fluid_system used for this fluid
+ species (list[str]): List of species names in the associated fluid_system
+ array_composition (FloatArray): Array of the molar amounts of the species in the fluid
+ array_element_composition (FloatArray): Array of the element composition in the fluid
+ array_fractions (FloatArray): Array of the molar fractions of the species in the fluid
+ total (FloatArray | float): Array of the sums of the molar amount of all species
+ fs (fluid_system): Reference to the fluid_system used for this fluid
"""
__array_priority__ = 100
@@ -606,7 +615,7 @@ class elements:
"""Represent a fluid by composition of elements.
Attributes:
- array_element_composition: Array of the element composition
+ array_element_composition (FloatArray): Array of the element composition
"""
__array_priority__ = 100
diff --git a/tests/md_to_code.py b/tests/md_to_code.py
new file mode 100644
index 0000000..2d9c7dc
--- /dev/null
+++ b/tests/md_to_code.py
@@ -0,0 +1,128 @@
+import re
+from typing import Generator, Iterable
+from dataclasses import dataclass
+import sys
+
+
+@dataclass
+class markdown_segment:
+ code_block: bool
+ language: str
+ text: str
+
+
+def convert_to(target_format: str, md_filename: str, out_filename: str, language: str = 'python'):
+ with open(md_filename, "r") as f_in, open(out_filename, "w") as f_out:
+ segments = segment_markdown(f_in)
+
+ if target_format == 'test':
+ f_out.write('\n'.join(segments_to_test(segments, language)))
+ elif target_format == 'script':
+ f_out.write('\n'.join(segments_to_script(segments, language)))
+ elif target_format == 'striped_markdown':
+ f_out.write('\n'.join(segments_to_striped_markdown(segments, language)))
+ else:
+ raise ValueError('Unknown target format')
+
+
+def segment_markdown(markdown_file: Iterable[str]) -> Generator[markdown_segment, None, None]:
+ regex = re.compile(r"(?:^```\s*(?P(?:\w|-)*)$)", re.MULTILINE)
+
+ block_language: str = ''
+ code_block = False
+ line_buffer: list[str] = []
+
+ for line in markdown_file:
+ match = regex.match(line)
+ if match:
+ if line_buffer:
+ yield markdown_segment(code_block, block_language, ''.join(line_buffer))
+ line_buffer.clear()
+ block_language = match.group('language')
+ code_block = not code_block
+ else:
+ line_buffer.append(line)
+
+ if line_buffer:
+ yield markdown_segment(code_block, block_language, '\n'.join(line_buffer))
+
+
+def segments_to_script(segments: Iterable[markdown_segment], test_language: str = "python") -> Generator[str, None, None]:
+ for segment in segments:
+ if segment.code_block:
+ if segment.language == test_language:
+ yield segment.text
+
+ else:
+ for line in segment.text.splitlines():
+ yield '# | ' + line
+ yield ''
+
+ else:
+ for line in segment.text.strip(' \n').splitlines():
+ yield '# ' + line
+ yield ''
+
+
+def segments_to_striped_markdown(segments: Iterable[markdown_segment], test_language: str = "python") -> Generator[str, None, None]:
+ for segment in segments:
+ if segment.code_block:
+ if segment.language == test_language:
+ yield "``` " + test_language
+ yield segment.text
+ yield "```"
+
+ elif segment.language:
+ for line in segment.text.splitlines():
+ yield '# | ' + line
+ yield ''
+
+ else:
+ for line in segment.text.strip(' \n').splitlines():
+ yield '# ' + line
+ yield ''
+
+
+def segments_to_test(segments: Iterable[markdown_segment], script_language: str = "python") -> Generator[str, None, None]:
+
+ ret_block_flag = False
+
+ yield 'def run_test():'
+
+ for segment in segments:
+ if segment.code_block:
+ if segment.language == script_language:
+ lines = [line for line in segment.text.splitlines() if line.strip()]
+ ret_block_flag = lines[-1] if (not re.match(r'^[^(]*=', lines[-1]) and
+ not lines[-1].startswith('import ') and
+ not lines[-1].startswith('from ') and
+ not lines[-1].startswith(' ')) else None
+ # print('Last line: ', ret_block_flag, '-----------', lines[-1])
+
+ yield ''
+ yield ' print("---------------------------------------------------------")'
+ yield ''
+ if ret_block_flag:
+ yield from [' ' + str(line) for line in segment.text.splitlines()[:-1]]
+ yield f' print("-- Result (({ret_block_flag})):")'
+ yield f' print(({ret_block_flag}).__repr__().strip())'
+ else:
+ yield from [' ' + str(line) for line in segment.text.splitlines()]
+
+ elif ret_block_flag:
+ yield ' ref_str = r"""'
+ yield from [str(line) for line in segment.text.splitlines()]
+ yield '"""'
+ yield f' print("-- Reference (({ret_block_flag})):")'
+ yield ' print(ref_str.strip())'
+ yield f' assert ({ret_block_flag}).__repr__().strip() == ref_str.strip()'
+ ret_block_flag = False
+
+ yield '\nif __name__ == "__main__":'
+ yield ' run_test()'
+
+
+if __name__ == "__main__":
+ format = sys.argv[1]
+ assert format in ['test', 'script']
+ convert_to(sys.argv[1], sys.argv[2], sys.argv[3])
diff --git a/tests/test_doc_examples.py b/tests/test_doc_examples.py
index 9963d37..61222a4 100644
--- a/tests/test_doc_examples.py
+++ b/tests/test_doc_examples.py
@@ -1,53 +1,28 @@
-import re
-from typing import Generator
-
-
-def convert_markdown_file(md_filename: str, out_filename: str):
- with open(md_filename, "r") as f_in:
- with open(out_filename, "w") as f_out:
- f_out.write('def run_test():\n')
- for block in markdown_to_code([line for line in f_in]):
- f_out.write(block + '\n')
-
-
-def markdown_to_code(lines: list[str], language: str = "python") -> Generator[str, None, None]:
- regex = re.compile(
- r"(?P^```\s*(?P(\w|-)*)\n)(?P.*?\n)(?P```)",
- re.DOTALL | re.MULTILINE,
- )
- blocks = [
- (match.group("block_language"), match.group("code"))
- for match in regex.finditer("".join(lines))
- ]
-
- ret_block_flag = False
-
- for block_language, block in blocks:
- if block_language == language:
- lines = [line for line in block.splitlines() if line.strip()]
- ret_block_flag = lines[-1] if '=' not in lines[-1] else None
-
- yield ''
- yield ' print("---------------------------------------------------------")'
- yield ''
- if ret_block_flag:
- yield from [' ' + str(line) for line in block.splitlines()[:-1]]
- else:
- yield from [' ' + str(line) for line in block.splitlines()]
- yield f' print("-- Result (({ret_block_flag})):")'
- yield f' print(({ret_block_flag}).__repr__().strip())'
-
- elif ret_block_flag:
- yield ' ref_str = r"""'
- yield from [str(line) for line in block.splitlines()]
- yield '"""'
- yield f' print("-- Reference (({ret_block_flag})):")'
- yield ' print(ref_str.strip())'
- yield f' assert ({ret_block_flag}).__repr__().strip() == ref_str.strip()'
- ret_block_flag = False
+import md_to_code
+from glob import glob
+import importlib
+import os
def test_readme():
- convert_markdown_file('README.md', 'tests/autogenerated_readme.py')
+ md_to_code.convert_to('test', 'README.md', 'tests/autogenerated_readme.py')
import autogenerated_readme
autogenerated_readme.run_test()
+
+
+def test_example_code():
+ filter = 'examples/*.md'
+
+ files = glob(filter)
+ for path in files:
+ file_name = '.'.join(os.path.basename(path).split('.')[:-1])
+ if not file_name.lower() == 'readme':
+ print(f"> Test Example {file_name} ...")
+ md_to_code.convert_to('test', path, f'tests/autogenerated_{file_name}.py')
+ mod = importlib.import_module(f'autogenerated_{file_name}')
+ mod.run_test()
+
+
+if __name__ == "__main__":
+ test_readme()
+ test_example_code()