From 360683a6334655a08b6e5613658e9a83556f3f57 Mon Sep 17 00:00:00 2001 From: Nicolas Kruse Date: Wed, 4 Jun 2025 17:53:37 +0200 Subject: [PATCH] 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