diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index bdfae9f8af..d705ac10a4 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -157,11 +157,11 @@ jobs:
run: |
sudo apt update
sudo apt install libboost-dev gfortran libomp-dev libomp5 \
- libopenblas-openmp-dev libhdf5-dev
+ libopenblas-openmp-dev libhdf5-dev doxygen graphviz
- name: Upgrade pip
run: python3 -m pip install -U pip setuptools wheel
- name: Install Python dependencies
- run: python3 -m pip install ruamel.yaml scons numpy cython pandas pytest
+ run: python3 -m pip install ruamel.yaml scons numpy cython pandas pytest Jinja2
pytest-xdist pytest-github-actions-annotate-failures pint graphviz
- name: Build Cantera
run: python3 `which scons` build env_vars=all
@@ -177,6 +177,13 @@ jobs:
run: |
LD_LIBRARY_PATH=build/lib
python3 -m pytest -raP -v -n auto --durations=50 test/python
+ - name: Generate clib_experimental
+ run: |
+ python3 `which scons` doxygen
+ python3 interfaces/sourcegen/run.py --api=clib --output=.
+ python3 `which scons` build clib_experimental=y
+ - name: Run googletests for clib_experimental
+ run: python3 `which scons` test-clib-experimental --debug=time
macos-multiple-pythons:
name: ${{ matrix.macos-version }} with Python ${{ matrix.python-version }}
@@ -893,9 +900,16 @@ jobs:
steps:
- uses: actions/checkout@v4
name: Checkout the repository
+ - name: Override default Python (Windows)
+ # Other operating systems use default system Python 3
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+ architecture: x64
+ if: matrix.os == 'windows-2022'
- name: Install Python dependencies
- # Install for the default system Python 3, which is used by 'dotnet build'
- run: python3 -m pip install ruamel.yaml Jinja2
+ # Install Python dependencies, which are used by 'dotnet build'
+ run: python3 -m pip install ruamel.yaml Jinja2 typing-extensions
- name: Install library dependencies with micromamba (Windows)
uses: mamba-org/setup-micromamba@v1
with:
diff --git a/SConstruct b/SConstruct
index 7d910dd100..0c72d54d5b 100644
--- a/SConstruct
+++ b/SConstruct
@@ -134,6 +134,10 @@ if "clean" in COMMAND_LINE_TARGETS:
remove_file(name)
for name in Path("site_scons").glob("**/*.pyc"):
remove_file(name)
+ for name in Path("include/cantera/clib_experimental").glob("*.h"):
+ remove_file(name)
+ for name in Path("src/clib_experimental").glob("*.cpp"):
+ remove_file(name)
logger.status("Done removing output files.", print_level=False)
@@ -364,6 +368,10 @@ config_options = [
"sphinx_docs",
"Build HTML documentation for Cantera using Sphinx.",
False),
+ BoolOption(
+ "clib_experimental",
+ "Build experimental CLib.",
+ False),
BoolOption(
"run_examples",
"""Run examples to generate plots and outputs for Sphinx Gallery. Disable to
diff --git a/doc/doxygen/Doxyfile b/doc/doxygen/Doxyfile
index e2f006a640..51034c8fde 100644
--- a/doc/doxygen/Doxyfile
+++ b/doc/doxygen/Doxyfile
@@ -1013,7 +1013,8 @@ RECURSIVE = YES
# Note that relative paths are relative to the directory from which doxygen is
# run.
-EXCLUDE = include/cantera/ext
+EXCLUDE = include/cantera/ext \
+ include/cantera/clib_experimental
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
# directories that are symbolic links (a Unix file system feature) are excluded
diff --git a/include/cantera/clib_experimental/.gitignore b/include/cantera/clib_experimental/.gitignore
new file mode 100644
index 0000000000..424c745c12
--- /dev/null
+++ b/include/cantera/clib_experimental/.gitignore
@@ -0,0 +1 @@
+*.h
diff --git a/include/cantera/clib_experimental/README.md b/include/cantera/clib_experimental/README.md
new file mode 100644
index 0000000000..17b4e3014b
--- /dev/null
+++ b/include/cantera/clib_experimental/README.md
@@ -0,0 +1,25 @@
+# Cantera – Experimental CLib Interface
+
+This directory and the associated `src/clib_experimental` folder contain an
+experimental re-implementation of Cantera's traditional CLib interface.
+
+## Code Generation
+
+Run the following command from the Cantera root folder:
+
+```
+scons doxygen
+python3 interfaces/sourcegen/run.py --api=clib --output=.
+scons build clib_experimental=y
+```
+
+A rudimentary test suite ensures that code performs as expected:
+
+```
+scons test-clib-experimental
+```
+
+## Status
+
+The experimental CLib Interface is in preview and still missing many features
+needed for parity with the traditional CLib interface.
diff --git a/interfaces/dotnet/Cantera/Cantera.csproj b/interfaces/dotnet/Cantera/Cantera.csproj
index 17761cbdc6..37e361451a 100644
--- a/interfaces/dotnet/Cantera/Cantera.csproj
+++ b/interfaces/dotnet/Cantera/Cantera.csproj
@@ -45,7 +45,7 @@
$([MSBuild]::NormalizePath($(IntermediateOutputPath)/sourcegen/))
-
diff --git a/interfaces/sourcegen/README.md b/interfaces/sourcegen/README.md
index 3f3561578e..466b7c79a0 100644
--- a/interfaces/sourcegen/README.md
+++ b/interfaces/sourcegen/README.md
@@ -2,12 +2,11 @@
The `sourcegen.generate_source` function crudely parses the CLib header files and generates intermediate objects which represent the functions:
* header file path
-* funcs: list of
- * return type (string)
- * name (string)
- * params: list of
- * return type
- * name
+* funcs: list of `Func` objects containing
+ * return type (`string`)
+ * name (`string`)
+ * params: list of function arguments (`ArgList`)
+ * optional annotations
`sourcegen.generate_source` then delegates the source generation to a language-specific sub-package. The sub-package creates the source files in a location appropriate to the build for that language’s build process.
@@ -34,4 +33,3 @@ Each sub-package can contain a yaml-based config file named `config.yaml`. The c
The config file may contain additional values for use by the language-specific sub-package.
## running from the command line
-
diff --git a/interfaces/sourcegen/run.py b/interfaces/sourcegen/run.py
index c207ec87f3..aac4b0a731 100644
--- a/interfaces/sourcegen/run.py
+++ b/interfaces/sourcegen/run.py
@@ -2,6 +2,48 @@
# at https://cantera.org/license.txt for license and copyright information.
import sys
+import argparse
+import textwrap
+
import sourcegen
-sourcegen.generate_source(*sys.argv[1:])
+def main(argv=None):
+ parser = create_argparser()
+ if argv is None and len(sys.argv) < 2:
+ parser.print_help(sys.stderr)
+ sys.exit(1)
+ args = parser.parse_args(argv)
+ lang = args.api
+ output = args.output
+ verbose = args.verbose
+ sourcegen.generate_source(lang, output, verbose=verbose)
+
+def create_argparser():
+ parser = argparse.ArgumentParser(
+ description=(
+ "Experimental source generator for creating Cantera interface code."),
+ epilog=textwrap.dedent(
+ """
+ The **sourcegen** utility is invoked as follows::
+
+ python path/to/sourcegen/run.py --api=csharp --output=.
+
+ where the relative path has to be provided as the utility is not installed.
+ Currently supported API options are: 'csharp', 'clib' and 'yaml'.
+ """),
+ formatter_class=argparse.RawDescriptionHelpFormatter
+ )
+ parser.add_argument(
+ "-v", "--verbose", action="store_true", default=False,
+ help="show additional logging output")
+ parser.add_argument(
+ "--api", default="",
+ help="language of generated Cantera API code")
+ parser.add_argument(
+ "--output", default="",
+ help="specifies the OUTPUT folder name")
+
+ return parser
+
+if __name__ == "__main__":
+ main()
diff --git a/interfaces/sourcegen/sourcegen/_HeaderFileParser.py b/interfaces/sourcegen/sourcegen/_HeaderFileParser.py
index 073a10f423..64658856d2 100644
--- a/interfaces/sourcegen/sourcegen/_HeaderFileParser.py
+++ b/interfaces/sourcegen/sourcegen/_HeaderFileParser.py
@@ -1,4 +1,4 @@
-"""Parser for existing CLib headers."""
+"""Parser for YAML header configurations or existing CLib headers."""
# This file is part of Cantera. See License.txt in the top-level directory or
# at https://cantera.org/license.txt for license and copyright information.
@@ -6,28 +6,97 @@
from pathlib import Path
import logging
import re
-from typing import List
+from typing import Iterable
+from typing_extensions import Self
-from ._dataclasses import HeaderFile, Func
+from ._dataclasses import HeaderFile, Func, Recipe
+from ._helpers import read_config
-_logger = logging.getLogger()
+_LOGGER = logging.getLogger()
-_clib_path = Path(__file__).parent.joinpath("../../../include/cantera/clib").resolve()
-_clib_ignore = ["clib_defs.h", "ctmatlab.h"]
+_CLIB_PATH = Path(__file__).parents[3] / "include" / "cantera" / "clib"
+_CLIB_IGNORE = ["clib_defs.h", "ctmatlab.h"]
+
+_DATA_PATH = Path(__file__).parent / "_data"
class HeaderFileParser:
+ """
+ Parser for header files or corresponding YAML specifications.
+
+ Provides for convenience methods to generate lists of `HeaderFile` objects, which
+ themselves are used for subsequent code scaffolding.
+ """
- def __init__(self, path: Path, ignore_funcs: List[str] = None):
+ def __init__(self, path: Path, ignore_funcs: Iterable[str] = None) -> None:
self._path = path
self._ignore_funcs = ignore_funcs
@classmethod
- def from_headers(cls, ignore_files, ignore_funcs) -> List[HeaderFile]:
+ def headers_from_yaml(
+ cls: Self, ignore_files: Iterable[str], ignore_funcs: Iterable[str]
+ ) -> list[HeaderFile]:
+ """Parse header file YAML configuration."""
+ files = sorted(
+ ff for ff in _DATA_PATH.glob("*.yaml") if ff.name not in ignore_files)
+ return [cls(ff, ignore_funcs.get(ff.name, []))._parse_yaml() for ff in files]
+
+ def _parse_yaml(self) -> HeaderFile:
+ def read_docstring():
+ doc = []
+ with self._path.open("r", encoding="utf-8") as fid:
+ while True:
+ line = fid.readline()
+ if line.startswith("#"):
+ doc.append(line.removeprefix("#").strip())
+ else:
+ break
+ if doc and doc[0].startswith("This file is part of "):
+ return []
+ return doc
+
+ msg = f" parsing {self._path.name!r}"
+ _LOGGER.info(msg)
+ config = read_config(self._path)
+ if self._ignore_funcs:
+ msg = f" ignoring {self._ignore_funcs!r}"
+ _LOGGER.info(msg)
+
+ recipes = []
+ prefix = config["prefix"]
+ base = config["base"]
+ parents = config.get("parents", [])
+ derived = config.get("derived", [])
+ for recipe in config["recipes"]:
+ if recipe["name"] in self._ignore_funcs:
+ continue
+ uses = recipe.get("uses", [])
+ if not isinstance(uses, list):
+ uses = [uses]
+ recipes.append(
+ Recipe(recipe["name"],
+ recipe.get("implements", ""),
+ uses,
+ recipe.get("what", ""),
+ recipe.get("brief", ""),
+ recipe.get("code", ""),
+ prefix,
+ base,
+ parents,
+ derived))
+
+ return HeaderFile(self._path, [], prefix, base, parents, derived, recipes,
+ read_docstring())
+
+ @classmethod
+ def headers_from_h(
+ cls: Self, ignore_files: Iterable[str], ignore_funcs: Iterable[str]
+ ) -> list[HeaderFile]:
"""Parse existing header file."""
- files = [_ for _ in _clib_path.glob("*.h")
- if _.name not in ignore_files + _clib_ignore]
- return [cls(_, ignore_funcs.get(_.name, []))._parse_h() for _ in files]
+ files = [ff for ff in _CLIB_PATH.glob("*.h")
+ if ff.name not in ignore_files + _CLIB_IGNORE]
+ files.sort()
+ return [cls(ff, ignore_funcs.get(ff.name, []))._parse_h() for ff in files]
def _parse_h(self) -> HeaderFile:
ct = self._path.read_text()
@@ -41,9 +110,11 @@ def _parse_h(self) -> HeaderFile:
parsed = map(Func.from_str, c_functions)
- _logger.info(f" parsing {self._path.name!r}")
+ msg = f" parsing {self._path.name!r}"
+ _LOGGER.info(msg)
if self._ignore_funcs:
- _logger.info(f" ignoring {self._ignore_funcs!r}")
+ msg = f" ignoring {self._ignore_funcs!r}"
+ _LOGGER.info(msg)
parsed = [f for f in parsed if f.name not in self._ignore_funcs]
diff --git a/interfaces/sourcegen/sourcegen/_SourceGenerator.py b/interfaces/sourcegen/sourcegen/_SourceGenerator.py
index 9849672448..cf79f12bcf 100644
--- a/interfaces/sourcegen/sourcegen/_SourceGenerator.py
+++ b/interfaces/sourcegen/sourcegen/_SourceGenerator.py
@@ -5,7 +5,6 @@
from abc import ABCMeta, abstractmethod
from pathlib import Path
-from typing import List
from ._dataclasses import HeaderFile
@@ -14,9 +13,9 @@ class SourceGenerator(metaclass=ABCMeta):
"""Specifies the interface of a language-specific SourceGenerator"""
@abstractmethod
- def __init__(self, out_dir: Path, config: dict):
+ def __init__(self, out_dir: Path, config: dict, templates: dict) -> None:
pass
@abstractmethod
- def generate_source(self, headers_files: List[HeaderFile]):
+ def generate_source(self, headers_files: list[HeaderFile]) -> None:
pass
diff --git a/interfaces/sourcegen/sourcegen/_TagFileParser.py b/interfaces/sourcegen/sourcegen/_TagFileParser.py
new file mode 100644
index 0000000000..1a0fb32c8a
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/_TagFileParser.py
@@ -0,0 +1,299 @@
+"""Parser for tag files and XML generated by doxygen."""
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+import sys
+from pathlib import Path
+import re
+from typing import Sequence, Iterable
+from typing_extensions import Self
+import logging
+from dataclasses import dataclass
+import xml.etree.ElementTree as ET
+
+from ._dataclasses import ArgList, Param, CFunc
+from ._helpers import with_unpack_iter
+
+
+_LOGGER = logging.getLogger(__name__)
+
+_TAG_PATH = Path(__file__).parents[3] / "build" / "doc"
+_XML_PATH = _TAG_PATH / "doxygen" / "xml"
+
+
+@dataclass(frozen=True)
+@with_unpack_iter
+class TagInfo:
+ """Represents information parsed from a doxygen tag file."""
+
+ base: str = "" #: Qualified scope (skipping Cantera namespace)
+ type: str = "" #: Return type
+ name: str = "" #: Function name
+ arglist: str = "" #: Function argument list (original XML string)
+ anchorfile: str = "" #: doxygen anchor file
+ anchor: str = "" #: doxygen anchor
+
+ @classmethod
+ def from_xml(cls: Self, qualified_name: str, xml: str) -> Self:
+ """Create tag information based on XML data."""
+ base = ""
+ if "::" in qualified_name:
+ base = qualified_name.split("::", 1)[0]
+
+ xml_tree = ET.fromstring(xml)
+ return cls(base,
+ xml_tree.find("type").text,
+ xml_tree.find("name").text,
+ xml_tree.find("arglist").text,
+ xml_tree.find("anchorfile").text.replace(".html", ".xml"),
+ xml_tree.find("anchor").text)
+
+ def __bool__(self) -> bool:
+ return all([self.type, self.name, self.arglist, self.anchorfile, self.anchor])
+
+ @property
+ def signature(self) -> str:
+ """Generate function signature based on tag information."""
+ return f"{self.type} {self.name}{self.arglist}"
+
+ @property
+ def id(self) -> str:
+ """Generate doxygen id."""
+ return f"{self.anchorfile.replace('.xml', '')}_1{self.anchor}"
+
+ @property
+ def qualified_name(self) -> str:
+ """Return qualified name."""
+ if self.base:
+ return f"{self.base}::{self.name}"
+ return self.name
+
+
+@dataclass(frozen=True)
+@with_unpack_iter
+class TagDetails(TagInfo):
+ """Create tag information based on XML data."""
+
+ location: str = "" #: File containing doxygen description
+ briefdescription: str = "" #: Brief doxygen description
+ parameterlist: list[Param] | None = None #: Annotated doxygen parameter list
+
+
+class TagFileParser:
+ """Class handling contents of doxygen tag file."""
+
+ _known: dict[str, str] #: Dictionary of known functions and corresponding XML tags
+
+ def __init__(self, bases: dict[str, str]) -> None:
+ tag_file = _TAG_PATH / "Cantera.tag"
+ if not tag_file.exists():
+ msg = (f"Tag file does not exist at expected location:\n {tag_file}\n"
+ "Run 'scons doxygen' to generate.")
+ _LOGGER.critical(msg)
+ sys.exit(1)
+
+ logging.info("Parsing doxygen tags...")
+ doxygen_tags = tag_file.read_text()
+ self._parse_doxyfile(doxygen_tags, bases)
+
+ def _parse_doxyfile(self, doxygen_tags: str, bases: Sequence[str]) -> None:
+ """Retrieve class and function information from Cantera namespace."""
+
+ def xml_compounds(kind: str, names: Sequence[str]) -> dict[str, str]:
+ regex = re.compile(rf'')
+ found = []
+ compounds = {}
+ for compound in re.findall(regex, doxygen_tags):
+ qualified_name = ET.fromstring(compound).find("name").text
+ compound_name = qualified_name.split(":")[-1]
+ if compound_name in names:
+ found.append(compound_name)
+ compounds[compound_name] = compound
+ if not (set(names) - set(found)):
+ return compounds
+ missing = '", "'.join(set(names) - set(found))
+ msg = f"Missing {kind!r} compound(s):\n {missing!r}\nusing regex "
+ msg += f"{regex}. Continuing with remaining compounds: \n {found!r}"
+ _LOGGER.error(msg)
+
+ # Parse content of namespace Cantera
+ namespace = xml_compounds("namespace", ["Cantera"])["Cantera"]
+ qualified_names = []
+ xml_tree = ET.fromstring(namespace).findall("class")
+ for element in xml_tree:
+ if element.attrib.get("kind", "") == "class":
+ qualified_names.append(element.text)
+ class_names = [_.split(":")[-1] for _ in qualified_names]
+
+ # Handle exceptions for unknown/undocumented classes
+ unknown = set(bases) - set(class_names)
+ if "', '".join(unknown):
+ unknown = "', '".join(unknown)
+ msg = ("Class(es) in configuration file are missing "
+ f"from tag file: {unknown!r}")
+ _LOGGER.critical(msg)
+ exit(1)
+
+ # Parse content of classes that are specified by the configuration file
+ class_names = set(bases) & set(class_names)
+ classes = xml_compounds("class", class_names)
+
+ def xml_members(kind: str, text: str, prefix: str = "") -> dict[str, str]:
+ regex = re.compile(rf'')
+ functions = {}
+ for func in re.findall(regex, text):
+ func_name = f'{prefix}{ET.fromstring(func).find("name").text}'
+ if func_name in functions:
+ # tag file may contain duplicates
+ if func not in functions[func_name]:
+ functions[func_name].append(func)
+ else:
+ functions[func_name] = [func]
+ return functions
+
+ # Get known functions from namespace and methods from classes
+ self._known = xml_members("function", namespace)
+ for name, cls in classes.items():
+ prefix = f"{name}::"
+ self._known.update(xml_members("function", cls, prefix))
+
+ def exists(self, cxx_func: str) -> bool:
+ """Check whether doxygen tag exists."""
+ return cxx_func in self._known
+
+ def detect(self, name: str, bases: Iterable[str], permissive: bool = True) -> str:
+ """Detect qualified method name."""
+ for base in bases:
+ name_ = f"{base}::{name}"
+ if self.exists(name_):
+ return name_
+ if self.exists(name):
+ return name
+ if permissive:
+ return ""
+ msg = f"Unable to detect {name!r} in doxygen tags."
+ _LOGGER.critical(msg)
+ exit(1)
+
+ def tag_info(self, func_string: str) -> TagInfo:
+ """Look up tag information based on (partial) function signature."""
+ cxx_func = func_string.split("(")[0].split(" ")[-1]
+ if cxx_func not in self._known:
+ msg = f"Could not find {cxx_func!r} in doxygen tag file."
+ _LOGGER.critical(msg)
+ sys.exit(1)
+ ix = 0
+ if len(self._known[cxx_func]) > 1:
+ # Disambiguate functions with same name
+ # TODO: current approach does not use information on default arguments
+ known_args = [ET.fromstring(xml).find("arglist").text
+ for xml in self._known[cxx_func]]
+ known_args = [ArgList.from_xml(al).short_str() for al in known_args]
+ args = re.findall(re.compile(r"(?<=\().*(?=\))"), func_string)
+ if not args and "()" in known_args:
+ # Candidate function without arguments exists
+ ix = known_args.index("()")
+ elif not args:
+ # Function does not use arguments
+ known = "\n - ".join([""] + known_args)
+ msg = (f"Need argument list to disambiguate {func_string!r}. "
+ f"possible matches are:{known}")
+ _LOGGER.critical(msg)
+ sys.exit(1)
+ else:
+ args = f"({args[0]}"
+ ix = -1
+ for i, known in enumerate(known_args):
+ if known.startswith(args):
+ # Detected argument list that uses default arguments
+ ix = i
+ break
+ if ix < 0:
+ msg = f"Unable to match {func_string!r} to known functions."
+ _LOGGER.critical(msg)
+ sys.exit(1)
+
+ return TagInfo.from_xml(cxx_func, self._known[cxx_func][ix])
+
+ def cxx_func(self, func_string: str) -> CFunc:
+ """Generate annotated C++ function specification."""
+ details = tag_lookup(self.tag_info(func_string))
+ ret_param = Param.from_xml(details.type)
+
+ # Merge attributes from doxygen signature and doxygen annotations
+ args = ArgList.from_xml(details.arglist).params # from signature
+ args_annotated = details.parameterlist # from documentation
+ args_merged = []
+ for arg in args:
+ for desc in args_annotated:
+ if arg.name == desc.name:
+ args_merged.append(
+ Param(arg.p_type, arg.name,
+ desc.description, desc.direction, arg.default))
+ break
+ else:
+ args_merged.append(Param(arg.p_type, arg.name, "Undocumented."))
+
+ return CFunc(ret_param.p_type, details.name, ArgList(args_merged),
+ details.briefdescription, None, ret_param.description,
+ details.base, [])
+
+
+def tag_lookup(tag_info: TagInfo) -> TagDetails:
+ """Retrieve tag details from doxygen tree."""
+ xml_file = _XML_PATH / tag_info.anchorfile
+ if not xml_file.exists():
+ msg = f"Tag file does not exist at expected location:\n {xml_file}"
+ _LOGGER.error(msg)
+ return TagDetails()
+
+ xml_details = xml_file.read_text()
+ id_ = tag_info.id
+ regex = re.compile(rf'')
+ matches = re.findall(regex, xml_details)
+
+ if not matches:
+ msg = f"No XML matches found for {tag_info.qualified_name!r}"
+ _LOGGER.error(msg)
+ return TagDetails()
+ if len(matches) != 1:
+ msg = f"Inconclusive XML matches found for {tag_info.qualified_name!r}"
+ _LOGGER.warning(msg)
+ matches = matches[:1]
+
+ def no_refs(entry: str) -> str:
+ # Remove stray XML markup that causes problems with xml.etree
+ if "[")
+ for ref in re.findall(regex, entry):
+ entry = entry.replace(ref, "][")
+ entry = entry.replace("][", "").replace("]", "")
+ return entry
+
+ def xml_parameterlist(xml_tree: ET) -> list[Param]:
+ # Resolve/flatten parameter list
+ names = []
+ directions = []
+ for element in xml_tree.find("parameternamelist"):
+ names.append(element.text)
+ directions.append(element.attrib.get("direction", ""))
+ description = xml_tree.find("parameterdescription").find("para").text.strip()
+ return [Param("", n, description, d) for n, d in zip(names, directions)]
+
+ xml = matches[0]
+ xml_tree = ET.fromstring(no_refs(xml))
+
+ par_list = []
+ xml_details = xml_tree.find("detaileddescription")
+ if xml_details:
+ # TODO: confirm that this is always the last "para" entry
+ xml_list = xml_details.findall("para")[-1].find("parameterlist")
+ if xml_list:
+ for item in xml_list.findall("parameteritem"):
+ par_list.extend(xml_parameterlist(item))
+
+ return TagDetails(*tag_info,
+ xml_tree.find("location").attrib["file"],
+ xml_tree.find("briefdescription").find("para").text.strip(),
+ par_list)
diff --git a/interfaces/sourcegen/sourcegen/_data/README.md b/interfaces/sourcegen/sourcegen/_data/README.md
new file mode 100644
index 0000000000..895709da13
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/_data/README.md
@@ -0,0 +1,58 @@
+# Automatic Code Generation (Experimental)
+
+This folder holds YAML configuration data for code generation. Code generation uses
+output of the *doxygen* utility for information on function declarations as well as
+documentation. The collected information represents annotated CLib functions that form
+the basis for specific APIs generated by *sourcegen*.
+
+## YAML Specification Files
+
+Each file contains information for CLib headers that implement functions and methods
+built around a specific Cantera class. The following fields are supported:
+
+- `prefix`: Prefix used for CLib functions.
+- `base`: Base class of the CLib library; examples: `Solution`, `ThermoPhase`.
+ Stand-alone functions defined in the `Cantera` namespace use `""` as a base.
+- `parents`: List of class parents; used to locate C++ methods (default: `[]`).
+- `derived`: List of derived classes; used to locate C++ methods (default: `[]`).
+- `recipes`: List of CLib *recipes* (see below).
+
+In addition, the description of the YAML file (commented block at top of file) may be
+used as part of the docstring of auto-generated API files.
+
+The *sourcegen* utility implements logic to automatically detect CLib functions types
+based on a recipe, which is subsequently used to scaffold API functions using dedicated
+*Jinja* templates. The following recipe/CLib function types are differentiated:
+
+- `function`: Regular function defined in the `Cantera` namespace.
+- `constructor`: A CLib constructor adds new (or existing) C++ objects to CLib storage.
+ As all objects are handled via smart `shared_ptr<>`, a CLib constructor requires a
+ C++ utility functions that returning a pointer to a new object or an object that is
+ not yet added to CLib storage. Constructor names should start with `new`.
+- `destructor`: A CLib destructor removes a C++ object from CLib. Destructor names
+ should start with `del`.
+- `getter`: Implements a getter method of a C++ class.
+- `setter`: Implements a setter method of a C++ class.
+- `method`: Generic method of a C++ class.
+- `noop`: No operation.
+- `reserved`: Reserved (hard-coded) CLib functions which include service functions for
+ CLib storage (examples: `cabinetSize`, `parentHandle`) or functions that do not have
+ a C++ equivalent (example: `getCanteraError`).
+
+## YAML Recipes
+
+Recipes include all information required for the auto-generation of a corresponding
+CLib function. Each recipe uses the following fields:
+
+- `name`: Name of the CLib function to be generated (without prefix).
+- `implements`: Optional name or signature of the implemented C++ function/method. If
+ left empty, *sourcegen* searches for doxygen tags matching the `name` field.
+ A qualified name is sufficient if C++ functions/methods are unique, for example
+ `Func1::type`. A full signature is required whenever shortened signatures with
+ default arguments are used and/or multiple C++ function/method variants exist, for
+ example `Phase::moleFraction(size_t)`.
+- `uses`: Optional list of auxiliary C++ class methods used by the CLib function. The
+ exact usage depends on the type of the implemented CLib function.
+- `what`: Optional override for auto-detected recipe/CLib function type.
+- `brief`: Optional override for brief description from doxygen documentation.
+- `code`: Optional custom code to override auto-generated code.
diff --git a/interfaces/sourcegen/sourcegen/_data/ct_auto.yaml b/interfaces/sourcegen/sourcegen/_data/ct_auto.yaml
new file mode 100644
index 0000000000..b9937bc8c3
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/_data/ct_auto.yaml
@@ -0,0 +1,28 @@
+# The main library of the auto-generated CLib API contains %Cantera service functions.
+# Partially implements a replacement for CLib's traditional @c ct library.
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+prefix: ct3
+base: ""
+recipes:
+- name: getCanteraVersion
+ implements: version
+- name: getGitCommit
+ implements: gitCommit
+- name: getCanteraError
+- name: addCanteraDirectory
+ implements: addDirectory
+- name: getDataDirectories
+- name: findInputFile
+- name: suppress_deprecation_warnings
+- name: make_deprecation_warnings_fatal
+- name: suppress_warnings
+- name: warnings_suppressed
+- name: make_warnings_fatal
+- name: suppress_thermo_warnings
+- name: clearStorage
+- name: resetStorage
+# - name: setLogWriter # only used by .NET API
+# - name: setLogCallback # only used by .NET API
diff --git a/interfaces/sourcegen/sourcegen/_data/ctfunc_auto.yaml b/interfaces/sourcegen/sourcegen/_data/ctfunc_auto.yaml
new file mode 100644
index 0000000000..ad82a38524
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/_data/ctfunc_auto.yaml
@@ -0,0 +1,36 @@
+# Auto-generated CLib API for %Cantera's Func1 class.
+# Implements a replacement for CLib's traditional @c ctfunc library.
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+prefix: func13
+base: Func1
+recipes:
+- name: check
+ implements: checkFunc1
+- name: newBasic
+ implements: newFunc1(const string&, double)
+- name: newAdvanced
+ implements: newFunc1(const string&, const vector&)
+- name: newCompound
+ implements: newFunc1(const string&, const shared_ptr, const shared_ptr)
+- name: newModified
+ implements: newFunc1(const string&, const shared_ptr, double)
+- name: newSum
+ implements: newSumFunction
+- name: newDiff
+ implements: newDiffFunction
+- name: newProd
+ implements: newProdFunction
+- name: newRatio
+ implements: newRatioFunction
+- name: type
+- name: eval
+- name: newDerivative
+ what: constructor
+ implements: Func1::derivative
+- name: write
+- name: del
+ what: destructor
+- name: cabinetSize
diff --git a/interfaces/sourcegen/sourcegen/_data/ctkin_auto.yaml b/interfaces/sourcegen/sourcegen/_data/ctkin_auto.yaml
new file mode 100644
index 0000000000..2a1c6e2ad9
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/_data/ctkin_auto.yaml
@@ -0,0 +1,19 @@
+# Auto-generated CLib API for %Cantera's Kinetics class.
+# Partially implements a replacement for CLib's traditional @c ct library.
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+prefix: kin3
+base: Kinetics
+parents: [] # List of parent classes
+derived: [] # List of specializations
+recipes:
+- name: nReactions
+- name: kineticsType
+- name: getFwdRatesOfProgress
+- name: del
+ what: noop
+ brief: Destructor; required by some APIs although object is managed by Solution.
+- name: cabinetSize
+- name: parentHandle
diff --git a/interfaces/sourcegen/sourcegen/_data/ctsol_auto.yaml b/interfaces/sourcegen/sourcegen/_data/ctsol_auto.yaml
new file mode 100644
index 0000000000..4f0b8b6b54
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/_data/ctsol_auto.yaml
@@ -0,0 +1,43 @@
+# Auto-generated CLib API for %Cantera's Solution class.
+# Partially implements a replacement for CLib's traditional @c ct library.
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+prefix: sol3
+base: Solution
+parents: [] # List of parent classes
+derived: [Interface] # List of specializations
+recipes:
+- name: newSolution
+ implements: newSolution(const string&, const string&, const string&)
+ uses: [thermo, kinetics, transport]
+- name: newInterface
+ implements:
+ newInterface(const string&, const string&, const vector>&)
+ uses: [thermo, kinetics]
+- name: del
+ uses: [thermo, kinetics, transport]
+- name: name
+- name: setName
+- name: thermo
+- name: kinetics
+- name: transport
+- name: setTransportModel
+ code: |-
+ try {
+ auto obj = SolutionCabinet::at(handle);
+ TransportCabinet::del(
+ TransportCabinet::index(*(obj->transport()), handle));
+ obj->setTransportModel(model);
+ return TransportCabinet::add(obj->transport(), handle);
+ } catch (...) {
+ return handleAllExceptions(-2, ERR);
+ }
+- name: nAdjacent
+- name: adjacent
+ implements: Solution::adjacent(size_t)
+ uses: [thermo, kinetics, transport]
+ what: constructor # registers object in CLib storage
+# - name: adjacentName
+- name: cabinetSize
diff --git a/interfaces/sourcegen/sourcegen/_data/ctthermo_auto.yaml b/interfaces/sourcegen/sourcegen/_data/ctthermo_auto.yaml
new file mode 100644
index 0000000000..a5cb8fafd9
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/_data/ctthermo_auto.yaml
@@ -0,0 +1,59 @@
+# Auto-generated CLib API for %Cantera's ThermoPhase class.
+# Partially implements a replacement for CLib's traditional @c ct library.
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+prefix: thermo3
+base: ThermoPhase
+parents: [Phase] # List of parent classes
+derived: [] # List of specializations
+recipes:
+- name: report
+- name: nElements
+- name: nSpecies
+- name: temperature
+- name: setTemperature
+- name: pressure
+- name: setPressure
+- name: density
+- name: setDensity
+- name: molarDensity
+- name: meanMolecularWeight
+- name: moleFraction
+ implements: Phase::moleFraction(size_t)
+- name: massFraction
+ implements: Phase::massFraction(size_t)
+- name: getMoleFractions
+ uses: nSpecies
+- name: getMassFractions
+ uses: nSpecies
+- name: setMoleFractions
+ uses: nSpecies
+- name: setMassFractions
+ uses: nSpecies
+- name: setMoleFractionsByName
+ implements: Phase::setMoleFractionsByName(const string&)
+- name: setMassFractionsByName
+ implements: Phase::setMassFractionsByName(const string&)
+- name: enthalpy_mole
+- name: enthalpy_mass
+- name: entropy_mole
+- name: entropy_mass
+- name: intEnergy_mole
+- name: intEnergy_mass
+- name: cp_mole
+- name: cp_mass
+- name: getPartialMolarEnthalpies
+- name: getPartialMolarEntropies
+- name: getPartialMolarIntEnergies
+- name: getPartialMolarCp
+- name: getPartialMolarVolumes
+- name: equilibrate
+ implements:
+ ThermoPhase::equilibrate(const string&, const string&, double, int, int, int)
+- name: del
+ what: noop
+ brief: Destructor; required by some APIs although object is managed by Solution.
+- name: cabinetSize
+- name: parentHandle
diff --git a/interfaces/sourcegen/sourcegen/_data/cttrans_auto.yaml b/interfaces/sourcegen/sourcegen/_data/cttrans_auto.yaml
new file mode 100644
index 0000000000..671e68ca12
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/_data/cttrans_auto.yaml
@@ -0,0 +1,20 @@
+# Auto-generated CLib API for %Cantera's Transport class.
+# Partially implements a replacement for CLib's traditional @c ct library.
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+prefix: trans3
+base: Transport
+parents: [] # List of parent classes
+derived: [] # List of specializations
+recipes:
+- name: transportModel
+- name: viscosity
+- name: thermalConductivity
+- name: getMixDiffCoeffs
+- name: del
+ what: noop
+ brief: Destructor; required by some APIs although object is managed by Solution.
+- name: cabinetSize
+- name: parentHandle
diff --git a/interfaces/sourcegen/sourcegen/_dataclasses.py b/interfaces/sourcegen/sourcegen/_dataclasses.py
index f8f45d644e..7d02d9ab8b 100644
--- a/interfaces/sourcegen/sourcegen/_dataclasses.py
+++ b/interfaces/sourcegen/sourcegen/_dataclasses.py
@@ -1,10 +1,13 @@
+"""Data classes common to sourcegen scaffolders."""
+
# This file is part of Cantera. See License.txt in the top-level directory or
# at https://cantera.org/license.txt for license and copyright information.
from dataclasses import dataclass
import re
from pathlib import Path
-from typing import List, Any, Tuple, Iterator
+from typing import Any, Iterator
+from typing_extensions import Self
from ._helpers import with_unpack_iter
@@ -12,19 +15,49 @@
@dataclass(frozen=True)
@with_unpack_iter
class Param:
- """Represents a function parameter"""
+ """Class representing a function parameter."""
+
+ p_type: str #: Parameter type
+ name: str = "" #: Parameter name; may be empty if used for return argument
- p_type: str
- name: str = ""
+ description: str = "" #: Parameter description (optional annotation)
+ direction: str = "" #: Direction of parameter (optional annotation)
+ default: Any = None #: Default value (optional)
@classmethod
- def from_str(cls, param: str) -> 'Param':
- """Generate Param from string parameter"""
+ def from_str(cls: Self, param: str, doc: str = "") -> Self:
+ """Generate Param from parameter string."""
+ param = param.strip()
+ default = None
+ if "=" in param:
+ param, _, default = param.partition("=")
parts = param.strip().rsplit(" ", 1)
if len(parts) == 2 and parts[0] not in ["const", "virtual", "static"]:
- return cls(*parts)
+ if "@param" not in doc:
+ return cls(*parts, "", "", default)
+ items = doc.split()
+ if items[1] != parts[1]:
+ msg = f"Documented variable {items[1]!r} does not match {parts[1]!r}"
+ raise ValueError(msg)
+ direction = items[0].split("[")[1].split("]")[0] if "[" in items[0] else ""
+ return cls(*parts, " ".join(items[2:]), direction, default)
return cls(param)
+ @classmethod
+ def from_xml(cls: Self, param: str) -> Self:
+ """
+ Generate Param from XML string.
+
+ Note: Converts from doxygen style to simplified C++ whitespace notation.
+ """
+ for rep in [(" &", "& "), ("< ", "<"), (" >", ">"), (" *", "* ")]:
+ param = param.replace(*rep)
+ return cls.from_str(param.strip())
+
+ def short_str(self) -> str:
+ """String representation of the parameter without parameter name."""
+ return self.p_type
+
def long_str(self) -> str:
"""String representation of the parameter with parameter name."""
if not self.name:
@@ -34,58 +67,74 @@ def long_str(self) -> str:
@dataclass(frozen=True)
class ArgList:
- """Represents a function argument list"""
+ """Represents a function argument list."""
- params: List[Param]
- spec: str = "" #: trailing Specification (example: `const`)
+ params: list[Param] #: List of function parameters
+ spec: str = "" #: Trailing specification (example: `const`)
@staticmethod
- def _split_arglist(arglist: str) -> Tuple[str, str]:
+ def _split_arglist(arglist: str) -> tuple[str, str]:
"""Split string into text within parentheses and trailing specification."""
arglist = arglist.strip()
if not arglist:
return "", ""
spec = arglist[arglist.rfind(")") + 1:]
# match text within parentheses
- regex = re.compile(r'(?<=\().*(?=\))', flags=re.DOTALL)
+ regex = re.compile(r"(?<=\().*(?=\))", flags=re.DOTALL)
arglist = re.findall(regex, arglist)[0]
return arglist, spec
@classmethod
- def from_str(cls, arglist: str) -> 'ArgList':
+ def from_str(cls: Self, arglist: str) -> Self:
"""Generate ArgList from string argument list."""
arglist, spec = cls._split_arglist(arglist)
if not arglist:
return cls([], spec)
return cls([Param.from_str(arg) for arg in arglist.split(",")], spec)
- def __getitem__(self, k):
+ @classmethod
+ def from_xml(cls: Self, arglist: str) -> Self:
+ """Generate ArgList from XML string argument list."""
+ arglist, spec = cls._split_arglist(arglist)
+ if not arglist:
+ return cls([], spec)
+ return cls([Param.from_xml(arg) for arg in arglist.split(",")], spec)
+
+ def __len__(self) -> int:
+ return len(self.params)
+
+ def __getitem__(self, k: int) -> Param:
return self.params[k]
- def __iter__(self) -> "Iterator[Param]":
+ def __iter__(self) -> Iterator[Param]:
return iter(self.params)
+ def short_str(self) -> str:
+ """String representation of the argument list without parameter names."""
+ args = ", ".join(par.short_str() for par in self.params)
+ return f"({args}) {self.spec}".strip()
+
def long_str(self) -> str:
"""String representation of the argument list with parameter names."""
- args = ', '.join([par.long_str() for par in self.params])
+ args = ", ".join([par.long_str() for par in self.params])
return f"({args}) {self.spec}".strip()
@dataclass(frozen=True)
@with_unpack_iter
class Func:
- """Represents a function parsed from a C header file."""
+ """Represents a function declaration in a C/C++ header file."""
- ret_type: str # may include leading specifier
- name: str
- arglist: ArgList
+ ret_type: str #: Return type; may include leading specifier
+ name: str #: Function name
+ arglist: ArgList #: Argument list
@classmethod
- def from_str(cls, func: str) -> 'Func':
+ def from_str(cls: Self, func: str) -> Self:
"""Generate Func from declaration string of a function."""
- func = func.strip()
- # match all characters before an opening parenthesis '(' or end of line
- name = re.findall(r'.*?(?=\(|$)', func)[0]
+ func = func.rstrip(";").strip()
+ # match all characters before an opening parenthesis "(" or end of line
+ name = re.findall(r".*?(?=\(|$)", func)[0]
arglist = ArgList.from_str(func.replace(name, "").strip())
r_type = ""
if " " in name:
@@ -99,8 +148,87 @@ def declaration(self) -> str:
@dataclass(frozen=True)
@with_unpack_iter
-class HeaderFile:
- """Represents information about a parsed C header file"""
+class CFunc(Func):
+ """Represents an annotated function declaration in a C/C++ header file."""
+
+ brief: str = "" #: Brief description (optional)
+ implements: Self = None #: Implemented C++ function/method (optional)
+ returns: str = "" #: Description of returned value (optional)
+ base: str = "" #: Qualified scope of function/method (optional)
+ uses: list[Self] | None = None #: List of auxiliary C++ methods (optional)
+
+ @classmethod
+ def from_str(cls: Self, func: str, brief: str = "") -> Self:
+ """Generate annotated CFunc from header block of a function."""
+ lines = func.split("\n")
+ func = Func.from_str(lines[-1])
+ if len(lines) == 1:
+ return cls(*func, brief, None, "", "", [])
+ returns = ""
+ args = []
+ for ix, line in enumerate(lines[:-1]):
+ line = line.strip().lstrip("*").strip()
+ if ix == 1 and not brief:
+ brief = line
+ elif line.startswith("@param"):
+ # assume that variables are documented in order
+ arg = func.arglist[len(args)].long_str()
+ args.append(Param.from_str(arg, line))
+ elif line.startswith("@returns"):
+ returns = line.lstrip("@returns").strip()
+ args = ArgList(args)
+ return cls(func.ret_type, func.name, args, brief, None, returns, "", [])
+
+ def short_declaration(self) -> str:
+ """Return a short string representation."""
+ ret = (f"{self.name}{self.arglist.short_str()}").strip()
+ if self.base:
+ return f"{self.ret_type} {self.base}::{ret}"
+ return f"{self.ret_type} {ret}"
+
+ @property
+ def ret_param(self) -> Param:
+ """Assemble return parameter."""
+ return Param(self.ret_type, "", self.returns)
+
+
+@dataclass
+@with_unpack_iter
+class Recipe:
+ """
+ Represents a recipe for a CLib method.
+
+ Class holds contents of YAML header configuration.
+ """
+
+ name: str #: name of method (without prefix)
+ implements: str #: signature of implemented C++ function/method
+ uses: str | list[str] #: auxiliary C++ methods used by recipe
+ what: str #: override auto-detection of recipe type
+ brief: str #: override brief description from doxygen documentation
+ code: str #: custom code to override autogenerated code (stub: to be implemented)
- path: Path
- funcs: List[Func]
+ prefix: str #: prefix used for CLib access function
+ base: str #: C++ class implementing method (if applicable)
+ parents: list[str] #: list of C++ parent classes (if applicable)
+ derived: list[str] #: list of C++ specializations (if applicable)
+
+
+@dataclass
+class HeaderFile:
+ """Represents information about a parsed C header file."""
+
+ path: Path #: output folder
+ funcs: list[Func] #: list of functions to be scaffolded
+
+ prefix: str = "" #: prefix used for CLib function names
+ base: str = "" #: base class of C++ methods (if applicable)
+ parents: list[str] | None = None #: list of C++ parent class(es)
+ derived: list[str] | None = None #: list of C++ specialization(s)
+ recipes: list[Recipe] | None = None #: list of header recipes read from YAML
+ docstring: list[str] | None = None #: lines representing docstring of YAML file
+
+ def output_name(self, auto: str = "3", suffix: str = "") -> Path:
+ """Return updated path."""
+ ret = self.path.parent / self.path.name.replace("_auto", auto)
+ return ret.with_suffix(suffix)
diff --git a/interfaces/sourcegen/sourcegen/_helpers.py b/interfaces/sourcegen/sourcegen/_helpers.py
index 901dc1cf10..7049eba8ad 100644
--- a/interfaces/sourcegen/sourcegen/_helpers.py
+++ b/interfaces/sourcegen/sourcegen/_helpers.py
@@ -5,6 +5,7 @@
from pathlib import Path
from ruamel import yaml
+
def read_config(config_file: Path) -> dict:
"""Read YAML configuration file."""
if config_file.is_file():
diff --git a/interfaces/sourcegen/sourcegen/_orchestrate.py b/interfaces/sourcegen/sourcegen/_orchestrate.py
index dc0831ab89..4919eab037 100644
--- a/interfaces/sourcegen/sourcegen/_orchestrate.py
+++ b/interfaces/sourcegen/sourcegen/_orchestrate.py
@@ -6,14 +6,14 @@
from pathlib import Path
import logging
import sys
-from typing import List, Dict
from ._HeaderFileParser import HeaderFileParser
from ._SourceGenerator import SourceGenerator
+from .clib import CLibSourceGenerator
from ._helpers import read_config
-_logger = logging.getLogger()
+_LOGGER = logging.getLogger()
class CustomFormatter(logging.Formatter):
"""Minimalistic logging output"""
@@ -23,28 +23,51 @@ def format(self, record):
return formatter.format(record)
-def generate_source(lang: str, out_dir: str=""):
+def generate_source(lang: str, out_dir: str = "", verbose = False) -> None:
"""Main entry point of sourcegen."""
loghandler = logging.StreamHandler(sys.stdout)
loghandler.setFormatter(CustomFormatter())
- _logger.handlers.clear()
- _logger.addHandler(loghandler)
- _logger.setLevel(logging.DEBUG)
- _logger.info(f"Generating {lang!r} source files...")
+ _LOGGER.handlers.clear()
+ _LOGGER.addHandler(loghandler)
+ _LOGGER.setLevel(logging.DEBUG if verbose else logging.INFO)
+
+ if not out_dir:
+ _LOGGER.critical("Aborting: sourcegen requires output folder information.")
+ exit(1)
module = importlib.import_module(__package__ + "." + lang)
root = Path(module.__file__).parent
config = read_config(root / "config.yaml")
templates = read_config(root / "templates.yaml")
- ignore_files: List[str] = config.pop("ignore_files", [])
- ignore_funcs: Dict[str, List[str]] = config.pop("ignore_funcs", {})
+ ignore_files: list[str] = config.pop("ignore_files", [])
+ ignore_funcs: dict[str, list[str]] = config.pop("ignore_funcs", {})
+
+ msg = f"Starting sourcegen for {lang!r} API"
+ _LOGGER.info(msg)
- files = HeaderFileParser.from_headers(ignore_files, ignore_funcs)
+ if lang == "clib":
+ # prepare for generation of CLib headers in main processing step
+ files = HeaderFileParser.headers_from_yaml(ignore_files, ignore_funcs)
+ elif lang == "csharp":
+ # csharp parses existing (traditional) CLib header files
+ files = HeaderFileParser.headers_from_h(ignore_files, ignore_funcs)
+ else:
+ # generate CLib headers from YAML specifications as a preprocessing step
+ files = HeaderFileParser.headers_from_yaml(ignore_files, ignore_funcs)
+ clib_root = Path(__file__).parent / "clib"
+ clib_config = read_config(clib_root / "config.yaml")
+ clib_templates = read_config(clib_root / "templates.yaml")
+ for key in ["ignore_files", "ignore_funcs"]:
+ clib_config.pop(key)
+ clib_scaffolder = CLibSourceGenerator(None, clib_config, clib_templates)
+ clib_scaffolder.resolve_tags(files)
# find and instantiate the language-specific SourceGenerator
+ msg = f"Generating {lang!r} source files..."
+ _LOGGER.info(msg)
_, scaffolder_type = inspect.getmembers(module,
lambda m: inspect.isclass(m) and issubclass(m, SourceGenerator))[0]
scaffolder: SourceGenerator = scaffolder_type(out_dir, config, templates)
scaffolder.generate_source(files)
- _logger.info("Done.")
+ _LOGGER.info("Done.")
diff --git a/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py b/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py
new file mode 100644
index 0000000000..6990319682
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/clib/_CLibSourceGenerator.py
@@ -0,0 +1,563 @@
+"""Generator for CLib source files."""
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+import sys
+from pathlib import Path
+import logging
+
+from jinja2 import Environment, BaseLoader
+
+from ._Config import Config
+
+from .._dataclasses import HeaderFile, Param, ArgList, CFunc, Recipe
+from .._SourceGenerator import SourceGenerator
+from .._TagFileParser import TagFileParser
+
+
+_LOGGER = logging.getLogger()
+
+class CLibSourceGenerator(SourceGenerator):
+ """The SourceGenerator for generating CLib."""
+
+ _clib_bases: list[str] = None #: list of bases provided via YAML configurations
+
+ def __init__(self, out_dir: str, config: dict, templates: dict) -> None:
+ self._out_dir = out_dir or None
+ if self._out_dir is not None:
+ self._out_dir = Path(out_dir)
+ self._out_dir.mkdir(parents=True, exist_ok=True)
+ self._config = Config.from_parsed(**config)
+ self._templates = templates
+ self._doxygen_tags = None
+
+ @staticmethod
+ def _javadoc_comment(block: str) -> str:
+ """Build deblanked JavaDoc-style (C-style) comment block."""
+ block = ["/**"] + block.strip().split("\n")
+ block = "\n * ".join(block).strip() + "\n */"
+ return "\n".join([line.rstrip() for line in block.split("\n")])
+
+ def _scaffold_annotation(self, c_func: CFunc, what: str) -> str:
+ """Build annotation block via Jinja."""
+ loader = Environment(loader=BaseLoader, trim_blocks=True, lstrip_blocks=True)
+ par_template = loader.from_string(self._templates["clib-param"])
+ template = loader.from_string(self._templates["clib-comment"])
+
+ def param(item: Param) -> str:
+ ret = par_template.render(par=item)
+ return f"{ret:<19} {item.description}"
+
+ implements = what
+ if isinstance(c_func.implements, CFunc):
+ implements += f": {c_func.implements.short_declaration()}"
+ block = template.render(
+ brief=c_func.brief,
+ params=[param(par) for par in c_func.arglist],
+ returns=c_func.returns, implements=implements,
+ relates=[f"{uu.base}::{uu.name}()" for uu in c_func.uses])
+ return self._javadoc_comment(block)
+
+ def _handle_crosswalk(self, what: str, crosswalk: dict, derived: list[str]) -> str:
+ """Crosswalk for object handle."""
+ cabinet = None
+ classes = list(self._config.includes.keys()) + derived
+ for base in classes:
+ ret_type = what.replace(f"<{base}>", "")
+ if ret_type in crosswalk:
+ ret_type = crosswalk[ret_type]
+ cabinet = base
+ break
+ if cabinet:
+ # successful crosswalk with cabinet object
+ return cabinet
+
+ msg = f"Failed crosswalk for handle type {what!r} using {classes}."
+ _LOGGER.critical(msg)
+ sys.exit(1)
+
+ def _ret_crosswalk(
+ self, what: str, derived: list[str]) -> tuple[Param, list[Param]]:
+ """Crosswalk for return type."""
+ what = what.replace("virtual ", "")
+ if what in self._config.ret_type_crosswalk:
+ ret_type = self._config.ret_type_crosswalk[what]
+ if ret_type == "char*":
+ # string expressions require special handling
+ returns = Param(
+ "int", "",
+ "Actual length of string or -1 for exception handling.")
+ buffer = [
+ Param("int", "bufLen", "Length of reserved array.", "in"),
+ Param(ret_type, "buf", "Returned string value.", "out")]
+ return returns, buffer
+ if ret_type == "void":
+ returns = Param(
+ "int", "", "Zero for success or -1 for exception handling.")
+ return returns, []
+ if not any([what.startswith("shared_ptr"), ret_type.endswith("[]")]):
+ # direct correspondence
+ return Param(ret_type), []
+
+ # all other types require reserved buffers
+ what = "vector" if what.startswith("vector") else "array"
+ returns = Param(
+ "int", "", f"Actual length of {what} including \0 or -1 for exception handling.")
+ buffer = [
+ Param("int", "bufLen", "Length of reserved array.", "in"),
+ Param(ret_type, "valueBuf", f"Returned {what} value.", "out")]
+ return returns, buffer
+
+ if "shared_ptr" in what:
+ # check for crosswalk with object from includes
+ handle = self._handle_crosswalk(
+ what, self._config.ret_type_crosswalk, derived)
+ returns = Param(
+ "int", "",
+ f"Handle to stored {handle} object or -1 for exception handling.")
+ return returns, []
+
+ msg = f"Failed crosswalk for return type {what!r}."
+ _LOGGER.critical(msg)
+ sys.exit(1)
+
+ def _prop_crosswalk(self, par_list: list[Param]) -> list[Param]:
+ """Crosswalk for argument type."""
+ if not par_list:
+ return []
+ params = []
+ for par in par_list:
+ what = par.p_type
+ if what in self._config.prop_type_crosswalk:
+ if "vector<" in what:
+ params.append(
+ Param("int", f"{par.name}Len",
+ f"Length of vector reserved for {par.name}.", "in"))
+ elif what.endswith("* const") or what.endswith("double*"):
+ direction = "in" if what.startswith("const") else "out"
+ params.append(
+ Param("int", f"{par.name}Len",
+ f"Length of array reserved for {par.name}.", direction))
+ ret_type = self._config.prop_type_crosswalk[what]
+ params.append(Param(ret_type, par.name, par.description, par.direction))
+ elif "shared_ptr" in what:
+ handle = self._handle_crosswalk(
+ what, self._config.prop_type_crosswalk, [])
+ if "vector<" in what:
+ params.append(
+ Param("int", f"{par.name}Len",
+ f"Length of array reserved for {par.name}.", "in"))
+ description = f"Memory holding {handle} objects. "
+ description += par.description
+ params.append(Param("const int*", par.name, description.strip()))
+ else:
+ description = f"Integer handle to {handle} object. "
+ description += par.description
+ params.append(
+ Param("int", par.name, description.strip(), par.direction))
+ else:
+ msg = f"Failed crosswalk for argument type {what!r}."
+ _LOGGER.critical(msg)
+ sys.exit(1)
+ return params
+
+ @staticmethod
+ def _reverse_crosswalk(c_func: CFunc, base: str) -> tuple[dict[str, str], set[str]]:
+ """Translate CLib arguments back to Jinja argument list."""
+ handle = ""
+ args = []
+ lines = []
+ buffer = []
+ bases = set()
+
+ def shared_object(cxx_type) -> str:
+ """Extract object type from shared_ptr."""
+ if "shared_ptr<" not in cxx_type:
+ return None
+ return cxx_type.split("<")[-1].split(">")[0]
+
+ c_args = c_func.arglist
+ cxx_func = c_func.implements
+ if not cxx_func:
+ if len(c_args) and "char*" in c_args[-1].p_type:
+ cxx_func = CFunc("string", "dummy", ArgList([]), "", None, "", "base")
+ else:
+ cxx_func = CFunc("void", "dummy", ArgList([]), "", None, "", "base")
+ cxx_ix = 0
+ check_array = False
+ for c_ix, c_par in enumerate(c_func.arglist):
+ c_name = c_par.name
+ if cxx_ix >= len(cxx_func.arglist):
+ if c_ix == 0 and cxx_func.base and "len" not in c_name.lower():
+ handle = c_name
+ c_ix += 1
+ if c_ix == len(c_args):
+ break
+ cxx_type = cxx_func.ret_type
+
+ # Handle output buffer
+ if "string" in cxx_type: # or cxx_func.name == "dummy":
+ buffer = ["string out",
+ f"copyString(out, {c_args[c_ix+1].name}, "
+ f"{c_args[c_ix].name});",
+ "int(out.size()) + 1"] # include \0
+ else:
+ msg = (f"Scaffolding failed for {c_func.name!r}: reverse crosswalk "
+ f"not implemented for {cxx_type!r}:\n{c_func.declaration()}")
+ _LOGGER.critical(msg)
+ exit(1)
+ break
+
+ cxx_arg = cxx_func.arglist[cxx_ix]
+ if c_name != cxx_arg.name:
+ # Encountered object handle or length indicator
+ if c_ix == 0:
+ handle = c_name
+ elif c_name.endswith("Len"):
+ check_array = True
+ else:
+ msg = (f"Scaffolding failed for {c_func.name!r}: "
+ f"unexpected behavior for {c_name!r}.")
+ _LOGGER.critical(msg)
+ exit(1)
+ continue
+
+ cxx_type = cxx_arg.p_type
+ if check_array:
+ # Need to handle cross-walked parameter with length information
+ c_prev = c_args[c_ix-1].name
+ if "vector>
+ cxx_type = cxx_type.lstrip("const ").rstrip("&")
+ lines.extend([
+ f"{cxx_type} {c_name}_;",
+ f"for (int i = 0; i < {c_prev}; i++) {{",
+ f" {c_name}_.push_back({base}Cabinet::at({c_name}[i]));",
+ "}",
+ ])
+ args.append(f"{c_name}_")
+ elif "vector" in cxx_type:
+ # Example: vector par_(par, par + parLen);
+ cxx_type = cxx_type.rstrip("&")
+ lines.append(
+ f"{cxx_type} {c_name}_({c_name}, {c_name} + {c_prev});")
+ args.append(f"{c_name}_")
+ elif "*" in cxx_type:
+ # Can be passed directly; example: double *const
+ args.append(c_name)
+ else:
+ msg = (f"Scaffolding failed for {c_func.name!r}: reverse "
+ f"crosswalk not implemented for {cxx_type!r}.")
+ _LOGGER.critical(msg)
+ exit(1)
+ check_array = False
+ elif "shared_ptr" in cxx_type:
+ # Retrieve object from cabinet
+ obj_base = shared_object(cxx_type)
+ args.append(f"{base}Cabinet::at({c_name})")
+ if obj_base != base:
+ bases |= {obj_base}
+ elif cxx_type == "bool":
+ lines.append(f"bool {c_name}_ = ({c_name} != 0);")
+ args.append(f"{c_name}_")
+ else:
+ # Regular parameter
+ args.append(c_name)
+ cxx_ix += 1
+
+ # Obtain class and getter for managed objects
+ uses = [(shared_object(uu.ret_type), uu.name) for uu in c_func.uses]
+ bases |= {uu[0] for uu in uses if uu[0]}
+
+ # Ensure that all error codes are set correctly
+ error = [-1, "ERR"]
+ cxx_type = cxx_func.ret_type
+ if cxx_type.endswith("int") or cxx_type.endswith("size_t"):
+ error = ["ERR", "ERR"]
+ elif cxx_type.endswith("double"):
+ error = ["DERR", "DERR"]
+ elif "shared_ptr" in cxx_type:
+ obj_base = shared_object(cxx_type)
+ if obj_base == base:
+ buffer = ["auto obj", "", f"{obj_base}Cabinet::index(*obj)"]
+ else:
+ buffer = ["auto obj", "", f"{obj_base}Cabinet::index(*obj, {handle})"]
+ bases |= {obj_base}
+ error = ["-2", "ERR"]
+ elif cxx_type.endswith("void"):
+ buffer = ["", "", "0"]
+
+ ret = {
+ "handle": handle, "lines": lines, "buffer": buffer, "uses": uses,
+ "cxx_base": base, "cxx_name": cxx_func.name, "cxx_args": args,
+ "cxx_implements": cxx_func.short_declaration(), "error": error,
+ "c_func": c_func.name, "c_args": [arg.name for arg in c_func.arglist],
+ }
+ return ret, bases
+
+ def _scaffold_body(self, c_func: CFunc, recipe: Recipe) -> tuple[str, set[str]]:
+ """Scaffold body of generic CLib function via Jinja."""
+ loader = Environment(loader=BaseLoader, trim_blocks=True, lstrip_blocks=True)
+ args, bases = self._reverse_crosswalk(c_func, recipe.base)
+ args["what"] = recipe.what
+
+ if recipe.code:
+ # override auto-generated code
+ template = loader.from_string(self._templates["clib-custom-code"])
+ args["lines"] = recipe.code.strip(" \n").split("\n")
+
+ elif recipe.what == "noop":
+ template = loader.from_string(self._templates["clib-noop"])
+
+ elif recipe.what == "function":
+ template = loader.from_string(self._templates["clib-function"])
+
+ elif recipe.what == "constructor":
+ template = loader.from_string(self._templates["clib-constructor"])
+
+ elif recipe.what == "destructor":
+ template = loader.from_string(self._templates["clib-destructor"])
+
+ elif recipe.what == "method":
+ template = loader.from_string(self._templates["clib-method"])
+
+ elif recipe.what == "getter":
+ if "void" in c_func.implements.ret_type:
+ template = loader.from_string(self._templates["clib-array-getter"])
+ else:
+ template = loader.from_string(self._templates["clib-method"])
+
+ elif recipe.what == "setter":
+ if "*" in c_func.implements.arglist[0].p_type:
+ template = loader.from_string(self._templates["clib-array-setter"])
+ else:
+ template = loader.from_string(self._templates["clib-method"])
+
+ elif recipe.what == "reserved":
+ args["cabinets"] = [kk for kk in self._clib_bases if kk]
+ template = loader.from_string(
+ self._templates[f"clib-reserved-{recipe.name}-cpp"])
+
+ else:
+ msg = f"{recipe.what!r} not implemented: {c_func.name!r}."
+ _LOGGER.critical(msg)
+ exit(1)
+
+ return template.render(**args), bases
+
+ def _resolve_recipe(self, recipe: Recipe) -> CFunc:
+ """Build CLib header from recipe and doxygen annotations."""
+ def merge_params(implements: str, cxx_func: CFunc) -> tuple[list[Param], int]:
+ """Create preliminary CLib argument list."""
+ obj_handle = []
+ if "::" in implements:
+ # If class method, add handle as first parameter
+ what = implements.split("::")[0]
+ obj_handle.append(
+ Param("int", "handle", f"Handle to queried {what} object."))
+ if "(" not in implements:
+ return obj_handle + cxx_func.arglist.params, cxx_func
+
+ # Signature may skip C++ default parameters
+ args_short = CFunc.from_str(implements).arglist
+ if len(args_short) < len(cxx_func.arglist):
+ cxx_arglist = ArgList(cxx_func.arglist[:len(args_short)])
+ cxx_func = CFunc(cxx_func.ret_type, cxx_func.name,
+ cxx_arglist, cxx_func.brief, cxx_func.implements,
+ cxx_func.returns, cxx_func.base, cxx_func.uses)
+
+ return obj_handle + cxx_func.arglist.params, cxx_func
+
+ func_name = f"{recipe.prefix}_{recipe.name}"
+ reserved = ["cabinetSize", "parentHandle",
+ "getCanteraError", "clearStorage", "resetStorage"]
+ if recipe.name in reserved:
+ recipe.what = "reserved"
+ loader = Environment(loader=BaseLoader)
+ msg = f" generating {func_name!r} -> {recipe.what}"
+ _LOGGER.debug(msg)
+ header = loader.from_string(
+ self._templates[f"clib-reserved-{recipe.name}-h"]
+ ).render(base=recipe.base, prefix=recipe.prefix)
+ return CFunc.from_str(header, brief=recipe.brief)
+
+ # Ensure that all functions/methods referenced in recipe are detected correctly
+ bases = [recipe.base] + recipe.parents + recipe.derived
+ if not recipe.implements:
+ recipe.implements = self._doxygen_tags.detect(recipe.name, bases)
+ recipe.uses = [self._doxygen_tags.detect(uu.split("(")[0], bases, False)
+ for uu in recipe.uses]
+
+ cxx_func = None
+ ret_param = Param("void")
+ args = []
+ brief = ""
+
+ if recipe.implements:
+ msg = f" generating {func_name!r} -> {recipe.implements}"
+ _LOGGER.debug(msg)
+ cxx_func = self._doxygen_tags.cxx_func(recipe.implements)
+
+ # Convert C++ return type to format suitable for crosswalk:
+ # Incompatible return parameters are buffered and appended to back
+ ret_param, buffer_params = self._ret_crosswalk(
+ cxx_func.ret_type, recipe.derived)
+ par_list, cxx_func = merge_params(recipe.implements, cxx_func)
+ prop_params = self._prop_crosswalk(par_list)
+ brief = cxx_func.brief
+ args = prop_params + buffer_params
+
+ if cxx_func and not recipe.what:
+ # Autodetection of CLib function purpose ("what")
+ cxx_arglen = len(cxx_func.arglist)
+ if not cxx_func.base:
+ if (cxx_func.name.startswith("new") and
+ any(base in cxx_func.ret_type
+ for base in [recipe.base] + recipe.derived)):
+ recipe.what = "constructor"
+ else:
+ recipe.what = "function"
+ elif "void" not in cxx_func.ret_type and cxx_arglen == 0:
+ recipe.what = "getter"
+ elif "void" in cxx_func.ret_type and cxx_arglen == 1:
+ p_type = cxx_func.arglist[0].p_type
+ if cxx_func.name.startswith("get"):
+ recipe.what = "getter"
+ elif "*" in p_type and not p_type.startswith("const"):
+ recipe.what = "getter" # getter assigns to existing array
+ else:
+ recipe.what = "setter"
+ elif any(recipe.implements.startswith(base)
+ for base in [recipe.base] + recipe.parents + recipe.derived):
+ recipe.what = "method"
+ else:
+ _LOGGER.critical("Unable to auto-detect function type.")
+ exit(1)
+
+ elif recipe.name == "del" and not recipe.what:
+ recipe.what = "destructor"
+
+ if recipe.what in ["destructor", "noop"]:
+ # these function types don't have direct C++ equivalents
+ msg = f" generating {func_name!r} -> {recipe.what}"
+ _LOGGER.debug(msg)
+ if recipe.what == "noop":
+ args = []
+ brief= "No operation."
+ ret_param = Param(
+ "int", "", "Always zero.")
+ else:
+ args = [Param("int", "handle", f"Handle to {recipe.base} object.")]
+ brief= f"Delete {recipe.base} object."
+ ret_param = Param(
+ "int", "", "Zero for success and -1 for exception handling.")
+ buffer_params = []
+
+ if recipe.brief:
+ brief = recipe.brief
+ uses = [self._doxygen_tags.cxx_func(uu) for uu in recipe.uses]
+ return CFunc(ret_param.p_type, func_name, ArgList(args), brief, cxx_func,
+ ret_param.description, None, uses)
+
+ def _write_header(self, headers: HeaderFile) -> None:
+ """Parse header specification and generate header file."""
+ loader = Environment(loader=BaseLoader, trim_blocks=True, lstrip_blocks=True)
+
+ filename = headers.output_name(suffix=".h", auto="3")
+ msg = f" scaffolding {filename.name!r}"
+ _LOGGER.info(msg)
+
+ template = loader.from_string(self._templates["clib-definition"])
+ declarations = []
+ for c_func, recipe in zip(headers.funcs, headers.recipes):
+ msg = f" scaffolding {c_func.name!r} header"
+ _LOGGER.debug(msg)
+ declarations.append(
+ template.render(
+ declaration=c_func.declaration(),
+ annotations=self._scaffold_annotation(c_func, recipe.what)))
+ declarations = "\n\n".join(declarations)
+
+ guard = f"__{filename.name.upper().replace('.', '_')}__"
+ template = loader.from_string(self._templates["clib-header-file"])
+ output = template.render(
+ name=filename.stem, guard=guard, declarations=declarations,
+ prefix=headers.prefix, base=headers.base, docstring=headers.docstring)
+
+ out = (Path(self._out_dir) /
+ "include" / "cantera" / "clib_experimental" / filename.name)
+ msg = f" writing {filename.name!r}"
+ _LOGGER.info(msg)
+ out.parent.mkdir(parents=True, exist_ok=True)
+ out.write_text(output + "\n")
+
+ def _write_implementation(self, headers: HeaderFile) -> None:
+ """Parse header specification and generate implementation file."""
+ loader = Environment(loader=BaseLoader, trim_blocks=True, lstrip_blocks=True)
+
+ filename = headers.output_name(suffix=".cpp", auto="3")
+ msg = f" scaffolding {filename.name!r}"
+ _LOGGER.info(msg)
+
+ template = loader.from_string(self._templates["clib-implementation"])
+ implementations = []
+ other = set()
+ for c_func, recipe in zip(headers.funcs, headers.recipes):
+ msg = f" scaffolding {c_func.name!r} implementation"
+ _LOGGER.debug(msg)
+ body, bases = self._scaffold_body(c_func, recipe)
+ implementations.append(
+ template.render(declaration=c_func.declaration(),body=body))
+ other |= bases
+ implementations = "\n\n".join(implementations)
+ str_utils = "copyString" in implementations
+
+ if not headers.base:
+ # main CLib file receives references to all cabinets
+ other = [kk for kk in self._clib_bases if kk]
+ includes = []
+ for obj in [headers.base] + list(other):
+ includes += self._config.includes[obj]
+
+ template = loader.from_string(self._templates["clib-source-file"])
+ output = template.render(
+ name=filename.stem, implementations=implementations,
+ prefix=headers.prefix, base=headers.base, docstring=headers.docstring,
+ includes=includes, other=other, str_utils=str_utils)
+
+ out = Path(self._out_dir) / "src" / "clib_experimental" / filename.name
+ msg = f" writing {filename.name!r}"
+ _LOGGER.info(msg)
+ out.parent.mkdir(parents=True, exist_ok=True)
+ out.write_text(output + "\n")
+
+ def resolve_tags(self, headers_files: list[HeaderFile]) -> None:
+ """Resolve doxygen tags."""
+ def get_bases() -> tuple[list[str], list[str]]:
+ bases = set()
+ classes = set()
+ for headers in headers_files:
+ bases |= {headers.base}
+ for recipe in headers.recipes:
+ classes |= set([recipe.base] + recipe.parents + recipe.derived)
+ return sorted(bases), sorted(classes)
+
+ self._clib_bases, classes = get_bases()
+ self._doxygen_tags = TagFileParser(classes)
+
+ for headers in headers_files:
+ msg = f" resolving recipes in {headers.path.name!r}:"
+ _LOGGER.info(msg)
+ c_funcs = []
+ for recipe in headers.recipes:
+ c_funcs.append(self._resolve_recipe(recipe))
+ headers.funcs = c_funcs
+
+ def generate_source(self, headers_files: list[HeaderFile]) -> None:
+ """Generate output."""
+ self.resolve_tags(headers_files)
+
+ for headers in headers_files:
+ self._write_header(headers)
+ self._write_implementation(headers)
diff --git a/interfaces/sourcegen/sourcegen/clib/_Config.py b/interfaces/sourcegen/sourcegen/clib/_Config.py
new file mode 100644
index 0000000000..1de5c5bee3
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/clib/_Config.py
@@ -0,0 +1,47 @@
+"""Configuration data class used by CLib source generator."""
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+from dataclasses import dataclass
+from typing_extensions import Self
+
+
+@dataclass(frozen=True)
+class Config:
+ """Provides configuration info for the CLibSourceGenerator class"""
+
+ ret_type_crosswalk = {
+ "bool": "int",
+ "void": "int",
+ "int": "int",
+ "size_t": "int",
+ "double": "double",
+ "shared_ptr": "int",
+ "string": "char*",
+ "vector": "double[]",
+ "vector": "int[]",
+ }
+
+ prop_type_crosswalk = {
+ "bool": "int",
+ "int": "int",
+ "size_t": "int",
+ "double": "double",
+ "const double": "double",
+ "double*": "double*",
+ "double* const": "double*",
+ "const double* const": "const double*",
+ "const string&": "const char*",
+ "shared_ptr": "int",
+ "const shared_ptr": "int",
+ "const vector&": "const double*",
+ "const vector>&": "int[]",
+ }
+
+ includes: dict[str, list[str]]
+
+ @classmethod
+ def from_parsed(cls: Self, *, includes: dict[str, list[str]] | None = None) -> Self:
+ """Ensure that configurations are correct."""
+ return cls(includes or {})
diff --git a/interfaces/sourcegen/sourcegen/clib/__init__.py b/interfaces/sourcegen/sourcegen/clib/__init__.py
new file mode 100644
index 0000000000..cd19070a83
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/clib/__init__.py
@@ -0,0 +1,4 @@
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+from ._CLibSourceGenerator import CLibSourceGenerator
diff --git a/interfaces/sourcegen/sourcegen/clib/config.yaml b/interfaces/sourcegen/sourcegen/clib/config.yaml
new file mode 100644
index 0000000000..f5059f5830
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/clib/config.yaml
@@ -0,0 +1,28 @@
+# Configuration for CLib code generation.
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+# Ignore these files entirely:
+ignore_files: []
+
+# Ignore these specific functions:
+ignore_funcs: {}
+ # ctsol_auto.yaml: [setTransport]
+
+# Cabinets with associated includes
+includes:
+ "":
+ - cantera/base/global.h
+ Solution:
+ - cantera/base/Solution.h
+ Interface:
+ - cantera/base/Interface.h
+ ThermoPhase:
+ - cantera/thermo/ThermoFactory.h
+ Kinetics:
+ - cantera/kinetics/KineticsFactory.h
+ Transport:
+ - cantera/transport/TransportFactory.h
+ Func1:
+ - cantera/numerics/Func1Factory.h
diff --git a/interfaces/sourcegen/sourcegen/clib/templates.yaml b/interfaces/sourcegen/sourcegen/clib/templates.yaml
new file mode 100644
index 0000000000..c99511d095
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/clib/templates.yaml
@@ -0,0 +1,368 @@
+# Definitions used for Jinja template replacement.
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+clib-param: |-
+ @param{{ '[' + par.direction + ']' if par.direction }} {{ par.name }}
+
+clib-comment: |-
+ {{ brief }}
+
+ {% for par in params %}
+ {{ par }}
+ {% endfor %}
+ {% if returns %}
+ {{ '@returns' + 12*' ' + returns }}
+ {% endif %}
+ {% if params or returns %}
+
+ {% endif %}
+ {% if implements %}
+ {{ '@implements ' + implements }}
+ {% endif %}
+ {% if relates %}
+ {{ '@relates ' + ', '.join(relates) }}
+ {% endif %}
+
+clib-definition: |-
+ {{ annotations }}
+ {{ declaration }};
+
+clib-reserved-parentHandle-h: |-
+ /**
+ * Return handle to parent of {{ base }} object.
+ * @param handle Handle to queried {{ base }} object.
+ * @returns Parent handle or -1 for exception handling.
+ */
+ int {{ prefix }}_parentHandle(int handle);
+
+clib-reserved-cabinetSize-h: |-
+ /**
+ * Return size of {{ base }} storage.
+ * @returns Size or -1 for exception handling.
+ */
+ int {{ prefix }}_cabinetSize();
+
+clib-reserved-getCanteraError-h: |-
+ /**
+ * Get %Cantera error.
+ * @param[in] bufLen Length of reserved array.
+ * @param[out] buf String containing %Cantera error.
+ * @returns Actual length of string or -1 for exception handling.
+ */
+ int {{ prefix }}_getCanteraError(int bufLen, char* buf);
+
+clib-reserved-resetStorage-h: |-
+ /**
+ * Delete all objects and erase mapping.
+ * @returns Zero if successful or -1 for exception handling.
+ */
+ int {{ prefix }}_resetStorage();
+
+clib-reserved-clearStorage-h: |-
+ /**
+ * Delete all objects with mapping preserved.
+ * @returns Zero if successful or -1 for exception handling.
+ */
+ int {{ prefix }}_clearStorage();
+
+clib-header-file: |-
+ /**
+ * {{ name.upper() }} - Experimental CLib %Cantera interface library.
+ *
+ * @file {{ name }}.cpp
+ *
+ {% if docstring %}
+ {% for line in docstring %}
+ * {{ line }}
+ {% endfor %}
+ *
+ {% endif %}
+ * This library of functions is designed to encapsulate %Cantera functionality
+ * and make it available for use in languages and applications other than C++.
+ * A set of library functions is provided that are declared "extern C". All
+ * %Cantera objects are stored and referenced by integers - no pointers are
+ * passed to or from the calling application.
+ *
+ * This file was generated by sourcegen. It will be re-generated by the
+ * %Cantera build process. Do not manually edit.
+ *
+ * @warning This module is an experimental part of the %Cantera API and
+ * may be changed or removed without notice.
+ */
+
+ // This file is part of Cantera. See License.txt in the top-level directory or
+ // at https://cantera.org/license.txt for license and copyright information.
+
+ #ifndef {{ guard }}
+ #define {{ guard }}
+
+ #ifdef __cplusplus
+ extern "C" {
+ #endif
+
+ {{ declarations | indent(4) }}
+
+ #ifdef __cplusplus
+ }
+ #endif
+
+ #endif // {{ guard }}
+
+clib-function: |-
+ // function: {{ cxx_implements }}
+ try {
+ {% for line in lines %}
+ {{ line }}
+ {% endfor %}
+ {% if buffer %}
+ {% if buffer[0] %}
+ {{ buffer[0] }} = {{ cxx_name }}({{ ', '.join(cxx_args) }});
+ {% else %}
+ {{ cxx_name }}({{ ', '.join(cxx_args) }});
+ {% endif %}
+ {% if buffer[1] %}
+ {{ buffer[1] }}
+ {% endif %}
+ return {{ buffer[2] }};
+ {% else %}
+ return {{ cxx_name }}({{ ', '.join(cxx_args) }});
+ {% endif %}
+ } catch (...) {
+ return handleAllExceptions({{ error[0] }}, {{ error[1] }});
+ }
+
+clib-constructor: |-
+ // constructor: {{ cxx_implements }}
+ try {
+ {% for line in lines %}
+ {{ line }}
+ {% endfor %}
+ {% if uses %}
+ {% if handle %}
+ auto obj = {{ cxx_base }}Cabinet::at({{ handle }})->{{ cxx_name }}({{ ', '.join(cxx_args) }});
+ {% else %}
+ auto obj = {{ cxx_name }}({{ ', '.join(cxx_args) }});
+ {% endif %}
+ int id = {{ cxx_base }}Cabinet::add(obj);
+ // add all associated objects
+ {% for typ, getter in uses %}
+ if (obj->{{ getter }}()) {
+ {{ typ }}Cabinet::add(obj->{{ getter }}(), id);
+ }
+ {% endfor %}
+ return id;
+ {% else %}
+ {% if handle %}
+ return {{ cxx_base }}Cabinet::add({{ cxx_base }}Cabinet::at({{ handle }})->{{ cxx_name }}({{ ', '.join(cxx_args) }}));
+ {% else %}
+ return {{ cxx_base }}Cabinet::add({{ cxx_name }}({{ ', '.join(cxx_args) }}));
+ {% endif %}
+ {% endif %}
+ } catch (...) {
+ return handleAllExceptions({{ error[0] }}, {{ error[1] }});
+ }
+
+clib-destructor: |-
+ // destructor
+ try {
+ {% if uses %}
+ auto obj = {{ cxx_base }}Cabinet::at({{ handle }});
+ // remove all associated objects in reversed order
+ {% for typ, getter in uses[-1::-1] %}
+ if (obj->{{ getter }}()) {
+ int index = {{ typ }}Cabinet::index(*(obj->{{ getter }}()), {{ handle }});
+ if (index >= 0) {
+ {{ typ }}Cabinet::del(index);
+ }
+ }
+ {% endfor %}
+ {% endif %}
+ {{ cxx_base }}Cabinet::del({{ handle }});
+ return 0;
+ } catch (...) {
+ return handleAllExceptions(-1, ERR);
+ }
+
+clib-method: |-
+ // {{ what }}: {{ cxx_implements }}
+ try {
+ {% for line in lines %}
+ {{ line }}
+ {% endfor %}
+ {% if buffer %}
+ {% if buffer[0] %}
+ {{ buffer[0] }} = {{ cxx_base }}Cabinet::at({{ handle }})->{{ cxx_name }}({{ ', '.join(cxx_args) }});
+ {% else %}
+ {{ cxx_base }}Cabinet::at({{ handle }})->{{ cxx_name }}({{ ', '.join(cxx_args) }});
+ {% endif %}
+ {% if buffer[1] %}
+ {{ buffer[1] }}
+ {% endif %}
+ return {{ buffer[2] }};
+ {% else %}
+ return {{ cxx_base }}Cabinet::at({{ handle }})->{{ cxx_name }}({{ ', '.join(cxx_args) }});
+ {% endif %}
+ } catch (...) {
+ return handleAllExceptions({{ error[0] }}, {{ error[1] }});
+ }
+
+clib-array-getter: |-
+ // getter: {{ cxx_implements }}
+ try {
+ auto& obj = {{ cxx_base }}Cabinet::at({{ handle }});
+ {% if uses %}
+ if ({{ c_args[1] }} != obj->{{ uses[0][1] }}()) {
+ throw CanteraError("{{ c_func }}",
+ "Invalid output array size; expected size {} but received {}.",
+ obj->{{ uses[0][1] }}(), {{ c_args[1] }});
+ }
+ {% else %}
+ // no size checking specified
+ {% endif %}
+ obj->{{ cxx_name }}({{ cxx_args[0] }});
+ return 0;
+ } catch (...) {
+ return handleAllExceptions(-1, ERR);
+ }
+
+clib-array-setter: |-
+ // setter: {{ cxx_implements }}
+ try {
+ auto& obj = {{ cxx_base }}Cabinet::at({{ handle }});
+ {% if uses %}
+ if ({{ c_args[1] }} != obj->{{ uses[0][1] }}()) {
+ throw CanteraError("{{ c_func }}",
+ "Invalid input array size; expected size {} but received {}.",
+ obj->{{ uses[0][1] }}(), {{ c_args[1] }});
+ }
+ {% else %}
+ // no size checking specified
+ {% endif %}
+ obj->{{ cxx_name }}({{ cxx_args[0] }});
+ return 0;
+ } catch (...) {
+ return handleAllExceptions(-1, ERR);
+ }
+
+clib-noop: |-
+ // no-op
+ return 0;
+
+clib-custom-code: |-
+ // {{ what }}: {{ cxx_implements }}
+ // ********** custom code begin **********
+ {% for line in lines %}
+ {{ line }}
+ {% endfor %}
+ // *********** custom code end ***********
+
+clib-implementation: |-
+ {{ declaration }}
+ {
+ {{ body | indent(4) }}
+ }
+
+clib-reserved-parentHandle-cpp: |-
+ // reserved: {{ cxx_base }} cabinet parent
+ try {
+ return {{ cxx_base }}Cabinet::parent(handle);
+ } catch (...) {
+ return handleAllExceptions(-2, ERR);
+ }
+
+clib-reserved-cabinetSize-cpp: |-
+ // reserved: int Cabinet<{{ cxx_base }}>.size()
+ try {
+ return {{ cxx_base }}Cabinet::size();
+ } catch (...) {
+ return handleAllExceptions(-1, ERR);
+ }
+
+clib-reserved-getCanteraError-cpp: |-
+ // reserved: string Application::Instance()->lastErrorMessage();
+ try {
+ string err = Application::Instance()->lastErrorMessage();
+ copyString(err, buf, bufLen);
+ return int(err.size());
+ } catch (...) {
+ return handleAllExceptions(-1, ERR);
+ }
+
+clib-reserved-resetStorage-cpp: |-
+ // reserved: void Cabinet::reset()
+ try {
+ {% for base in cabinets %}
+ {{ base }}Cabinet::reset();
+ {% endfor %}
+ return 0;
+ } catch (...) {
+ return handleAllExceptions(-1, ERR);
+ }
+
+clib-reserved-clearStorage-cpp: |-
+ // reserved: void Cabinet::clear()
+ try {
+ {% for base in cabinets %}
+ {{ base }}Cabinet::clear();
+ {% endfor %}
+ return 0;
+ } catch (...) {
+ return handleAllExceptions(-1, ERR);
+ }
+
+clib-source-file: |-
+ /**
+ * {{ name.upper() }} - Experimental CLib %Cantera interface library.
+ *
+ * @file {{ name }}.cpp
+ *
+ {% if docstring %}
+ {% for line in docstring %}
+ * {{ line }}
+ {% endfor %}
+ *
+ {% endif %}
+ * This file was generated by sourcegen. It will be re-generated by the
+ * %Cantera build process. Do not manually edit.
+ *
+ * @warning This module is an experimental part of the %Cantera API and
+ * may be changed or removed without notice.
+ */
+
+ // This file is part of Cantera. See License.txt in the top-level directory or
+ // at https://cantera.org/license.txt for license and copyright information.
+
+ #include "cantera/clib_experimental/{{ name }}.h"
+ #include "../clib/clib_utils.h"
+
+ {% if str_utils %}
+ #include "cantera/base/stringUtils.h"
+ {% endif %}
+ {% for entry in includes %}
+ #include "{{ entry }}"
+ {% endfor %}
+
+ using namespace Cantera;
+
+ {% if base %}
+ // Define Cabinet<{{ base }}> (single-instance object)
+ typedef Cabinet<{{ base }}> {{ base }}Cabinet;
+ // Note: cabinet is already created by the traditional CLib interface
+ // template<> {{ base }}Cabinet* {{ base }}Cabinet::s_storage = 0; // initialized here
+ template<> {{ base }}Cabinet* {{ base }}Cabinet::s_storage; // temporary
+
+ {% endif %}
+ {% if other %}
+ {% for entry in other %}
+ typedef Cabinet<{{ entry }}> {{ entry }}Cabinet;
+ template<> {{ entry }}Cabinet* {{ entry }}Cabinet::s_storage; // initialized elsewhere
+
+ {% endfor %}
+ {% endif %}
+ extern "C" {
+
+ {{ implementations | indent(4) }}
+
+ } // extern "C"
diff --git a/interfaces/sourcegen/sourcegen/csharp/_CSharpSourceGenerator.py b/interfaces/sourcegen/sourcegen/csharp/_CSharpSourceGenerator.py
index 2e2548aa9b..fd875b6faa 100644
--- a/interfaces/sourcegen/sourcegen/csharp/_CSharpSourceGenerator.py
+++ b/interfaces/sourcegen/sourcegen/csharp/_CSharpSourceGenerator.py
@@ -4,7 +4,6 @@
from pathlib import Path
import sys
import logging
-from typing import List, Dict
import re
from jinja2 import Environment, BaseLoader
@@ -21,8 +20,18 @@
class CSharpSourceGenerator(SourceGenerator):
"""The SourceGenerator for scaffolding C# files for the .NET interface"""
+ def __init__(self, out_dir: str, config: dict, templates: dict) -> None:
+ if not out_dir:
+ _logger.critical("Non-empty string identifying output path required.")
+ sys.exit(1)
+ self._out_dir = Path(out_dir)
+
+ # use the typed config
+ self._config = Config.from_parsed(**config)
+ self._templates = templates
+
def _get_property_text(self, clib_area: str, c_name: str, cs_name: str,
- known_funcs: Dict[str, CsFunc]) -> str:
+ known_funcs: dict[str, CsFunc]) -> str:
getter = known_funcs.get(clib_area + "_" + c_name)
if getter:
@@ -57,16 +66,6 @@ def _get_property_text(self, clib_area: str, c_name: str, cs_name: str,
_logger.critical(f"Unable to scaffold properties of type {prop_type!r}!")
sys.exit(1)
- def __init__(self, out_dir: str, config: dict, templates: dict):
- if not out_dir:
- _logger.critical("Non-empty string identifying output path required.")
- sys.exit(1)
- self._out_dir = Path(out_dir)
-
- # use the typed config
- self._config = Config.from_parsed(**config)
- self._templates = templates
-
def _get_wrapper_class_name(self, clib_area: str) -> str:
return self._config.class_crosswalk[clib_area]
@@ -160,7 +159,7 @@ def _write_file(self, file_name: str, template_name: str, **kwargs) -> None:
self._out_dir.joinpath(file_name).write_text(contents, encoding="utf-8")
- def _scaffold_interop(self, header_file_path: Path, cs_funcs: List[CsFunc]):
+ def _scaffold_interop(self, header_file_path: Path, cs_funcs: list[CsFunc]) -> None:
template = _loader.from_string(self._templates["csharp-interop-func"])
function_list = [
template.render(unsafe=func.unsafe(), declaration=func.declaration())
@@ -170,7 +169,8 @@ def _scaffold_interop(self, header_file_path: Path, cs_funcs: List[CsFunc]):
self._write_file(
file_name, "csharp-scaffold-interop", cs_functions=function_list)
- def _scaffold_handles(self, header_file_path: Path, handles: Dict[str, str]):
+ def _scaffold_handles(
+ self, header_file_path: Path, handles: dict[str, str]) -> None:
template = _loader.from_string(self._templates["csharp-base-handle"])
handle_list = [
template.render(class_name=key, release_func_name=val)
@@ -180,7 +180,7 @@ def _scaffold_handles(self, header_file_path: Path, handles: Dict[str, str]):
self._write_file(
file_name, "csharp-scaffold-handles", cs_handles=handle_list)
- def _scaffold_derived_handles(self):
+ def _scaffold_derived_handles(self) -> None:
template = _loader.from_string(self._templates["csharp-derived-handle"])
handle_list = [
template.render(derived_class_name=key, base_class_name=val)
@@ -190,8 +190,8 @@ def _scaffold_derived_handles(self):
self._write_file(
file_name, "csharp-scaffold-handles", cs_handles=handle_list)
- def _scaffold_wrapper_class(self, clib_area: str, props: Dict[str, str],
- known_funcs: Dict[str, CsFunc]):
+ def _scaffold_wrapper_class(self, clib_area: str, props: dict[str, str],
+ known_funcs: dict[str, CsFunc]) -> None:
property_list = [
self._get_property_text(clib_area, c_name, cs_name, known_funcs)
for c_name, cs_name in props.items()]
@@ -204,10 +204,10 @@ def _scaffold_wrapper_class(self, clib_area: str, props: Dict[str, str],
wrapper_class_name=wrapper_class_name, handle_class_name=handle_class_name,
cs_properties=property_list)
- def generate_source(self, headers_files: List[HeaderFile]):
+ def generate_source(self, headers_files: list[HeaderFile]) -> None:
self._out_dir.mkdir(parents=True, exist_ok=True)
- known_funcs: Dict[str, List[CsFunc]] = {}
+ known_funcs: dict[str, list[CsFunc]] = {}
for header_file in headers_files:
cs_funcs = list(map(self._convert_func, header_file.funcs))
@@ -225,5 +225,5 @@ def generate_source(self, headers_files: List[HeaderFile]):
self._scaffold_derived_handles()
- for (clib_area, props) in self._config.wrapper_classes.items():
+ for clib_area, props in self._config.wrapper_classes.items():
self._scaffold_wrapper_class(clib_area, props, known_funcs)
diff --git a/interfaces/sourcegen/sourcegen/csharp/_Config.py b/interfaces/sourcegen/sourcegen/csharp/_Config.py
index 3d525d841d..4a0b2221d3 100644
--- a/interfaces/sourcegen/sourcegen/csharp/_Config.py
+++ b/interfaces/sourcegen/sourcegen/csharp/_Config.py
@@ -2,12 +2,12 @@
# at https://cantera.org/license.txt for license and copyright information.
from dataclasses import dataclass
-from typing import Dict
+from typing_extensions import Self
@dataclass(frozen=True)
class Config:
- """Provides configuration info for the CSharpSourceGenerator class"""
+ """Provides configuration info for the CSharpSourceGenerator class."""
ret_type_crosswalk = {
@@ -24,17 +24,19 @@ class Config:
}
# These we load from the parsed YAML config file
- class_crosswalk: Dict[str, str]
+ class_crosswalk: dict[str, str]
- class_accessors: Dict[str, str]
+ class_accessors: dict[str, str]
- derived_handles: Dict[str, str]
+ derived_handles: dict[str, str]
- wrapper_classes: Dict[str, Dict[str, str]]
+ wrapper_classes: dict[str, dict[str, str]]
@classmethod
- def from_parsed(cls, *,
- class_crosswalk=None, class_accessors=None,
- derived_handles=None, wrapper_classes=None):
+ def from_parsed(cls: Self, *,
+ class_crosswalk: dict[str, str] | None = None,
+ class_accessors: dict[str, str] | None = None,
+ derived_handles: dict[str, str] | None = None,
+ wrapper_classes: dict[str, dict[str, str]] | None = None):
return cls(class_crosswalk or {}, class_accessors or {},
derived_handles or {}, wrapper_classes or {})
diff --git a/interfaces/sourcegen/sourcegen/csharp/_dataclasses.py b/interfaces/sourcegen/sourcegen/csharp/_dataclasses.py
index d7d53b01ee..0dd653762d 100644
--- a/interfaces/sourcegen/sourcegen/csharp/_dataclasses.py
+++ b/interfaces/sourcegen/sourcegen/csharp/_dataclasses.py
@@ -2,7 +2,6 @@
# at https://cantera.org/license.txt for license and copyright information.
from dataclasses import dataclass
-from typing import Union
from .._helpers import with_unpack_iter
from .._dataclasses import Func
@@ -14,8 +13,8 @@ class CsFunc(Func):
"""Represents a C# interop method"""
is_handle_release_func: bool
- handle_class_name: Union[str, None]
+ handle_class_name: str | None
- def unsafe(self):
+ def unsafe(self) -> bool:
"""Identify pointers within argument lists."""
return any(p.p_type.endswith("*") for p in self.arglist)
diff --git a/interfaces/sourcegen/sourcegen/yaml/_YamlSourceGenerator.py b/interfaces/sourcegen/sourcegen/yaml/_YamlSourceGenerator.py
new file mode 100644
index 0000000000..ab0aa981da
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/yaml/_YamlSourceGenerator.py
@@ -0,0 +1,66 @@
+"""
+Generator for YAML output.
+
+Used for illustration purposes: the `CLibSourceGenerator` is used to preprocess YAML
+header specifications, which yields the `HeaderFile` objects used by this source
+generator.
+"""
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+from pathlib import Path
+import logging
+
+from jinja2 import Environment, BaseLoader
+
+from .._dataclasses import HeaderFile, CFunc
+from .._SourceGenerator import SourceGenerator
+
+
+_LOGGER = logging.getLogger()
+
+class YamlSourceGenerator(SourceGenerator):
+ """The SourceGenerator for generating CLib."""
+
+ def __init__(self, out_dir: str, config: dict, templates: dict) -> None:
+ self._out_dir = out_dir or None
+ if self._out_dir is not None:
+ self._out_dir = Path(out_dir)
+ self._out_dir.mkdir(parents=True, exist_ok=True)
+ self._config = config
+ self._templates = templates
+
+ def _write_yaml(self, headers: HeaderFile) -> None:
+ """Parse header file and generate YAML output."""
+ loader = Environment(loader=BaseLoader, trim_blocks=True, lstrip_blocks=True)
+
+ definition = loader.from_string(self._templates["yaml-definition"])
+ declarations = []
+ for c_func, recipe in zip(headers.funcs, headers.recipes):
+ msg = f" scaffolding {c_func.name!r} implementation"
+ _LOGGER.debug(msg)
+ implements = ""
+ if isinstance(c_func.implements, CFunc):
+ implements = c_func.implements.short_declaration()
+ declarations.append(
+ definition.render(c_func=c_func,
+ returns=c_func.returns, implements=implements,
+ relates=c_func.uses, what=recipe.what))
+
+ filename = headers.output_name(suffix=".yaml", auto="3")
+ template = loader.from_string(self._templates["yaml-file"])
+ output = template.render(filename=filename.name, header_entries=declarations)
+
+ out = Path(self._out_dir) / "yaml" / filename.name
+ msg = f" writing {filename.name!r}"
+ _LOGGER.info(msg)
+ out.parent.mkdir(parents=True, exist_ok=True)
+ out.write_text(output + "\n")
+
+ def generate_source(self, headers_files: list[HeaderFile]) -> None:
+ """Generate output."""
+ for headers in headers_files:
+ msg = f" parsing functions in {headers.path.name!r}"
+ _LOGGER.info(msg)
+ self._write_yaml(headers)
diff --git a/interfaces/sourcegen/sourcegen/yaml/__init__.py b/interfaces/sourcegen/sourcegen/yaml/__init__.py
new file mode 100644
index 0000000000..b3e39bec8c
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/yaml/__init__.py
@@ -0,0 +1,4 @@
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+from ._YamlSourceGenerator import YamlSourceGenerator
diff --git a/interfaces/sourcegen/sourcegen/yaml/config.yaml b/interfaces/sourcegen/sourcegen/yaml/config.yaml
new file mode 100644
index 0000000000..73adc9bbf5
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/yaml/config.yaml
@@ -0,0 +1,10 @@
+# Configuration for YAML output.
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+# Ignore these files entirely:
+ignore_files: []
+
+# Ignore these specific functions:
+ignore_funcs: {}
diff --git a/interfaces/sourcegen/sourcegen/yaml/templates.yaml b/interfaces/sourcegen/sourcegen/yaml/templates.yaml
new file mode 100644
index 0000000000..626690e6ce
--- /dev/null
+++ b/interfaces/sourcegen/sourcegen/yaml/templates.yaml
@@ -0,0 +1,35 @@
+# Definitions used for Jinja template replacement.
+
+# This file is part of Cantera. See License.txt in the top-level directory or
+# at https://cantera.org/license.txt for license and copyright information.
+
+yaml-definition: |-
+ {{ c_func.name }}:
+ description: {{ c_func.brief }}
+ what: {{ what }}
+ {% if c_func.arglist %}
+ parameters:
+ {% for par in c_func.arglist %}
+ {{ par.name }}: {{ par.description }}
+ {% endfor %}
+ {% endif %}
+ declaration: {{ c_func.declaration() }}
+ {% if c_func.returns %}
+ {{ 'returns: ' + c_func.returns }}
+ {% endif %}
+
+yaml-file: |-
+ # @file {{ filename }}
+ #
+ # This file was generated by sourcegen. It will be re-generated by the
+ # Cantera build process. Do not manually edit.
+ #
+ # @warning This module is an experimental part of the Cantera API and
+ # may be changed or removed without notice.
+
+ # This file is part of Cantera. See License.txt in the top-level directory or
+ # at https://cantera.org/license.txt for license and copyright information.
+
+ {% for header_entry in header_entries %}
+ {{ header_entry }}
+ {% endfor %}
diff --git a/src/SConscript b/src/SConscript
index 004180aac3..d958e0a515 100644
--- a/src/SConscript
+++ b/src/SConscript
@@ -39,6 +39,8 @@ libs = [('base', ['cpp'], baseSetup),
('zeroD', ['cpp'], defaultSetup),
('clib', ['cpp'], defaultSetup),
]
+if env['clib_experimental']:
+ libs.append(('clib_experimental', ['cpp'], defaultSetup))
localenv = env.Clone()
localenv.Prepend(CPPPATH=[Dir('#include'), Dir('.')])
diff --git a/src/clib_experimental/.gitignore b/src/clib_experimental/.gitignore
new file mode 100644
index 0000000000..ce1da4c53c
--- /dev/null
+++ b/src/clib_experimental/.gitignore
@@ -0,0 +1 @@
+*.cpp
diff --git a/test/SConscript b/test/SConscript
index 823ccc13a9..91709ce9d5 100644
--- a/test/SConscript
+++ b/test/SConscript
@@ -209,6 +209,8 @@ def addPythonTest(testname, subset):
# Instantiate tests
addTestProgram('clib', 'clib')
+if localenv['clib_experimental']:
+ addTestProgram('clib_experimental', 'clib-experimental')
addTestProgram('equil', 'equil')
addTestProgram('general', 'general')
addTestProgram('kinetics', 'kinetics')
diff --git a/test/clib_experimental/test_clib3.cpp b/test/clib_experimental/test_clib3.cpp
new file mode 100644
index 0000000000..5cb1672799
--- /dev/null
+++ b/test/clib_experimental/test_clib3.cpp
@@ -0,0 +1,304 @@
+#include
+#include
+#include
+
+#include "cantera/core.h"
+#include "cantera/clib/clib_defs.h"
+#include "cantera/clib_experimental/ct3.h"
+#include "cantera/clib_experimental/ctsol3.h"
+#include "cantera/clib_experimental/ctthermo3.h"
+#include "cantera/clib_experimental/ctkin3.h"
+#include "cantera/clib_experimental/cttrans3.h"
+
+using namespace Cantera;
+using ::testing::HasSubstr;
+
+string reportError()
+{
+ vector output_buf;
+ int buflen = ct3_getCanteraError(0, output_buf.data());
+ output_buf.resize(buflen);
+ ct3_getCanteraError(buflen, output_buf.data());
+ return string(output_buf.data());
+}
+
+TEST(ct3, cabinet_exceptions)
+{
+ sol3_newSolution("h2o2.yaml", "ohmech", "default");
+ sol3_name(999, 0, 0);
+
+ string err = reportError();
+ EXPECT_THAT(err, HasSubstr("Index 999 out of range."));
+
+ sol3_thermo(998);
+ err = reportError();
+ EXPECT_THAT(err, HasSubstr("Index 998 out of range."));
+
+ int ret = sol3_del(997);
+ ASSERT_EQ(ret, -1);
+ err = reportError();
+ EXPECT_THAT(err, HasSubstr("Index 997 out of range."));
+
+ int ref = sol3_newSolution("h2o2.yaml", "ohmech", "default");
+ sol3_del(ref);
+ int thermo = sol3_thermo(ref);
+ EXPECT_EQ(thermo, -2);
+ err = reportError();
+ EXPECT_THAT(err, HasSubstr("has been deleted."));
+
+ ct3_resetStorage();
+ ret = sol3_del(0);
+ ASSERT_EQ(ret, -1);
+ err = reportError();
+ EXPECT_THAT(err, HasSubstr("Index 0 out of range."));
+}
+
+TEST(ct3, new_solution)
+{
+ ct3_resetStorage();
+
+ string name = "ohmech";
+ int ref = sol3_newSolution("h2o2.yaml", name.c_str(), "default");
+ ASSERT_EQ(ref, 0);
+
+ ASSERT_EQ(sol3_cabinetSize(), 1);
+ ASSERT_EQ(thermo3_cabinetSize(), 1);
+ ASSERT_EQ(kin3_cabinetSize(), 1);
+
+ int buflen = sol3_name(ref, 0, 0); // includes \0
+ ASSERT_EQ(buflen, int(name.size() + 1));
+
+ int thermo = sol3_thermo(ref);
+ ASSERT_EQ(thermo3_parentHandle(thermo), ref);
+
+ vector buf(buflen);
+ sol3_name(ref, buflen, buf.data());
+ string solName(buf.data());
+ ASSERT_EQ(solName, name);
+}
+
+TEST(ct3, sol3_objects)
+{
+ ct3_resetStorage();
+
+ int ref = sol3_newSolution("gri30.yaml", "gri30", "none");
+ ASSERT_EQ(ref, 0);
+ ASSERT_EQ(thermo3_cabinetSize(), 1); // one ThermoPhase object
+
+ int ref2 = sol3_newSolution("h2o2.yaml", "ohmech", "default");
+ ASSERT_EQ(ref2, 1);
+ ASSERT_EQ(thermo3_cabinetSize(), 2); // two ThermoPhase objects
+
+ int thermo = sol3_thermo(ref);
+ ASSERT_EQ(thermo3_parentHandle(thermo), ref);
+
+ int thermo2 = sol3_thermo(ref2);
+ ASSERT_EQ(thermo2, 1); // references stored object with index '1'
+ ASSERT_EQ(thermo3_nSpecies(thermo2), 10u);
+ ASSERT_EQ(thermo3_parentHandle(thermo2), ref2);
+
+ int kin = sol3_kinetics(ref);
+
+ int kin2 = sol3_kinetics(ref2);
+ ASSERT_EQ(kin2, 1);
+ ASSERT_EQ(kin3_nReactions(kin2), 29u);
+ ASSERT_EQ(kin3_parentHandle(kin2), ref2);
+ ASSERT_EQ(kin3_parentHandle(kin), ref);
+
+ int trans = sol3_transport(ref);
+ ASSERT_EQ(trans3_parentHandle(trans), ref);
+
+ int trans2 = sol3_transport(ref2);
+ ASSERT_EQ(trans2, 1);
+ int buflen = trans3_transportModel(trans2, 0, 0);
+ vector buf(buflen);
+ trans3_transportModel(trans2, buflen, buf.data());
+ string trName(buf.data());
+ ASSERT_EQ(trName, "mixture-averaged");
+ ASSERT_EQ(trans3_parentHandle(trans2), ref2);
+
+ sol3_del(ref2);
+ int nsp = thermo3_nSpecies(thermo2);
+ ASSERT_EQ(nsp, ERR);
+ string err = reportError();
+ EXPECT_THAT(err, HasSubstr("has been deleted."));
+
+ nsp = thermo3_nSpecies(thermo2);
+ ASSERT_EQ(nsp, ERR);
+ err = reportError();
+ EXPECT_THAT(err, HasSubstr("has been deleted."));
+
+ trans2 = sol3_setTransportModel(ref, "mixture-averaged");
+ ASSERT_EQ(trans2, 2);
+ buflen = trans3_transportModel(trans2, 0, 0);
+ buf.resize(buflen);
+ trans3_transportModel(trans2, buflen, buf.data());
+ trName = buf.data();
+ ASSERT_EQ(trName, "mixture-averaged");
+}
+
+TEST(ct3, new_interface)
+{
+ ct3_resetStorage();
+
+ int sol = sol3_newSolution("ptcombust.yaml", "gas", "none");
+ ASSERT_EQ(sol, 0);
+
+ vector adj{sol};
+ int surf = sol3_newInterface("ptcombust.yaml", "Pt_surf", 1, adj.data());
+ ASSERT_EQ(surf, 1);
+
+ int ph_surf = sol3_thermo(surf);
+ int buflen = sol3_name(ph_surf, 0, 0) + 1; // include \0
+ vector buf(buflen);
+ sol3_name(ph_surf, buflen, buf.data());
+ string solName(buf.data());
+ ASSERT_EQ(solName, "Pt_surf");
+
+ int kin3_surf = sol3_kinetics(surf);
+ buflen = kin3_kineticsType(kin3_surf, 0, 0) + 1; // include \0
+ buf.resize(buflen);
+ kin3_kineticsType(ph_surf, buflen, buf.data());
+ string kinType(buf.data());
+ ASSERT_EQ(kinType, "surface");
+}
+
+TEST(ct3, new_interface_auto)
+{
+ ct3_resetStorage();
+
+ vector adj;
+ int surf = sol3_newInterface("ptcombust.yaml", "Pt_surf", 0, adj.data());
+ ASSERT_EQ(surf, 0);
+
+ ASSERT_EQ(sol3_nAdjacent(surf), 1u);
+ int gas = sol3_adjacent(surf, 0);
+ ASSERT_EQ(gas, 1);
+
+ int buflen = sol3_name(gas, 0, 0) + 1; // include \0
+ vector buf(buflen);
+ sol3_name(gas, buflen, buf.data());
+ string solName(buf.data());
+ ASSERT_EQ(solName, "gas");
+}
+
+TEST(ct3, thermo)
+{
+ int ret;
+ int sol = sol3_newSolution("gri30.yaml", "gri30", "none");
+ int thermo = sol3_thermo(sol);
+ ASSERT_GE(thermo, 0);
+ size_t nsp = thermo3_nSpecies(thermo);
+ ASSERT_EQ(nsp, 53u);
+
+ ret = thermo3_setTemperature(thermo, 500);
+ ASSERT_EQ(ret, 0);
+ ret = thermo3_setPressure(thermo, 5 * 101325);
+ ASSERT_EQ(ret, 0);
+ ret = thermo3_setMoleFractionsByName(thermo, "CH4:1.0, O2:2.0, N2:7.52");
+ ASSERT_EQ(ret, 0);
+
+ ret = thermo3_equilibrate(thermo, "HP", "auto", 1e-9, 50000, 1000, 0);
+ ASSERT_EQ(ret, 0);
+ double T = thermo3_temperature(thermo);
+ ASSERT_GT(T, 2200);
+ ASSERT_LT(T, 2300);
+
+ size_t ns = thermo3_nSpecies(thermo);
+ vector work(ns);
+ vector X(ns);
+ thermo3_getMoleFractions(thermo, ns, X.data());
+
+ thermo3_getPartialMolarEnthalpies(thermo, ns, work.data());
+ double prod = std::inner_product(X.begin(), X.end(), work.begin(), 0.0);
+ ASSERT_NEAR(prod, thermo3_enthalpy_mole(thermo), 1e-6);
+
+ thermo3_getPartialMolarEntropies(thermo, ns, work.data());
+ prod = std::inner_product(X.begin(), X.end(), work.begin(), 0.0);
+ ASSERT_NEAR(prod, thermo3_entropy_mole(thermo), 1e-6);
+
+ thermo3_getPartialMolarIntEnergies(thermo, ns, work.data());
+ prod = std::inner_product(X.begin(), X.end(), work.begin(), 0.0);
+ ASSERT_NEAR(prod, thermo3_intEnergy_mole(thermo), 1e-6);
+
+ thermo3_getPartialMolarCp(thermo, ns, work.data());
+ prod = std::inner_product(X.begin(), X.end(), work.begin(), 0.0);
+ ASSERT_NEAR(prod, thermo3_cp_mole(thermo), 1e-6);
+
+ thermo3_getPartialMolarVolumes(thermo, ns, work.data());
+ prod = std::inner_product(X.begin(), X.end(), work.begin(), 0.0);
+ ASSERT_NEAR(prod, 1./thermo3_molarDensity(thermo), 1e-6);
+}
+
+TEST(ct3, kinetics)
+{
+ int sol0 = sol3_newSolution("gri30.yaml", "gri30", "none");
+ int thermo = sol3_thermo(sol0);
+ int kin = sol3_kinetics(sol0);
+ ASSERT_GE(kin, 0);
+
+ size_t nr = kin3_nReactions(kin);
+ ASSERT_EQ(nr, 325u);
+
+ thermo3_equilibrate(thermo, "HP", "auto", 1e-9, 50000, 1000, 0);
+ double T = thermo3_temperature(thermo);
+ thermo3_setTemperature(thermo, T - 200);
+
+ auto sol = newSolution("gri30.yaml", "gri30", "none");
+ auto phase = sol->thermo();
+ auto kinetics = sol->kinetics();
+
+ phase->equilibrate("HP");
+ ASSERT_NEAR(T, phase->temperature(), 1e-2);
+ phase->setTemperature(T - 200);
+
+ vector c_ropf(nr);
+ kin3_getFwdRatesOfProgress(kin, 325, c_ropf.data());
+ vector cpp_ropf(nr);
+ kinetics->getFwdRatesOfProgress(cpp_ropf.data());
+
+ for (size_t n = 0; n < nr; n++) {
+ ASSERT_NEAR(cpp_ropf[n], c_ropf[n], 1e-6);
+ }
+}
+
+TEST(ct3, transport)
+{
+ int sol0 = sol3_newSolution("gri30.yaml", "gri30", "default");
+ int thermo = sol3_thermo(sol0);
+ int tran = sol3_transport(sol0);
+
+ size_t nsp = thermo3_nSpecies(thermo);
+ vector c_dkm(nsp);
+ int ret = trans3_getMixDiffCoeffs(tran, 53, c_dkm.data());
+ ASSERT_EQ(ret, 0);
+
+ vector cpp_dkm(nsp);
+ auto sol = newSolution("gri30.yaml", "gri30");
+ auto transport = sol->transport();
+ transport->getMixDiffCoeffs(cpp_dkm.data());
+
+ for (size_t n = 0; n < nsp; n++) {
+ ASSERT_NEAR(cpp_dkm[n], c_dkm[n], 1e-10);
+ }
+}
+
+
+int main(int argc, char** argv)
+{
+ printf("Running main() from test_clib3.cpp\n");
+ testing::InitGoogleTest(&argc, argv);
+ make_deprecation_warnings_fatal();
+ printStackTraceOnSegfault();
+ Cantera::CanteraError::setStackTraceDepth(20);
+ /* commented code below is relevant to future PRs */
+ // vector fileNames = {"gtest-freeflame.yaml", "gtest-freeflame.h5"};
+ // for (const auto& fileName : fileNames) {
+ // if (std::ifstream(fileName).good()) {
+ // std::remove(fileName.c_str());
+ // }
+ // }
+ int result = RUN_ALL_TESTS();
+ appdelete();
+ return result;
+}
diff --git a/test/clib_experimental/test_ctfunc3.cpp b/test/clib_experimental/test_ctfunc3.cpp
new file mode 100644
index 0000000000..f46cd56309
--- /dev/null
+++ b/test/clib_experimental/test_ctfunc3.cpp
@@ -0,0 +1,305 @@
+#include
+#include
+#include
+
+#include "cantera/core.h"
+#include "cantera/clib_experimental/ctfunc3.h"
+#include "cantera/numerics/Func1Factory.h"
+
+using namespace Cantera;
+
+
+TEST(ctfunc3, invalid)
+{
+ // exceptions return -1
+ ASSERT_EQ(func13_newBasic("spam", 0.), -2);
+ ASSERT_EQ(func13_newAdvanced("eggs", 0, NULL), -2);
+}
+
+TEST(ctfunc3, sin)
+{
+ double omega = 2.1;
+ int fcn = func13_newBasic("sin", omega);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), sin(omega * 0.5));
+
+ int dfcn = func13_newDerivative(fcn);
+ EXPECT_DOUBLE_EQ(func13_eval(dfcn, 0.5), omega * cos(omega * 0.5));
+
+ int buflen = func13_write(fcn, "x", 0, 0);
+ vector buf(buflen);
+ func13_write(fcn, "x", buflen, buf.data());
+ string rep(buf.data());
+ ASSERT_EQ(rep, "\\sin(2.1x)");
+}
+
+TEST(ctfunc3, cos)
+{
+ double omega = 2.;
+ int fcn = func13_newBasic("cos", omega);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), cos(omega * 0.5));
+
+ int dfcn = func13_newDerivative(fcn);
+ EXPECT_DOUBLE_EQ(func13_eval(dfcn, 0.5), -omega * sin(omega * 0.5));
+}
+
+TEST(ctfunc3, exp)
+{
+ double omega = 2.;
+ int fcn = func13_newBasic("exp", omega);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), exp(omega * 0.5));
+
+ int dfcn = func13_newDerivative(fcn);
+ EXPECT_DOUBLE_EQ(func13_eval(dfcn, 0.5), omega * exp(omega * 0.5));
+}
+
+TEST(ctfunc3, log)
+{
+ double omega = 2.;
+ int fcn = func13_newBasic("log", omega);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 1. / omega), 0.);
+
+ int dfcn = func13_newDerivative(fcn);
+ EXPECT_DOUBLE_EQ(func13_eval(dfcn, .5), omega / .5);
+}
+
+TEST(ctfunc3, pow)
+{
+ double exp = .5;
+ int fcn = func13_newBasic("pow", exp);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), pow(0.5, exp));
+
+ int dfcn = func13_newDerivative(fcn);
+ EXPECT_DOUBLE_EQ(func13_eval(dfcn, 0.5), exp * pow(0.5, exp - 1));
+}
+
+TEST(ctfunc3, constant)
+{
+ double a = .5;
+ int fcn = func13_newBasic("constant", a);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), a);
+
+ int dfcn = func13_newDerivative(fcn);
+ EXPECT_DOUBLE_EQ(func13_eval(dfcn, .5), 0.);
+}
+
+TEST(ctfunc3, tabulated_linear)
+{
+ vector params = {0., 1., 2., 1., 0., 1.};
+
+ int fcn = func13_newAdvanced("tabulated-linear", params.size(), params.data());
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), 0.5);
+
+ // exceptions return -1
+ EXPECT_EQ(func13_newAdvanced("tabulated-linear", 5, params.data()), -2);
+}
+
+TEST(ctfunc3, tabulated_previous)
+{
+ vector params = {0., 1., 2., 1., 0., 1.};
+
+ int fcn = func13_newAdvanced("tabulated-previous", params.size(), params.data());
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), 1.);
+}
+
+TEST(ctfunc3, poly)
+{
+ double a0 = .5;
+ double a1 = .25;
+ double a2 = .125;
+ vector params = {a2, a1, a0};
+ int fcn = func13_newAdvanced("polynomial3", params.size(), params.data());
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, .5), (a2 * .5 + a1) * .5 + a0);
+
+ params = {1, 0, -2.2, 3.1};
+ fcn = func13_newAdvanced("polynomial3", params.size(), params.data());
+ int buflen = func13_write(fcn, "x", 0, 0);
+ vector buf(buflen);
+ func13_write(fcn, "x", buflen, buf.data());
+ string rep(buf.data());
+ ASSERT_EQ(rep, "x^3 - 2.2x + 3.1");
+}
+
+TEST(ctfunc3, Fourier)
+{
+ double a0 = .5;
+ double a1 = .25;
+ double b1 = .125;
+ double omega = 2.;
+ vector params = {a0, a1, omega, b1};
+ int fcn = func13_newAdvanced("Fourier", params.size(), params.data());
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(
+ func13_eval(fcn, .5), .5 * a0 + a1 * cos(omega * .5) + b1 * sin(omega * .5));
+}
+
+TEST(ctfunc3, Gaussian)
+{
+ double A = .5;
+ double t0 = .6;
+ double fwhm = .25;
+ vector params = {A, t0, fwhm};
+ int fcn = func13_newAdvanced("Gaussian", params.size(), params.data());
+ ASSERT_GE(fcn, 0);
+ double tau = fwhm / (2. * sqrt(log(2.)));
+ double x = - t0 / tau;
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.), A * exp(-x * x));
+
+ // exceptions return -1
+ EXPECT_EQ(func13_newAdvanced("Gaussian", 2, params.data()), -2);
+}
+
+TEST(ctfunc3, Arrhenius)
+{
+ double A = 38.7;
+ double b = 2.7;
+ double E = 2.619184e+07 / GasConstant;
+ vector params = {A, b, E};
+ int fcn = func13_newAdvanced("Arrhenius", params.size(), params.data());
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 1000.), A * pow(1000., b) * exp(-E/1000.));
+}
+
+TEST(ctmath, invalid)
+{
+ // exceptions return -1
+ int fcn0 = func13_newBasic("sin", 1.);
+ int fcn1 = func13_newBasic("cos", 1.);
+ ASSERT_EQ(func13_newCompound("foo", fcn0, fcn1), -2);
+ ASSERT_EQ(func13_newModified("bar", fcn0, 0.), -2);
+}
+
+TEST(ctmath, sum)
+{
+ double omega = 2.;
+ int fcn0 = func13_newBasic("sin", omega);
+ int fcn1 = func13_newBasic("cos", omega);
+ int fcn = func13_newCompound("sum", fcn0, fcn1);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), sin(omega * 0.5) + cos(omega * 0.5));
+ int fcn2 = func13_newSum(fcn0, fcn1);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), func13_eval(fcn2, 0.5));
+}
+
+TEST(ctmath, diff)
+{
+ double omega = 2.;
+ int fcn0 = func13_newBasic("sin", omega);
+ int fcn1 = func13_newBasic("cos", omega);
+ int fcn = func13_newCompound("diff", fcn0, fcn1);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), sin(omega * 0.5) - cos(omega * 0.5));
+ int fcn2 = func13_newDiff(fcn0, fcn1);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), func13_eval(fcn2, 0.5));
+}
+
+TEST(ctmath, prod)
+{
+ double omega = 2.;
+ int fcn0 = func13_newBasic("sin", omega);
+ int fcn1 = func13_newBasic("cos", omega);
+ int fcn = func13_newCompound("product", fcn0, fcn1);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), sin(omega * 0.5) * cos(omega * 0.5));
+ int fcn2 = func13_newProd(fcn0, fcn1);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), func13_eval(fcn2, 0.5));
+}
+
+TEST(ctmath, ratio)
+{
+ double omega = 2.;
+ int fcn0 = func13_newBasic("sin", omega);
+ int fcn1 = func13_newBasic("cos", omega);
+ int fcn = func13_newCompound("ratio", fcn0, fcn1);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), sin(omega * 0.5) / cos(omega * 0.5));
+ int fcn2 = func13_newRatio(fcn0, fcn1);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), func13_eval(fcn2, 0.5));
+}
+
+TEST(ctmath, composite)
+{
+ double omega = 2.;
+ int fcn0 = func13_newBasic("sin", omega);
+ int fcn1 = func13_newBasic("cos", omega);
+ int fcn = func13_newCompound("composite", fcn0, fcn1);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), sin(omega * cos(omega * 0.5)));
+}
+
+TEST(ctmath, times_constant)
+{
+ double omega = 2.;
+ int fcn0 = func13_newBasic("sin", omega);
+ double A = 1.234;
+ int fcn = func13_newModified("times-constant", fcn0, A);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), sin(omega * 0.5) * A);
+}
+
+TEST(ctmath, plus_constant)
+{
+ double omega = 2.;
+ int fcn0 = func13_newBasic("sin", omega);
+ double A = 1.234;
+ int fcn = func13_newModified("plus-constant", fcn0, A);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), sin(omega * 0.5) + A);
+}
+
+TEST(ctmath, periodic)
+{
+ double omega = 2.;
+ int fcn0 = func13_newBasic("sin", omega);
+ double A = 1.234;
+ int fcn = func13_newModified("periodic", fcn0, A);
+ ASSERT_GE(fcn, 0);
+ EXPECT_DOUBLE_EQ(func13_eval(fcn, 0.5), func13_eval(fcn, 0.5 + A));
+}
+
+TEST(ctmath, generic)
+{
+ // Composite function reported in issue #752
+
+ vector params = {0., 0., 1., 1.};
+ int fs = func13_newAdvanced("Fourier", params.size(), params.data()); // sin(x)
+ auto func13_s = newFunc1("sin", 1.);
+ EXPECT_DOUBLE_EQ(func13_eval(fs, 0.5), func13_s->eval(0.5));
+
+ int fs2 = func13_newCompound("product", fs, fs); // (sin(x)^2)
+ auto func13_s2 = newFunc1("product", func13_s, func13_s);
+ EXPECT_DOUBLE_EQ(func13_eval(fs2, 0.5), func13_s2->eval(0.5));
+
+ double one = 1.;
+ int fs1 = func13_newAdvanced("polynomial3", 1, &one); // 1.
+ auto func13_s1 = newFunc1("constant", 1.);
+ EXPECT_DOUBLE_EQ(func13_eval(fs1, 0.5), func13_s1->eval(0.5));
+ EXPECT_DOUBLE_EQ(func13_eval(fs1, 0.5), 1.);
+
+ int fs2_1 = func13_newCompound("diff", fs1, fs2); // 1-(sin(x))^2
+ auto func13_s2_1 = newFunc1("diff", func13_s1, func13_s2);
+ EXPECT_DOUBLE_EQ(func13_eval(fs2_1, 0.5), func13_s2_1->eval(0.5));
+
+ vector p_arr = {1., .5, 0.};
+ int fs3 = func13_newAdvanced("Arrhenius", 3, p_arr.data()); // sqrt function
+ auto func13_s3 = newFunc1("Arrhenius", p_arr);
+ EXPECT_DOUBLE_EQ(func13_eval(fs3, 0.5), func13_s3->eval(0.5));
+
+ // overall composite function
+ int fs4 = func13_newCompound("composite", fs3, fs2_1); // sqrt(1-(sin(x))^2)
+ auto func13_s4 = newFunc1("composite", func13_s3, func13_s2_1);
+ EXPECT_DOUBLE_EQ(func13_eval(fs4, 0.5), func13_s4->eval(0.5));
+
+ // an easier equivalent expression (using trigonometry)
+ auto func13_s5 = newFunc1("cos", 1.); // missing the absolute value
+ EXPECT_DOUBLE_EQ(func13_eval(fs4, 0.5), func13_s5->eval(0.5));
+ EXPECT_DOUBLE_EQ(func13_eval(fs4, 3.5), -func13_s5->eval(3.5));
+}