From e44611807130fbcfd9d9182e6a81bad49fc023ba Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:14 +0100 Subject: [PATCH 1/3] Add PyScript examples for pyreactlab-core Generated by apply_llm_response.py from prompts/pyreactlab-core/response.toml. Examples included: - introduce_a_reaction: Introduce a reaction - stoichiometry_matrix: Stoichiometry matrix for multiple reactions Generated-By: apply_llm_response.py --- examples/pyreactlab-core/README.md | 18 +++++++ .../introduce_a_reaction/code.py | 46 ++++++++++++++++ .../introduce_a_reaction/config.toml | 1 + .../introduce_a_reaction/setup.py | 45 ++++++++++++++++ examples/pyreactlab-core/order.json | 4 ++ .../stoichiometry_matrix/code.py | 52 +++++++++++++++++++ .../stoichiometry_matrix/config.toml | 1 + .../stoichiometry_matrix/setup.py | 24 +++++++++ 8 files changed, 191 insertions(+) create mode 100644 examples/pyreactlab-core/README.md create mode 100644 examples/pyreactlab-core/introduce_a_reaction/code.py create mode 100644 examples/pyreactlab-core/introduce_a_reaction/config.toml create mode 100644 examples/pyreactlab-core/introduce_a_reaction/setup.py create mode 100644 examples/pyreactlab-core/order.json create mode 100644 examples/pyreactlab-core/stoichiometry_matrix/code.py create mode 100644 examples/pyreactlab-core/stoichiometry_matrix/config.toml create mode 100644 examples/pyreactlab-core/stoichiometry_matrix/setup.py diff --git a/examples/pyreactlab-core/README.md b/examples/pyreactlab-core/README.md new file mode 100644 index 0000000..3834d58 --- /dev/null +++ b/examples/pyreactlab-core/README.md @@ -0,0 +1,18 @@ +# pyreactlab-core Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/pyreactlab-core/introduce_a_reaction/code.py b/examples/pyreactlab-core/introduce_a_reaction/code.py new file mode 100644 index 0000000..2441410 --- /dev/null +++ b/examples/pyreactlab-core/introduce_a_reaction/code.py @@ -0,0 +1,46 @@ +""" +A first look at PyReactLab-Core. + +We'll define a chemical reaction as a string and let the package parse +it into structured information: reactants, products, stoichiometric +coefficients, phases, and more. + +Docs: https://pyreactlab-core.readthedocs.io/ +""" +from IPython.core.display import display, HTML + +heading("Methanol synthesis from CO2 and hydrogen") +note( + "We describe the reaction with a compact string. The " + "(g) markers tell PyReactLab the phase of each " + "species, and => separates reactants from products." +) + +methanol_synthesis = Reaction( + name="CO2 Hydrogenation to Methanol", + reaction="CO2(g) + 3H2(g) => CH3OH(g) + H2O(g)", +) + +note(f"Parsed reaction string: {methanol_synthesis.reaction}") + +# A small HTML table summarising the parsed properties. +properties = { + "Reactants": methanol_synthesis.reactants_names, + "Products": methanol_synthesis.products_names, + "Reaction phase": methanol_synthesis.reaction_phase, + "State counts": methanol_synthesis.state_count, + "Stoichiometry": methanol_synthesis.reaction_stoichiometry, + "Carbon count per species": methanol_synthesis.carbon_count, +} + +rows = "".join( + f"{k}" + f"{v}" + for k, v in properties.items() +) +display(HTML(f"{rows}
"), append=True) + +heading("Reactants and products in detail", level=3) +note("Each reactant/product carries its coefficient, molecule, and state:") +display(methanol_synthesis.reactants, append=True) +display(methanol_synthesis.products, append=True) diff --git a/examples/pyreactlab-core/introduce_a_reaction/config.toml b/examples/pyreactlab-core/introduce_a_reaction/config.toml new file mode 100644 index 0000000..5acf836 --- /dev/null +++ b/examples/pyreactlab-core/introduce_a_reaction/config.toml @@ -0,0 +1 @@ +packages = ["pyreactlab-core"] diff --git a/examples/pyreactlab-core/introduce_a_reaction/setup.py b/examples/pyreactlab-core/introduce_a_reaction/setup.py new file mode 100644 index 0000000..1392b50 --- /dev/null +++ b/examples/pyreactlab-core/introduce_a_reaction/setup.py @@ -0,0 +1,45 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + """Wrap pyscript.display so output lands in the example target.""" + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +# Package imports for the first example. +from pyreactlab_core.models.reaction import Reaction diff --git a/examples/pyreactlab-core/order.json b/examples/pyreactlab-core/order.json new file mode 100644 index 0000000..33a06f0 --- /dev/null +++ b/examples/pyreactlab-core/order.json @@ -0,0 +1,4 @@ +[ + "introduce_a_reaction", + "stoichiometry_matrix" +] diff --git a/examples/pyreactlab-core/stoichiometry_matrix/code.py b/examples/pyreactlab-core/stoichiometry_matrix/code.py new file mode 100644 index 0000000..3efdd48 --- /dev/null +++ b/examples/pyreactlab-core/stoichiometry_matrix/code.py @@ -0,0 +1,52 @@ +# --------------------------------------------------------------------- +# Section 2: Build a stoichiometry matrix for a small reaction network. +# --------------------------------------------------------------------- + +heading("A two-reaction hydrogenation network") +note( + "PyReactLab can take a list of reactions and assemble the " + "stoichiometry matrix you'd use in reactor design or " + "reaction-engineering math. Negative entries mean a species is " + "consumed; positive entries mean it's produced." +) + +# Use the convenience constructor `rxn` to build Reaction instances. +methanol_synthesis = rxn( + reaction_str="CO2(g) + 3H2(g) => CH3OH(g) + H2O(g)", + name="CO2 Hydrogenation to Methanol", +) + +ethylene_hydrogenation = rxn( + reaction_str="C2H4(g) + H2(g) => C2H6(g)", + name="Ethylene Hydrogenation to Ethane", +) + +reactions = [methanol_synthesis, ethylene_hydrogenation] + +# Assemble the stoichiometry matrix across the union of all species. +matrix_info = rxns_stoichiometry(reactions=reactions) + +components = matrix_info["components"] +rows_dict = matrix_info["stoichiometry_matrices_dict"] + +# Wrap the result in a pandas DataFrame for a readable display. +stoich_df = pd.DataFrame( + rows_dict, + index=[r.name for r in reactions], + columns=components, +) + +note("Rows are reactions, columns are species (with phase suffixes):") +display(stoich_df, append=True) + +heading("Quick sanity checks", level=3) + +# Net change per reaction: should be zero only if mass-and-mole balanced +# across this column set; for these reactions, methanol synthesis has a +# net change in moles of gas, which is reaction-engineering reality. +net_change = stoich_df.sum(axis=1).rename("net mole change") +display(net_change.to_frame(), append=True) + +# Which reactions consume H2? +h2_consumers = stoich_df.index[stoich_df["H2-g"] < 0].tolist() +note(f"Reactions that consume H2(g): {h2_consumers}") diff --git a/examples/pyreactlab-core/stoichiometry_matrix/config.toml b/examples/pyreactlab-core/stoichiometry_matrix/config.toml new file mode 100644 index 0000000..ec65502 --- /dev/null +++ b/examples/pyreactlab-core/stoichiometry_matrix/config.toml @@ -0,0 +1 @@ +packages = ["pyreactlab-core", "pandas"] diff --git a/examples/pyreactlab-core/stoichiometry_matrix/setup.py b/examples/pyreactlab-core/stoichiometry_matrix/setup.py new file mode 100644 index 0000000..093f36f --- /dev/null +++ b/examples/pyreactlab-core/stoichiometry_matrix/setup.py @@ -0,0 +1,24 @@ +"""Setup for the stoichiometry matrix example.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import pandas as pd +from pyreactlab_core.models.reaction import Reaction +from pyreactlab_core import rxn, rxns_stoichiometry From 4367454bd41f8bf0b5d4500c95d17665c00f8ca1 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 11 Jun 2026 15:28:03 +0100 Subject: [PATCH 2/3] Fix second example and imports. --- .../introduce_a_reaction/code.py | 3 ++ .../introduce_a_reaction/setup.py | 3 -- .../stoichiometry_matrix/code.py | 38 ++++++++++++++----- .../stoichiometry_matrix/setup.py | 4 -- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/examples/pyreactlab-core/introduce_a_reaction/code.py b/examples/pyreactlab-core/introduce_a_reaction/code.py index 2441410..8c8095e 100644 --- a/examples/pyreactlab-core/introduce_a_reaction/code.py +++ b/examples/pyreactlab-core/introduce_a_reaction/code.py @@ -8,6 +8,9 @@ Docs: https://pyreactlab-core.readthedocs.io/ """ from IPython.core.display import display, HTML +# Package imports for the first example. +from pyreactlab_core.models.reaction import Reaction + heading("Methanol synthesis from CO2 and hydrogen") note( diff --git a/examples/pyreactlab-core/introduce_a_reaction/setup.py b/examples/pyreactlab-core/introduce_a_reaction/setup.py index 1392b50..07879f9 100644 --- a/examples/pyreactlab-core/introduce_a_reaction/setup.py +++ b/examples/pyreactlab-core/introduce_a_reaction/setup.py @@ -40,6 +40,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -# Package imports for the first example. -from pyreactlab_core.models.reaction import Reaction diff --git a/examples/pyreactlab-core/stoichiometry_matrix/code.py b/examples/pyreactlab-core/stoichiometry_matrix/code.py index 3efdd48..baef7c9 100644 --- a/examples/pyreactlab-core/stoichiometry_matrix/code.py +++ b/examples/pyreactlab-core/stoichiometry_matrix/code.py @@ -1,6 +1,10 @@ # --------------------------------------------------------------------- # Section 2: Build a stoichiometry matrix for a small reaction network. # --------------------------------------------------------------------- +import pandas as pd +from pyreactlab_core.models.reaction import Reaction +from pyreactlab_core import rxn, rxns_stoichiometry + heading("A two-reaction hydrogenation network") note( @@ -26,27 +30,43 @@ # Assemble the stoichiometry matrix across the union of all species. matrix_info = rxns_stoichiometry(reactions=reactions) -components = matrix_info["components"] -rows_dict = matrix_info["stoichiometry_matrices_dict"] +# Note: despite the "_dict" suffix, `stoichiometry_matrices_dict` is a +# list of per-reaction dicts (one {species: coefficient} mapping per +# reaction), aligned with the input `reactions` order. Pandas handles +# this shape natively when given a list of records, inferring columns +# from the union of keys. +rows = matrix_info["stoichiometry_matrices_dict"] -# Wrap the result in a pandas DataFrame for a readable display. stoich_df = pd.DataFrame( - rows_dict, + rows, index=[r.name for r in reactions], - columns=components, ) +# Reindex to the canonical species ordering from `component_list`, so +# the column order is stable rather than dict-iteration-dependent. +stoich_df = stoich_df.reindex(columns=matrix_info["component_list"]) + note("Rows are reactions, columns are species (with phase suffixes):") display(stoich_df, append=True) heading("Quick sanity checks", level=3) -# Net change per reaction: should be zero only if mass-and-mole balanced -# across this column set; for these reactions, methanol synthesis has a -# net change in moles of gas, which is reaction-engineering reality. +# Net mole change per reaction: sum of stoichiometric coefficients across +# all species. A negative value means the reaction consumes more moles of +# gas than it produces, which matters for reactor pressure and volume +# calculations. (This is not a mass balance — that would require weighting +# each coefficient by the species' molecular weight.) net_change = stoich_df.sum(axis=1).rename("net mole change") display(net_change.to_frame(), append=True) +note( + "Methanol synthesis shows a net change of -2 moles: four moles of " + "gas in (1 CO2 + 3 H2) become two moles out " + "(1 CH3OH + 1 H2O). Ethylene hydrogenation " + "loses one mole (2 in, 1 out). Both reactions favour higher " + "pressure by Le Chatelier's principle." +) + # Which reactions consume H2? h2_consumers = stoich_df.index[stoich_df["H2-g"] < 0].tolist() -note(f"Reactions that consume H2(g): {h2_consumers}") +note(f"Reactions that consume H2(g): {h2_consumers}") \ No newline at end of file diff --git a/examples/pyreactlab-core/stoichiometry_matrix/setup.py b/examples/pyreactlab-core/stoichiometry_matrix/setup.py index 093f36f..475e0e4 100644 --- a/examples/pyreactlab-core/stoichiometry_matrix/setup.py +++ b/examples/pyreactlab-core/stoichiometry_matrix/setup.py @@ -18,7 +18,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -import pandas as pd -from pyreactlab_core.models.reaction import Reaction -from pyreactlab_core import rxn, rxns_stoichiometry From 61e5774056ba907363b2952634f9e5ad68a09f03 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 26 Jun 2026 10:49:29 +0100 Subject: [PATCH 3/3] Add version_info to IPython shim for PyScript update. --- examples/pyreactlab-core/introduce_a_reaction/setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/pyreactlab-core/introduce_a_reaction/setup.py b/examples/pyreactlab-core/introduce_a_reaction/setup.py index 07879f9..a536bd1 100644 --- a/examples/pyreactlab-core/introduce_a_reaction/setup.py +++ b/examples/pyreactlab-core/introduce_a_reaction/setup.py @@ -25,6 +25,7 @@ def display(*args, **kwargs): core_display.HTML = HTML ipython.core = core core.display = core_display +ipython.version_info = (9, 0, 2, '') ipython.get_ipython = lambda: None ipython.display = core_display sys.modules["IPython"] = ipython