Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions docs/documentation/thermochemistry.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# Mechanism generation

MFC owns the Fortran thermochemistry generator in `toolchain/mfc/thermochem/`.
Cantera loads a mechanism, and `generate_fortran` produces `m_thermochem.f90`
in the target's build staging directory. The existing CMake build compiles that
Cantera loads a mechanism, and `generate_fortran` produces the Fypp source
`m_thermochem.fpp` in the target's build staging directory. The existing CMake build compiles that
module into MFC. Neither Cantera nor Python is called inside the flow solver.
Pyrometheus and JAX are not installation or runtime requirements.

Expand All @@ -18,7 +18,7 @@ renders the Fortran template. Neither is a runtime solver dependency.
|---|---|
| `thermochem/expressions.py` | NASA7, reaction-rate, equilibrium and transport expressions |
| `thermochem/fortran.py` | Supported-feature checks, expression formatting and module generation |
| `thermochem/module.f90.mako` | Fortran interface and numerical routines |
| `thermochem/module.fpp.mako` | Fortran interface and numerical routines |
| `thermochem/fingerprint.py` | Mechanism and generator content identities for build reuse |
| `run/input.py` | Mechanism resolution and generation for each target |

Expand All @@ -33,8 +33,10 @@ The generated module provides species metadata, caloric and ideal-gas properties
temperature inversion, net production rates, fused creation/destruction rates,
mixture viscosity and thermal conductivity, and species diffusivities. MFC still
owns reaction time integration, including alpha-QSS, and spatial transport
discretization. The generator emits single- or double-precision routines and the
selected CPU, OpenACC or OpenMP annotations. Nonchemistry builds retain the existing
discretization. The module follows MFC's source conventions: it uses `wp` from
`m_precision_select` and marks device routines with `$:GPU_ROUTINE`, so one generated
source serves every precision and offload configuration and compiler-specific
directive handling stays in MFC's macros. Nonchemistry builds retain the existing
dummy `h2o2.yaml` module to satisfy the shared Fortran interfaces.

The initial ownership change preserves the previous numerical formulas. In
Expand Down Expand Up @@ -91,7 +93,8 @@ hydrogen/xenon mechanism. They cover thermodynamics, energy/enthalpy inversion,
reaction production and destruction, elemental conservation, transport, single
precision, mixed-storage working-precision compatibility, long species names,
zero-concentration falloff with floating-point exception traps, and compilation
with OpenACC and OpenMP directives. The directive tests
with OpenACC and OpenMP directives. They run the generated source through Fypp with
MFC's macros, as the build does. The directive tests
execute on the host; they do not validate GPU offload on accelerator hardware.

Initialization tests check stream limits, normalization, elemental composition,
Expand Down
23 changes: 6 additions & 17 deletions toolchain/mfc/run/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# to avoid slow startup times for commands that don't use chemistry features
# Note: build is imported lazily to avoid circular import with build.py
from ..printer import cons
from ..state import ARG, ARGS, gpuConfigOptions
from ..state import ARGS


@dataclasses.dataclass(init=False)
Expand Down Expand Up @@ -83,22 +83,11 @@ def generate_fpp(self, target) -> None:
modules_dir = os.path.join(target.get_staging_dirpath(self), "modules", target.name)
common.create_directory(modules_dir)

# Match wp in m_precision_select; --mixed changes storage precision only.
real_type = "real(sp)" if ARG("single") else "real(dp)"

if ARG("gpu") == gpuConfigOptions.MP.value:
directive_str = "mp"
elif ARG("gpu") == gpuConfigOptions.ACC.value:
directive_str = "acc"
else:
directive_str = None

# Write the generated Fortran code to the m_thermochem.f90 file with the chosen precision
sol = self.get_cantera_solution()

thermochem_code = generate_fortran(sol, scalar_type=real_type, offload=directive_str)

common.file_write(os.path.join(modules_dir, "m_thermochem.f90"), thermochem_code, True)
# Fypp source: MFC's build resolves wp and the offload directives. syscheck builds without
# MFC's common sources (m_precision_select, macros) and does not use the module.
if target.name != "syscheck":
thermochem_code = generate_fortran(self.get_cantera_solution())
common.file_write(os.path.join(modules_dir, "m_thermochem.fpp"), thermochem_code, True)

cons.unindent()

Expand Down
46 changes: 25 additions & 21 deletions toolchain/mfc/test_thermochem.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import shutil
import subprocess
import sys
from pathlib import Path
from types import SimpleNamespace

Expand Down Expand Up @@ -52,18 +53,30 @@
"""


def fypp(directory, name, source):
"""Preprocess generated Fypp source the way MFC's CMake build does."""
executable = shutil.which("fypp") or str(Path(sys.executable).with_name("fypp"))
fpp, f90 = directory / f"{name}.fpp", directory / f"{name}.f90"
fpp.write_text(source)
include = ["-I", str(ROOT / "src/common/include"), "-I", str(ROOT / "src/common")]
defines = ["-D", 'MFC_COMPILER="GNU"', "-D", "MFC_CASE_OPTIMIZATION=False", "-D", "chemistry=False"]
subprocess.run([executable, "-m", "re", *include, *defines, "--no-folding", "--line-length=999", str(fpp), str(f90)], check=True, capture_output=True, text=True)
return f90


def compile_kernel(directory, gas, precision="dp", offload=None, *, source=None, driver_source=DRIVER, extra_flags=(), extra_sources=()):
compiler = shutil.which("gfortran")
if compiler is None:
pytest.skip("gfortran is required to validate generated Fortran")
module = directory / "m_thermochem.f90"
module.write_text(source if source is not None else generate_fortran(gas, scalar_type=f"real({precision})", offload=offload))
module = fypp(directory, "m_thermochem", source if source is not None else generate_fortran(gas))
driver = directory / "driver.f90"
driver.write_text(driver_source.replace("KIND", precision))
driver.write_text(driver_source.replace("KIND", "wp").replace("use m_thermochem", "use m_precision_select, only: wp\n use m_thermochem", 1))
executable = directory / "reference"
flags = {None: [], "acc": ["-fopenacc"], "mp": ["-fopenmp"]}[offload]
flags = {None: [], "acc": ["-fopenacc", "-DMFC_OpenACC"], "mp": ["-fopenmp", "-DMFC_OpenMP"]}[offload]
flags += {"dp": [], "sp": ["-DMFC_SINGLE_PRECISION"]}[precision]
sources = [ROOT / "src/common/m_precision_select.f90", *extra_sources, module, driver]
subprocess.run(
[compiler, "-cpp", "-O0", "-Wconversion", "-Werror=conversion", *flags, *extra_flags, *map(str, extra_sources), str(module), str(driver), "-o", str(executable)],
[compiler, "-cpp", "-O0", "-Wconversion", "-Werror=conversion", *flags, *extra_flags, *map(str, sources), "-o", str(executable)],
cwd=directory,
check=True,
capture_output=True,
Expand Down Expand Up @@ -150,28 +163,19 @@ def test_rejects_custom_orders():


@pytest.mark.parametrize("mode", ["double", "single", "mixed"])
def test_solver_working_precision(tmp_path, monkeypatch, mode):
def test_solver_working_precision(tmp_path, mode):
"""The module the toolchain writes compiles and agrees with Cantera in every precision mode."""
from mfc.run import input as input_module

monkeypatch.setattr(input_module, "ARG", lambda name: {"single": mode == "single", "mixed": mode == "mixed", "gpu": None}[name])
case = input_module.MFCInputFile("case.py", str(tmp_path), {"chemistry": "T", "cantera_file": "h2o2.yaml"})
monkeypatch.setattr(case, "get_fpp", lambda target: "")
case.get_fpp = lambda target: ""
target = SimpleNamespace(name="simulation", isDependency=False, get_staging_dirpath=lambda case: str(tmp_path))
case.generate_fpp(target)
source = (tmp_path / "modules/simulation/m_thermochem.f90").read_text()
driver = DRIVER.replace("use m_thermochem", "use m_thermochem\n use m_precision_select, only: wp")
flags = [] if mode == "double" else [f"-DMFC_{mode.upper()}_PRECISION"]
source = (tmp_path / "modules/simulation/m_thermochem.fpp").read_text()
precision = "sp" if mode == "single" else "dp"
flags = ["-DMFC_MIXED_PRECISION"] if mode == "mixed" else []
gas = ct.Solution("h2o2.yaml")
executable = compile_kernel(
tmp_path,
gas,
"wp",
source=source,
driver_source=driver,
extra_flags=flags,
extra_sources=[ROOT / "src/common/m_precision_select.f90"],
)
compare_kernel(executable, gas, "sp" if mode == "single" else "dp")
compare_kernel(compile_kernel(tmp_path, gas, precision, source=source, extra_flags=flags), gas, precision)


def test_long_species_names(tmp_path):
Expand Down
4 changes: 2 additions & 2 deletions toolchain/mfc/thermochem/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
__all__ = ["generate_fortran"]


def generate_fortran(solution, module_name="m_thermochem", scalar_type="real(dp)", offload=None):
def generate_fortran(solution, module_name="m_thermochem"):
"""Load the expression machinery only when generating Fortran."""
from .fortran import generate_fortran as generate

return generate(solution, module_name, scalar_type, offload)
return generate(solution, module_name)
2 changes: 1 addition & 1 deletion toolchain/mfc/thermochem/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ def generator_fingerprint():
"""Keep builds from different generator revisions in separate staging trees."""
digest = hashlib.sha256()
root = Path(__file__).parent
for name in ("__init__.py", "fortran.py", "expressions.py", "module.f90.mako"):
for name in ("__init__.py", "fortran.py", "expressions.py", "module.fpp.mako"):
digest.update((root / name).read_bytes())
return digest.hexdigest()
29 changes: 14 additions & 15 deletions toolchain/mfc/thermochem/fortran.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ def wrap_code(s, indent=4):
lines = s.split("\n")
result_lines = []
for ln in lines:
# Fypp directives must stay on one line.
if ln.lstrip().startswith(("$:", "#:", "@:")):
result_lines.append(ln)
continue
nspaces = count_leading_spaces(ln)
level, remainder = divmod(nspaces, indent)

Expand Down Expand Up @@ -124,34 +128,29 @@ def validate_mechanism(sol):
raise ValueError(f"{label}: Arrhenius pre-exponential factors must be positive")


def generate_fortran(solution, module_name="m_thermochem", scalar_type="real(dp)", offload=None):
"""Emit MFC's thermodynamic, kinetics and transport interface from Cantera."""
def generate_fortran(solution, module_name="m_thermochem"):
"""Emit MFC's thermodynamic, kinetics and transport interface from Cantera as Fypp source.

Precision (wp) and offload directives ($:GPU_ROUTINE) are resolved by MFC's build, so one
source serves every configuration.
"""
import re

if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,62}", module_name):
raise ValueError(f"Invalid Fortran module name: {module_name!r}")
if scalar_type not in ("real(sp)", "real(dp)"):
raise ValueError(f"Unsupported scalar type: {scalar_type!r}")
directives = {None: "! name", "acc": "!$acc routine seq", "mp": "!$omp declare target"}
if offload not in directives:
raise ValueError(f"Unsupported offload mode: {offload!r}")
validate_mechanism(solution)
kind = "sp" if scalar_type == "real(sp)" else "dp"
falloff = [(i, r) for i, r in enumerate(solution.reactions()) if r.reaction_type.startswith("falloff")]
three_body = [(i, r) for i, r in enumerate(solution.reactions()) if r.reaction_type == "three-body-Arrhenius"]
template = Template(filename=str(Path(__file__).with_name("module.f90.mako")))
template = Template(filename=str(Path(__file__).with_name("module.fpp.mako")))
return wrap_code(
template.render(
ct=ct,
sol=solution,
str_np=partial(str_np, kind=kind),
cgm=FortranExpressionMapper(kind),
str_np=partial(str_np, kind="wp"),
cgm=FortranExpressionMapper("wp"),
Variable=p.Variable,
float_to_fortran=partial(float_to_fortran, kind=kind),
real_type=scalar_type,
kind=kind,
float_to_fortran=partial(float_to_fortran, kind="wp"),
species_name_length=max(map(len, solution.species_names)),
gpu_routine=f"#define GPU_ROUTINE(name) {directives[offload]}",
module_name=module_name,
ce=expressions,
falloff_reactions=falloff,
Expand Down
Loading
Loading