From 360683a6334655a08b6e5613658e9a83556f3f57 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Wed, 4 Jun 2025 17:53:37 +0200 Subject: [PATCH 01/18] md_to_code updated to generate tests and human readable example code from markdown scripts --- docs/source/examples/soec_methane.md | 98 +++++++++++++++++++++ tests/md_to_code.py | 125 +++++++++++++++++++++++++++ tests/test_doc_examples.py | 56 ++---------- 3 files changed, 230 insertions(+), 49 deletions(-) create mode 100644 docs/source/examples/soec_methane.md create mode 100644 tests/md_to_code.py diff --git a/docs/source/examples/soec_methane.md b/docs/source/examples/soec_methane.md new file mode 100644 index 0000000..5e4fb61 --- /dev/null +++ b/docs/source/examples/soec_methane.md @@ -0,0 +1,98 @@ +```python +import gaspype as gp +from gaspype import R, F +import numpy as np +import matplotlib.pyplot as plt +``` + + +```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) +``` + + +```python +#Plot compositions on fuel and air side +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) +``` + +```python +o2_fuel_side = gp.oxygen_partial_pressure(fuel_side, t, p) +o2_air_side = air_side.get_x('O2') * p +``` + +```python +#Plot oxygen partial pressure +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']) +``` + +```python +z_O2 = 4 +nernst_voltage = R*t / (z_O2*F) * np.log(o2_air_side/o2_fuel_side) +``` + +```python +#Plot voltage potential +fig, ax = plt.subplots() +ax.set_xlabel("Conversion") +ax.set_ylabel("Voltage / V") +ax.plot(conversion, nernst_voltage, '-') +print(np.min(nernst_voltage)) +``` + +```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²') +``` + +```python +#Plot current density +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/tests/md_to_code.py b/tests/md_to_code.py new file mode 100644 index 0000000..a2ea263 --- /dev/null +++ b/tests/md_to_code.py @@ -0,0 +1,125 @@ + + +import re +from typing import Generator, Iterable, Literal +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 ') 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]) \ No newline at end of file diff --git a/tests/test_doc_examples.py b/tests/test_doc_examples.py index 9963d37..375abbd 100644 --- a/tests/test_doc_examples.py +++ b/tests/test_doc_examples.py @@ -1,53 +1,11 @@ -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 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_soec_example(): + md_to_code.convert_to('test', 'docs/source/examples/soec_methane.md', 'tests/autogenerated_soec_example.py') + import autogenerated_soec_example + autogenerated_soec_example.run_test() \ No newline at end of file From ddd543abf00b49a2f208b09f2bb88d2417907d28 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Wed, 4 Jun 2025 17:54:09 +0200 Subject: [PATCH 02/18] line breaks adjusted in readme --- README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) 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}) From 16d1d053d7e6912a22b29aa19da4e4187fb65e94 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Wed, 4 Jun 2025 18:00:01 +0200 Subject: [PATCH 03/18] readme for examples added --- examples/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 examples/README.md diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..ea6c379 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,12 @@ +# Example scripts + +``` bash +notedown .\docs\source\examples\soec_methane.md --to notebook --output .\docs\files\soec_methane.ipynb --run + +maybe: pip install ipykernel jupyter +maybe: python -m ipykernel install --user --name temp_kernel --display-name "Python (temp_kernel)" + + +jupyter nbconvert --to markdown .\docs\files\soec_methane.ipynb --output .\docs\files\soec_methane_out.md +``` + From cf7c5ebe950302fba031241822c5a3bec8dea9d8 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Wed, 4 Jun 2025 18:00:28 +0200 Subject: [PATCH 04/18] doc_build dependencies in project file added --- pyproject.toml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index a494b59..add4009 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,18 @@ dev = [ "types-PyYAML", "scipy-stubs" ] +doc_build = [ + "sphinx", + "sphinx_rtd_theme", + "sphinx-autodoc-typehints", + "myst-parser", + "pandas", + "matplotlib", + "ipykernel", + "jupyter", + "nbconvert", + "notedown" +] [tool.mypy] files = ["src"] From 60a8f99dafc4eae38bed3c0870eb0f2f9767696a Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Wed, 4 Jun 2025 18:00:40 +0200 Subject: [PATCH 05/18] drawing for example added --- docs/media/soc_inverted.svg | 193 ++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 docs/media/soc_inverted.svg 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 @@ + + + + + + + + + + + + + + + + + + + + + Relative cell lenghs + 1 + 0 + + + + Δz + Current + + + + From e30b4f1d47e79d3ddd2686a7c6df0306e1745857 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 09:02:52 +0200 Subject: [PATCH 06/18] files rearranged --- docs/source/readme.md | 2 -- {docs/source/examples => examples}/soec_methane.md | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 docs/source/readme.md rename {docs/source/examples => examples}/soec_methane.md (99%) 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/examples/soec_methane.md b/examples/soec_methane.md similarity index 99% rename from docs/source/examples/soec_methane.md rename to examples/soec_methane.md index 5e4fb61..d7d0e7e 100644 --- a/docs/source/examples/soec_methane.md +++ b/examples/soec_methane.md @@ -1,3 +1,5 @@ +# SOEC example + ```python import gaspype as gp from gaspype import R, F From 793b2a0ab461d9baee38d16574b56fb21cfcd119 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 09:04:42 +0200 Subject: [PATCH 07/18] doc build script and settings updated --- docs/source/conf.py | 5 +-- docs/source/generate_class_list.py | 58 ++++++++++++++++++++---------- docs/source/index.md | 9 +++-- 3 files changed, 46 insertions(+), 26 deletions(-) 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/generate_class_list.py b/docs/source/generate_class_list.py index 8fa77cf..605b699 100644 --- a/docs/source/generate_class_list.py +++ b/docs/source/generate_class_list.py @@ -1,3 +1,5 @@ +# This script generates the source md-files for all classes and functions for the docs + import importlib import inspect import fnmatch @@ -16,21 +18,21 @@ def write_classes(f: TextIOWrapper, patterns: list[str], module_name: str, title ] """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: @@ -44,18 +46,36 @@ def write_functions(f: TextIOWrapper, patterns: list[str], module_name: str, tit ] """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') + #f.write('## Classes\n\n') + write_classes(f, ['*'], 'gaspype', title='Classes') + + #f.write('## Functions\n\n') + write_functions(f, ['*'], 'gaspype', title='Functions') 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 From 6c3437f5095b332f763574acd4a423793c6712d0 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 09:05:40 +0200 Subject: [PATCH 08/18] example unit test updated to test all examples from example directory --- tests/md_to_code.py | 12 ++++++------ tests/test_doc_examples.py | 25 +++++++++++++++++++++---- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/tests/md_to_code.py b/tests/md_to_code.py index a2ea263..bb009e7 100644 --- a/tests/md_to_code.py +++ b/tests/md_to_code.py @@ -1,10 +1,9 @@ - - import re -from typing import Generator, Iterable, Literal +from typing import Generator, Iterable from dataclasses import dataclass import sys + @dataclass class markdown_segment: code_block: bool @@ -24,7 +23,7 @@ def convert_to(target_format: str, md_filename: str, out_filename: str, language 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) @@ -95,7 +94,7 @@ def segments_to_test(segments: Iterable[markdown_segment], script_language: str 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 ') else None - print('Last line: ', ret_block_flag, '-----------', lines[-1]) + # print('Last line: ', ret_block_flag, '-----------', lines[-1]) yield '' yield ' print("---------------------------------------------------------")' @@ -119,7 +118,8 @@ def segments_to_test(segments: Iterable[markdown_segment], script_language: str 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]) \ No newline at end of file + 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 375abbd..0b498de 100644 --- a/tests/test_doc_examples.py +++ b/tests/test_doc_examples.py @@ -1,11 +1,28 @@ import md_to_code +from glob import glob +import importlib +import os + def test_readme(): md_to_code.convert_to('test', 'README.md', 'tests/autogenerated_readme.py') import autogenerated_readme autogenerated_readme.run_test() -def test_soec_example(): - md_to_code.convert_to('test', 'docs/source/examples/soec_methane.md', 'tests/autogenerated_soec_example.py') - import autogenerated_soec_example - autogenerated_soec_example.run_test() \ No newline at end of file + +def test_example_code(): + filter = 'docs/source/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(file_name) + mod.run_test() + + +if __name__ == "__main__": + test_readme() + test_example_code() From 359120bbcfdd40f6b3eaddaef8457c3edb9d5959 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 09:06:33 +0200 Subject: [PATCH 09/18] project settings updated - e.g. dependencies for building docs and testing --- .gitignore | 2 +- pyproject.toml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) 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/pyproject.toml b/pyproject.toml index add4009..c66f0db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,11 +40,12 @@ dev = [ "cantera", "pyyaml>=6.0.1", "types-PyYAML", - "scipy-stubs" + "scipy-stubs", + "matplotlib" ] doc_build = [ "sphinx", - "sphinx_rtd_theme", + "pydata_sphinx_theme", "sphinx-autodoc-typehints", "myst-parser", "pandas", From 99e543e12a13da0ad62ee8fd0a68362e4ea63d99 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 09:07:43 +0200 Subject: [PATCH 10/18] script for rendering examples added for doc generation --- docs/source/render_examples.py | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 docs/source/render_examples.py diff --git a/docs/source/render_examples.py b/docs/source/render_examples.py new file mode 100644 index 0000000..ce33166 --- /dev/null +++ b/docs/source/render_examples.py @@ -0,0 +1,65 @@ +# 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 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') From b158a868521f756951dfb4c2c63f65fe20447dfc Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 09:08:13 +0200 Subject: [PATCH 11/18] doc build CD updated for rendering examples --- .github/workflows/docs.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index af5beb7..f228970 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,15 +17,16 @@ 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] + - name: Generate Class List + run: python ./docs/source/generate_class_list.py + - name: Generate Examples + run: python ./docs/source/render_examples.py - name: Build Docs run: | cd docs From 9fc5ea2dc111867a16b72e52e72a5990b79c7eec Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 10:11:54 +0200 Subject: [PATCH 12/18] Docstrings updated: types for properties and solver description added --- src/gaspype/__init__.py | 53 ++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/gaspype/__init__.py b/src/gaspype/__init__.py index e5ff204..29c16aa 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 __add__(self, other: 'fluid_system') -> 'fluid_system': assert isinstance(other, self.__class__) @@ -255,13 +265,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 @@ -598,7 +607,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 From 38eda3edcfbdb66ee34b2498a300a8f7789181fa Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 10:12:28 +0200 Subject: [PATCH 13/18] docs for types added --- docs/source/floatarray.md | 5 +++++ docs/source/generate_class_list.py | 15 +++++++++------ docs/source/ndfloat.md | 5 +++++ 3 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 docs/source/floatarray.md create mode 100644 docs/source/ndfloat.md 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 605b699..41a941f 100644 --- a/docs/source/generate_class_list.py +++ b/docs/source/generate_class_list.py @@ -6,8 +6,12 @@ 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 = [ @@ -17,7 +21,6 @@ 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.""" if description: f.write(f'{description}\n\n') @@ -36,7 +39,7 @@ def write_classes(f: TextIOWrapper, patterns: list[str], module_name: str, title 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 = [ @@ -45,7 +48,6 @@ 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.""" if description: f.write(f'{description}\n\n') @@ -74,8 +76,9 @@ def write_dochtree(f: TextIOWrapper, title: str, items: list[str]): if __name__ == "__main__": with open('docs/source/_autogenerated/index.md', 'w') as f: f.write('# Classes and functions\n\n') - #f.write('## Classes\n\n') + write_classes(f, ['*'], 'gaspype', title='Classes') - #f.write('## Functions\n\n') write_functions(f, ['*'], 'gaspype', title='Functions') + + write_manual(f, ['../ndfloat', '../floatarray'], title='Types') 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 From 1c07ffea363fc9cce3728b1e20d3c63e59030c34 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 10:13:34 +0200 Subject: [PATCH 14/18] docs CD: LICENSE added to docs --- .github/workflows/docs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f228970..ccecd04 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,6 +29,7 @@ jobs: 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 From d18ba0f78568565499201babd834d14a248c1e2f Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 10:48:08 +0200 Subject: [PATCH 15/18] Docs CD updated --- .github/workflows/docs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ccecd04..b74661d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,8 @@ jobs: mkdir -p src/gaspype/data printf 'gapy\x00\x00\x00\x00' > src/gaspype/data/therm_data.bin pip install .[doc_build] - - name: Generate Class List + 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 From 0578b552be7736c3a642715c1e93d7a9fe136be0 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 10:48:30 +0200 Subject: [PATCH 16/18] Example-readme updated --- examples/README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/examples/README.md b/examples/README.md index ea6c379..a77d071 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,12 +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 -notedown .\docs\source\examples\soec_methane.md --to notebook --output .\docs\files\soec_methane.ipynb --run +# 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 -maybe: pip install ipykernel jupyter -maybe: python -m ipykernel install --user --name temp_kernel --display-name "Python (temp_kernel)" - - -jupyter nbconvert --to markdown .\docs\files\soec_methane.ipynb --output .\docs\files\soec_methane_out.md +# 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 new_example.ipynb --NbConvertApp.use_output_suffix=False --ClearOutputPreprocessor.enabled=True --output-dir examples/ --output new_example.md +``` \ No newline at end of file From 7671a89e142450fb223ae01b4b9f23ddf70a8f01 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 13:15:34 +0200 Subject: [PATCH 17/18] Examples added, example readme and example unit test updated --- docs/source/render_examples.py | 2 +- examples/README.md | 2 +- examples/carbon_activity.md | 68 +++++++++++++++++++++++++++ examples/methane_mixtures.md | 60 +++++++++++++++++++++++ examples/soec_methane.md | 39 ++++++++++----- examples/sulfur_oxygen_equalibrium.md | 52 ++++++++++++++++++++ tests/md_to_code.py | 5 +- tests/test_doc_examples.py | 4 +- 8 files changed, 215 insertions(+), 17 deletions(-) create mode 100644 examples/carbon_activity.md create mode 100644 examples/methane_mixtures.md create mode 100644 examples/sulfur_oxygen_equalibrium.md diff --git a/docs/source/render_examples.py b/docs/source/render_examples.py index ce33166..d38ad0b 100644 --- a/docs/source/render_examples.py +++ b/docs/source/render_examples.py @@ -16,7 +16,7 @@ def run_cmd(command: list[str]): assert (not result.stderr or any('RuntimeWarning: ' in line for line in result.stderr.splitlines()) or - any('[NbConvertApp]' in line for line in result.stderr.splitlines())), 'ERROR: ' + result.stderr + 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): diff --git a/examples/README.md b/examples/README.md index a77d071..ba40e2f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -21,5 +21,5 @@ jupyter nbconvert --to markdown docs/source/_autogenerated/soec_methane.ipynb -- A new example Markdown file can be created from a Jupyter Notebook running the following command: ``` bash -jupyter nbconvert --to new_example.ipynb --NbConvertApp.use_output_suffix=False --ClearOutputPreprocessor.enabled=True --output-dir examples/ --output new_example.md +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 index d7d0e7e..a2814f4 100644 --- a/examples/soec_methane.md +++ b/examples/soec_methane.md @@ -1,4 +1,8 @@ -# SOEC example +# 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 @@ -7,7 +11,8 @@ 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 @@ -28,9 +33,8 @@ 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 -#Plot compositions on fuel and air side fig, ax = plt.subplots() ax.set_xlabel("Conversion") ax.set_ylabel("Molar fraction") @@ -44,13 +48,14 @@ 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 -#Plot oxygen partial pressure fig, ax = plt.subplots() ax.set_xlabel("Conversion") ax.set_ylabel("Oxygen partial pressure / Pa") @@ -59,13 +64,14 @@ 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 -#Plot voltage potential fig, ax = plt.subplots() ax.set_xlabel("Conversion") ax.set_ylabel("Voltage / V") @@ -73,24 +79,33 @@ 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. + +![Alt text](../../media/soc_inverted.svg) + +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) +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) +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 +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) +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 -#Plot current density -z_position = np.concatenate([[0], np.cumsum(dz)]) #Relative position of each node +z_position = np.concatenate([[0], np.cumsum(dz)]) # Relative position of each node fig, ax = plt.subplots() ax.set_xlabel("Relative cell position") 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/tests/md_to_code.py b/tests/md_to_code.py index bb009e7..2d9c7dc 100644 --- a/tests/md_to_code.py +++ b/tests/md_to_code.py @@ -93,7 +93,10 @@ def segments_to_test(segments: Iterable[markdown_segment], script_language: str 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 ') else None + 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 '' diff --git a/tests/test_doc_examples.py b/tests/test_doc_examples.py index 0b498de..61222a4 100644 --- a/tests/test_doc_examples.py +++ b/tests/test_doc_examples.py @@ -11,7 +11,7 @@ def test_readme(): def test_example_code(): - filter = 'docs/source/examples/*.md' + filter = 'examples/*.md' files = glob(filter) for path in files: @@ -19,7 +19,7 @@ def test_example_code(): 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(file_name) + mod = importlib.import_module(f'autogenerated_{file_name}') mod.run_test() From dafdade8330274d380695a9bbe47735b53efc82c Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Fri, 6 Jun 2025 13:24:46 +0200 Subject: [PATCH 18/18] Excluded autogenerated example code files in flake8 config, code style fixed --- .flake8 | 1 + docs/source/render_examples.py | 1 + 2 files changed, 2 insertions(+) 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/docs/source/render_examples.py b/docs/source/render_examples.py index d38ad0b..b66a25d 100644 --- a/docs/source/render_examples.py +++ b/docs/source/render_examples.py @@ -28,6 +28,7 @@ def run_rendering(input_path: str, output_directory: str): 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')