This commit is contained in:
Ingmar Schoegl 2025-02-15 02:16:44 +00:00 committed by GitHub
commit 1d1e6745dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 684 additions and 175 deletions

View File

@ -32,6 +32,8 @@
enum LogLevel { INFO, WARN , ERROR };
//! Represents a callback that is invoked to produce log output.
//! TODO: Only needed in the main CLib library. Should be moved once the
//! traditional CLib is removed.
typedef void
(*LogCallback)(enum LogLevel logLevel, const char* category, const char* message);

View File

@ -19,6 +19,7 @@ enum flow_t { NetFlow, OneWayFlow };
// forward references
class Path;
class ReactionPathBuilder;
/**
* Nodes in reaction path graphs.
@ -161,6 +162,15 @@ class ReactionPathDiagram
public:
ReactionPathDiagram() = default;
//! Construct new reaction path diagram.
/**
* The method creates a reaction path diagram for the fluxes of `element`
* according to instantaneous net reaction rates.
* @param kin Shared pointer to Kinetics object.
* @param element_ Element used for the calculation of net reaction rates.
*/
ReactionPathDiagram(shared_ptr<Kinetics> kin, const string& element_);
/**
* Destructor. Deletes all nodes and paths in the diagram.
*/
@ -188,6 +198,12 @@ public:
void writeData(std::ostream& s);
//! Get a (roughly) human-readable representation of the reaction path diagram.
/**
* @see writeData
*/
string getData();
/**
* Export the reaction path diagram. This method writes to stream
* @c s the commands for the 'dot' program in the @c GraphViz
@ -202,7 +218,19 @@ public:
*/
void exportToDot(std::ostream& s);
//! Export string in @c dot format.
/**
* Return a string containing the reaction path diagram formatted for use
* by Graphviz's 'dot' program.
* @see exportToDot
*/
string getDot();
void add(ReactionPathDiagram& d);
//! Add fluxes from other ReactionPathDiagram to this diagram.
void add(shared_ptr<ReactionPathDiagram> d);
SpeciesNode* node(size_t k) {
return m_nodes[k];
}
@ -221,6 +249,8 @@ public:
void addNode(size_t k, const string& nm, double x = 0.0);
//! Include only species and fluxes that are directly connected to a species.
//! Set to -1 to include all species.
void displayOnly(size_t k=npos) {
m_local = k;
}
@ -251,31 +281,63 @@ public:
}
vector<size_t> species();
vector<int> reactions();
//! Undocumented.
/**
* @todo Add documentation.
*/
void findMajorPaths(double threshold, size_t lda, double* a);
//! Set name of the font used.
void setFont(const string& font) {
m_font = font;
}
// public attributes
string title;
string bold_color = "blue";
string normal_color = "steelblue";
string dashed_color = "gray";
string element;
string m_font = "Helvetica";
//! Get the way flows are drawn. Either 'NetFlow' or 'OneWayFlow'
const string flowType() const;
//! Get the way flows are drawn. Either 'NetFlow' or 'OneWayFlow'
void setFlowType(const string& fType);
//! Build the reaction path diagram.
/**
* Called automatically by methods which return representations of the diagram,
* for example writeDot().
*/
void build();
//! Get logging messages generated while building the reaction path diagram.
string getLog();
//! @name Public Attributes
//! @{
string title; //!< Reaction path diagram title.
string bold_color = "blue"; //!< Color for bold lines.
string normal_color = "steelblue"; //!< Color for normal-weight lines.
string dashed_color = "gray"; //!< Color for dashed lines.
string element; //!< Element used for the construction of a reaction path diagram.
string m_font = "Helvetica"; //!< Reaction path diagram font.
//! Threshold for the minimum flux relative value that will be plotted.
double threshold = 0.005;
double bold_min = 0.2;
double dashed_max = 0.0;
double label_min = 0.0;
double x_size = -1.0;
double y_size = -1.0;
string name = "reaction_paths";
string dot_options = "center=1;";
double bold_min = 0.2; //!< Minimum relative flux for bold lines.
double dashed_max = 0.0; //!< Maximum relative flux for dashed lines.
double label_min = 0.0; //!< Minimum relative flux for labels.
double x_size = -1.0; //!< Maximum size (x-dimension).
double y_size = -1.0; //!< Maximum size (y-dimension).
string name = "reaction_paths"; //!< Name used for dot export.
string dot_options = "center=1;"; //!< Options for the 'dot' program.
//! The way flows are drawn. Either 'NetFlow' or 'OneWayFlow'
flow_t flow_type = NetFlow;
double scale = -1;
//! The scaling factor for the fluxes.
//! Set to -1 to normalize by the maximum net flux.
double scale = -1; //!< Scale to use for normalization.
//! The arrow width. If < 0, then scale with flux value.
double arrow_width = -5.0;
bool show_details = false;
double arrow_hue = 0.6666;
bool show_details = false; //!< Boolean flag to show details.
double arrow_hue = 0.6666; //!< Unused.
//! @}
protected:
double m_flxmax = 0.0;
@ -291,6 +353,11 @@ protected:
//! Indices of reactions that are included in the diagram
set<size_t> m_rxns;
size_t m_local = npos;
bool m_isBuilt = false; //!< Boolean indicating whether diagram is built.
shared_ptr<Kinetics> m_kin; //!< Kinetics used by ReactionPathBuilder
shared_ptr<ReactionPathBuilder> m_builder; //!< Shared pointer to ReactionPathBuilder
std::stringstream m_log; //!< Logging stream.
};
@ -334,6 +401,18 @@ protected:
map<string, size_t> m_enamemap;
};
//! Create a new reaction path diagram.
/**
* Returns a shared ReactionPath instance where the fluxes of `element`
* are calculated according to instantaneous net reaction rates.
* @param kin Shared pointer to Kinetics object.
* @param element Element used for the calculation of net reaction rates.
* @return shared_ptr<ReactionPathDiagram>
*/
shared_ptr<ReactionPathDiagram> newReactionPathDiagram(
shared_ptr<Kinetics> kin, const string& element);
}
#endif

View File

@ -4,18 +4,11 @@
#cython: language_level=3
#distutils: language = c++
from .ctcxx cimport *
from .kinetics cimport *
cdef extern from "<sstream>":
cdef cppclass CxxStringStream "std::stringstream":
string str()
cdef extern from "cantera/kinetics/ReactionPath.h":
cdef enum CxxFlow_t "flow_t":
CxxNetFlow "Cantera::NetFlow"
CxxOneWayFlow "Cantera::OneWayFlow"
cdef shared_ptr[CxxReactionPathDiagram] CxxNewReactionPathDiagram "Cantera::newReactionPathDiagram"(
shared_ptr[CxxKinetics], string) except +translate_exception
cdef cppclass CxxReactionPathDiagram "Cantera::ReactionPathDiagram":
cbool show_details
@ -29,24 +22,20 @@ cdef extern from "cantera/kinetics/ReactionPath.h":
double label_min
double scale
double arrow_width
CxxFlow_t flow_type
string title
void setFont(string)
string flowType()
void setFlowType(string) except +translate_exception
string m_font
void add(CxxReactionPathDiagram&) except +translate_exception
void exportToDot(CxxStringStream&)
void writeData(CxxStringStream&)
void add(shared_ptr[CxxReactionPathDiagram]) except +translate_exception
void displayOnly(size_t)
cdef cppclass CxxReactionPathBuilder "Cantera::ReactionPathBuilder":
void init(CxxStringStream&, CxxKinetics&) except +translate_exception
void build(CxxKinetics&, string&, CxxStringStream&, CxxReactionPathDiagram&, cbool)
void build() except +translate_exception
string getDot() except +translate_exception
string getData() except +translate_exception
string getLog() except +translate_exception
cdef class ReactionPathDiagram:
cdef CxxReactionPathDiagram diagram
cdef CxxReactionPathBuilder builder
cdef shared_ptr[CxxReactionPathDiagram] _diagram
cdef CxxReactionPathDiagram* diagram
cdef Kinetics kinetics
cdef str element
cdef pybool built
cdef CxxStringStream* _log

View File

@ -2,32 +2,25 @@
# at https://cantera.org/license.txt for license and copyright information.
from pathlib import Path
from cython.operator cimport dereference as deref
from ._utils cimport *
cdef class ReactionPathDiagram:
def __cinit__(self, *args, **kwargs):
self._log = new CxxStringStream()
def __dealloc__(self):
del self._log
def __init__(self, Kinetics kin, str element):
def __cinit__(self, _SolutionBase contents, str element, *args, **kwargs):
"""
Create a reaction path diagram for the fluxes of the element ``element``
according the the net reaction rates determined by the `Kinetics` object
``kin``.
"""
self.kinetics = kin
self.builder.init(deref(self._log), deref(kin.kinetics))
self.element = element
self.built = False
self.kinetics = contents
cdef shared_ptr[CxxKinetics] cxx_kin = contents.base.kinetics()
self._diagram = CxxNewReactionPathDiagram(cxx_kin, stringify(element))
self.diagram = self._diagram.get()
property show_details:
"""
Get/Set whether to show the details of which reactions contribute to the
flux.
Get/Set whether to show the details of which reactions contribute to the flux.
"""
def __get__(self):
return self.diagram.show_details
@ -36,8 +29,7 @@ cdef class ReactionPathDiagram:
property threshold:
"""
Get/Set the threshold for the minimum flux relative value that will be
plotted.
Get/Set the threshold for the minimum flux relative value that will be plotted.
"""
def __get__(self):
return self.diagram.threshold
@ -113,18 +105,9 @@ cdef class ReactionPathDiagram:
property flow_type:
""" Get/Set the way flows are drawn. Either 'NetFlow' or 'OneWayFlow' """
def __get__(self):
if self.diagram.flow_type == CxxNetFlow:
return 'NetFlow'
else:
return 'OneWayFlow'
return pystr(self.diagram.flowType())
def __set__(self, str value):
if value == 'OneWayFlow':
self.diagram.flow_type = CxxOneWayFlow
elif value == 'NetFlow':
self.diagram.flow_type = CxxNetFlow
else:
raise ValueError('Invalid flow_type: {!r}'.format(value))
self.diagram.setFlowType(stringify(value))
property arrow_width:
""" Get/Set the arrow width. If < 0, then scale with flux value. """
@ -142,7 +125,7 @@ cdef class ReactionPathDiagram:
def add(self, ReactionPathDiagram other):
""" Add fluxes from `other` to this diagram """
self.diagram.add(other.diagram)
self.diagram.add(other._diagram)
def display_only(self, int k):
"""
@ -156,11 +139,7 @@ cdef class ReactionPathDiagram:
Return a string containing the reaction path diagram formatted for use
by Graphviz's 'dot' program.
"""
if not self.built:
self.build()
cdef CxxStringStream out
self.diagram.exportToDot(out)
return pystr(out.str())
return pystr(self.diagram.getDot())
def write_dot(self, filename):
"""
@ -171,24 +150,16 @@ cdef class ReactionPathDiagram:
def get_data(self):
"""
Get a (roughly) human-readable representation of the reaction path
diagram.
Get a (roughly) human-readable representation of the reaction path diagram.
"""
if not self.built:
self.build()
cdef CxxStringStream out
self.diagram.writeData(out)
return pystr(out.str())
return pystr(self.diagram.getData())
def build(self, verbose=False):
"""
Build the reaction path diagram. Called automatically by methods which
return representations of the diagram, for example `write_dot()`.
"""
self.builder.build(deref(self.kinetics.kinetics),
stringify(self.element), deref(self._log),
self.diagram, True)
self.built = True
self.diagram.build()
if verbose:
print(self.log)
@ -197,4 +168,4 @@ cdef class ReactionPathDiagram:
Logging messages generated while building the reaction path diagram
"""
def __get__(self):
return pystr(self._log.str())
return pystr(self.diagram.getLog())

View File

@ -25,14 +25,19 @@ _XML_PATH = _TAG_PATH / "doxygen" / "xml"
@dataclass(frozen=True)
@with_unpack_iter
class TagInfo:
"""Represents information parsed from a doxygen tag file."""
"""
Represents information parsed from a doxygen tag file.
May represent a member function or a variable.
"""
base: str = "" #: Qualified scope (skipping Cantera namespace)
type: str = "" #: Return type
name: str = "" #: Function name
name: str = "" #: Function/variable name
arglist: str = "" #: Function argument list (original XML string)
anchorfile: str = "" #: doxygen anchor file
anchor: str = "" #: doxygen anchor
kind: str = "" #: Member kind
@classmethod
def from_xml(cls: Self, qualified_name: str, xml: str) -> Self:
@ -47,7 +52,8 @@ class TagInfo:
xml_tree.find("name").text,
xml_tree.find("arglist").text,
xml_tree.find("anchorfile").text.replace(".html", ".xml"),
xml_tree.find("anchor").text)
xml_tree.find("anchor").text,
xml_tree.attrib.get("kind", ""))
def __bool__(self) -> bool:
return all([self.type, self.name, self.arglist, self.anchorfile, self.anchor])
@ -154,13 +160,15 @@ class TagFileParser:
# Get known functions from namespace and methods from classes
self._known = xml_members("function", namespace)
self._known.update(xml_members("variable", namespace))
for name, cls in classes.items():
prefix = f"{name}::"
self._known.update(xml_members("function", cls, prefix))
self._known.update(xml_members("variable", cls, prefix))
def exists(self, cxx_func: str) -> bool:
def exists(self, cxx_member: str) -> bool:
"""Check whether doxygen tag exists."""
return cxx_func in self._known
return cxx_member in self._known
def detect(self, name: str, bases: Iterable[str], permissive: bool = True) -> str:
"""Detect qualified method name."""
@ -178,17 +186,17 @@ class TagFileParser:
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."
cxx_member = func_string.split("(")[0].split(" ")[-1]
if cxx_member not in self._known:
msg = f"Could not find {cxx_member!r} in doxygen tag file."
_LOGGER.critical(msg)
sys.exit(1)
ix = 0
if len(self._known[cxx_func]) > 1:
if len(self._known[cxx_member]) > 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]]
for xml in self._known[cxx_member]]
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:
@ -214,13 +222,18 @@ class TagFileParser:
_LOGGER.critical(msg)
sys.exit(1)
return TagInfo.from_xml(cxx_func, self._known[cxx_func][ix])
return TagInfo.from_xml(cxx_member, self._known[cxx_member][ix])
def cxx_func(self, func_string: str) -> CFunc:
"""Generate annotated C++ function specification."""
def cxx_member(self, func_string: str, setter: bool = False) -> CFunc | Param:
"""Generate annotated C++ function/variable specification."""
details = tag_lookup(self.tag_info(func_string))
ret_param = Param.from_xml(details.type)
if details.kind == "variable":
direction = "in" if setter else "out"
return Param(ret_param.p_type, details.name,
details.briefdescription, direction, None, details.base)
# Merge attributes from doxygen signature and doxygen annotations
args = ArgList.from_xml(details.arglist).params # from signature
args_annotated = details.parameterlist # from documentation
@ -250,7 +263,8 @@ def tag_lookup(tag_info: TagInfo) -> TagDetails:
xml_details = xml_file.read_text()
id_ = tag_info.id
regex = re.compile(rf'<memberdef kind="function" id="{id_}"[\s\S]*?</memberdef>')
kind_ = tag_info.kind
regex = re.compile(rf'<memberdef kind="{kind_}" id="{id_}"[\s\S]*?</memberdef>')
matches = re.findall(regex, xml_details)
if not matches:

View File

@ -33,6 +33,10 @@ based on a recipe, which is subsequently used to scaffold API functions using de
- `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.
- `variable-getter`: Implements a getter for a C++ class member variable or a variable
defined in the `Cantera` namespace. Functionality is inferred automatically.
- `variable-setter`: Implements a getter for a C++ class member variable or a variable
defined in the `Cantera` namespace. Field disambiguates from a `variable-getter`.
- `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
@ -44,14 +48,15 @@ Recipes include all information required for the auto-generation of a correspond
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)`.
- `implements`: Optional name or signature of the implemented C++ function/method or
variable. If left empty, *sourcegen* searches for doxygen tags matching the `name`
field. A name is sufficient if C++ functions/methods are unique, for example
`Func1::type`. A 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)`. The scope (part preceding `::`) may be omitted, as
it can be inferred based on the fields `base`, `parents` or `derived`.
- `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.
- `code`: Optional custom code to override auto-generated code (experimental).

View File

@ -12,6 +12,8 @@ recipes:
- name: getGitCommit
implements: gitCommit # inconsistent API (preexisting)
- name: getCanteraError
- name: setLogWriter
- name: setLogCallback
- name: addCanteraDirectory
implements: addDirectory # inconsistent API (preexisting)
- name: getDataDirectories
@ -24,7 +26,21 @@ recipes:
- name: suppress_thermo_warnings # inconsistent API (snake_case; preexisting)
- name: use_legacy_rate_constants # inconsistent API (snake_case; preexisting)
- name: appdelete
- name: Avogadro
- name: Boltzmann
- name: Planck
- name: ElectronCharge
- name: lightSpeed
- name: OneAtm
- name: OneBar
- name: fineStructureConstant
- name: ElectronMass
- name: GasConstant
- name: StefanBoltz
- name: Faraday
- name: permeability0
implements: permeability_0
- name: epsilon0
implements: epsilon_0
- name: clearStorage
- name: resetStorage
- name: setLogWriter
- name: setLogCallback

View File

@ -0,0 +1,85 @@
# 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.
docstring: |-
Auto-generated CLib API for %Cantera's ReactionPathDiagram class.
Implements a replacement for CLib's traditional @c ctrpath library.
prefix: rdiag3
base: ReactionPathDiagram
parents: [] # List of parent classes
derived: [] # List of specializations
recipes:
- name: newReactionPathDiagram # absorbs rbuild_init
- name: showDetails
implements: show_details
- name: setShowDetails # replacement for rdiag_detailed/rdiag_brief
implements: show_details
what: variable-setter
- name: threshold
- name: setThreshold
implements: threshold
what: variable-setter
- name: boldThreshold
implements: bold_min
- name: setBoldThreshold
implements: bold_min
what: variable-setter
- name: normalThreshold
implements: dashed_max
- name: setNormalThreshold
implements: dashed_max
what: variable-setter
- name: labelThreshold
implements: label_min
- name: setLabelThreshold
implements: label_min
what: variable-setter
- name: boldColor
implements: bold_color
- name: setBoldColor
implements: bold_color
what: variable-setter
- name: normalColor
implements: normal_color
- name: setNormalColor
implements: normal_color
what: variable-setter
- name: dashedColor
implements: dashed_color
- name: setDashedColor
implements: dashed_color
what: variable-setter
- name: dotOptions
implements: dot_options
- name: setDotOptions
implements: dot_options
what: variable-setter
- name: font
implements: m_font
- name: setFont
- name: scale
- name: setScale
implements: scale
what: variable-setter
- name: flowType
- name: setFlowType
- name: arrowWidth
implements: arrow_width
- name: setArrowWidth
implements: arrow_width
what: variable-setter
- name: title
- name: setTitle
implements: title
what: variable-setter
- name: add
- name: displayOnly
- name: getDot # previously part of rdiag_write
- name: getData # previously part of rdiag_write
- name: build # previously rbuild_build
- name: getLog # New in Cantera 3.2
- name: findMajor
implements: findMajorPaths
- name: del
- name: cabinetSize
- name: parentHandle

View File

@ -14,7 +14,15 @@ recipes:
- name: type # previously: ctkin_getReactionType
- name: usesThirdBody
- name: valid
# - name: id <--- member variable (access not yet implemented)
- name: id
- name: setId
implements: id
what: variable-setter
- name: allowNonreactantOrders
implements: allow_nonreactant_orders
- name: setAllowNonreactantOrders
implements: allow_nonreactant_orders
what: variable-setter
- name: del
- name: cabinetSize
- name: parentHandle

View File

@ -23,6 +23,7 @@ class Param:
description: str = "" #: Parameter description (optional annotation)
direction: str = "" #: Direction of parameter (optional annotation)
default: Any = None #: Default value (optional)
base: str = "" #: Base (optional). Only used if param represents a member variable
@classmethod
def from_str(cls: Self, param: str, doc: str = "") -> Self:
@ -165,23 +166,28 @@ class CFunc(Func):
if len(lines) == 1:
return cls(*func, brief, None, "", "", [])
returns = ""
args = []
doc_args = {p.name: p for p in func.arglist}
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))
# match parameter name
keys = [k for k in doc_args.keys() if line.split()[1] == k]
if len(keys) == 1:
key = keys[0]
doc_args[key] = Param.from_str(doc_args[key].long_str(), line)
elif line.startswith("@returns"):
returns = line.lstrip("@returns").strip()
args = ArgList(args)
args = ArgList(list(doc_args.values()))
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.arglist is None:
ret = (f"{self.name}").strip()
else:
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}"

View File

@ -164,8 +164,8 @@ class CLibSourceGenerator(SourceGenerator):
sys.exit(1)
return params
@staticmethod
def _reverse_crosswalk(c_func: CFunc, base: str) -> tuple[dict[str, str], set[str]]:
def _reverse_crosswalk(
self, c_func: CFunc, base: str) -> tuple[dict[str, str], set[str]]:
"""Translate CLib arguments back to Jinja argument list."""
handle = ""
args = []
@ -180,28 +180,41 @@ class CLibSourceGenerator(SourceGenerator):
return cxx_type.split("<")[-1].split(">")[0]
c_args = c_func.arglist
cxx_func = c_func.implements
if not cxx_func:
cxx_member = c_func.implements
if not cxx_member:
if c_func.name.endswith("new"):
# Default constructor
cxx_func = CFunc("auto", f"make_shared<{base}>", ArgList([]), "", None)
cxx_func = CFunc("auto", f"make_shared<{base}>", ArgList([]))
elif 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")
elif isinstance(cxx_member, Param):
if len(c_args) and "char*" in c_args[-1].p_type:
cxx_func = CFunc("string", cxx_member.name, None,
"", None, "", cxx_member.base)
else:
cxx_func = CFunc(cxx_member.p_type, cxx_member.name, None,
"", None, "", cxx_member.base)
else:
cxx_func = cxx_member
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 isinstance(cxx_member, Param) or 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):
if isinstance(cxx_member, Param) and cxx_member.direction == "out":
pass
elif isinstance(cxx_member, Param):
break
elif c_ix == len(c_args):
break
cxx_type = cxx_func.ret_type
# Handle output buffer
# Handle output buffer and/or variable assignments
cxx_type = cxx_func.ret_type
if "string" in cxx_type:
buffer = ["string out",
f"copyString(out, {c_args[c_ix+1].name}, "
@ -212,6 +225,11 @@ class CLibSourceGenerator(SourceGenerator):
"std::copy(out.begin(), out.end(), "
f"{c_args[c_ix+1].name});",
"int(out.size())"]
elif "bool" in cxx_type:
buffer = [f"{cxx_type} out", "", "int(out)"]
elif cxx_type in self._config.ret_type_crosswalk:
# can pass values directly
buffer = []
else:
msg = (f"Scaffolding failed for {c_func.name!r}: reverse crosswalk "
f"not implemented for {cxx_type!r}:\n{c_func.declaration()}")
@ -219,7 +237,10 @@ class CLibSourceGenerator(SourceGenerator):
exit(1)
break
cxx_arg = cxx_func.arglist[cxx_ix]
if isinstance(cxx_member, Param):
cxx_arg = cxx_member
else:
cxx_arg = cxx_func.arglist[cxx_ix]
if c_name != cxx_arg.name:
# Encountered object handle or length indicator
if c_ix == 0:
@ -306,6 +327,14 @@ class CLibSourceGenerator(SourceGenerator):
elif cxx_rtype.endswith("void"):
buffer = ["", "", "0"]
if isinstance(cxx_member, Param) and cxx_member.direction == "in":
c_name = c_func.arglist[-1].name
if cxx_rtype.endswith("bool"):
lines = [f"bool {c_name}_ = ({c_name} != 0);"]
args.append(f"{c_name}_")
else:
args.append(c_name)
ret = {
"base": base, "handle": handle, "lines": lines, "buffer": buffer,
"shared": shared, "checks": checks, "error": error, "cxx_rbase": cxx_rbase,
@ -333,6 +362,12 @@ class CLibSourceGenerator(SourceGenerator):
elif recipe.what == "function":
template = loader.from_string(self._templates["clib-function"])
elif recipe.what == "variable-getter":
template = loader.from_string(self._templates["clib-variable-getter"])
elif recipe.what == "variable-setter":
template = loader.from_string(self._templates["clib-variable-setter"])
elif recipe.what == "constructor":
template = loader.from_string(self._templates["clib-constructor"])
@ -376,7 +411,9 @@ class CLibSourceGenerator(SourceGenerator):
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]:
def merge_params(
implements: str, cxx_member: CFunc | Param
) -> tuple[list[Param], CFunc]:
"""Create preliminary CLib argument list."""
obj_handle = []
if "::" in implements:
@ -384,22 +421,28 @@ class CLibSourceGenerator(SourceGenerator):
what = implements.split("::")[0]
obj_handle.append(
Param("int", "handle", f"Handle to queried {what} object."))
if isinstance(cxx_member, Param):
if recipe.what.endswith("setter"):
return obj_handle + [cxx_member], cxx_member
return obj_handle, cxx_member
if "(" not in implements:
return obj_handle + cxx_func.arglist.params, cxx_func
return obj_handle + cxx_member.arglist.params, cxx_member
# 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)
if len(args_short) < len(cxx_member.arglist):
cxx_arglist = ArgList(cxx_member.arglist[:len(args_short)])
cxx_member = CFunc(cxx_member.ret_type, cxx_member.name,
cxx_arglist, cxx_member.brief, cxx_member.implements,
cxx_member.returns, cxx_member.base, cxx_member.uses)
return obj_handle + cxx_func.arglist.params, cxx_func
return obj_handle + cxx_member.arglist.params, cxx_member
func_name = f"{recipe.prefix}_{recipe.name}"
reserved = ["cabinetSize", "parentHandle",
"getCanteraError", "clearStorage", "resetStorage"]
"getCanteraError", "setLogWriter", "setLogCallback",
"clearStorage", "resetStorage"]
if recipe.name in reserved:
recipe.what = "reserved"
loader = Environment(loader=BaseLoader)
@ -414,10 +457,14 @@ class CLibSourceGenerator(SourceGenerator):
bases = [recipe.base] + recipe.parents + recipe.derived
if not recipe.implements:
recipe.implements = self._doxygen_tags.detect(recipe.name, bases)
elif recipe.base and "::" not in recipe.implements:
parts = list(recipe.implements.partition("("))
parts[0] = self._doxygen_tags.detect(parts[0], bases)
recipe.implements = "".join(parts)
recipe.uses = [self._doxygen_tags.detect(uu.split("(")[0], bases, False)
for uu in recipe.uses]
cxx_func = None
cxx_member = None
ret_param = Param("void")
args = []
brief = ""
@ -425,40 +472,54 @@ class CLibSourceGenerator(SourceGenerator):
if recipe.implements:
msg = f" generating {func_name!r} -> {recipe.implements}"
_LOGGER.debug(msg)
parts = list(recipe.implements.partition("("))
if not self._doxygen_tags.exists(parts[0]):
parts[0] = self._doxygen_tags.detect(parts[0], bases, False)
recipe.implements = "".join(parts)
cxx_func = self._doxygen_tags.cxx_func(recipe.implements)
cxx_member = self._doxygen_tags.cxx_member(
recipe.implements, recipe.what.endswith("setter"))
# 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 isinstance(cxx_member, CFunc):
# 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_member.ret_type, recipe.derived)
par_list, cxx_member = merge_params(recipe.implements, cxx_member)
prop_params = self._prop_crosswalk(par_list)
args = prop_params + buffer_params
brief = cxx_member.brief
elif recipe.what == "variable-setter":
ret_param = Param("int")
par_list, cxx_member = merge_params(recipe.implements, cxx_member)
args = self._prop_crosswalk(par_list)
brief = cxx_member.description
else:
# Variable getter
prop_params, cxx_member = merge_params(recipe.implements, cxx_member)
ret_param, buffer_params = self._ret_crosswalk(
cxx_member.p_type, recipe.derived)
args = prop_params + buffer_params
brief = cxx_member.description
if recipe.what and cxx_func:
if recipe.what and cxx_member:
# Recipe type and corresponding C++ function are known
pass
elif cxx_func:
elif cxx_member and isinstance(cxx_member, Param):
# Recipe represents a variable getter/setter
recipe.what = "variable-getter"
elif cxx_member:
# 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
cxx_arglen = len(cxx_member.arglist)
if not cxx_member.base:
if (cxx_member.name.startswith("new") and
any(base in cxx_member.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:
elif "void" not in cxx_member.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"):
elif "void" in cxx_member.ret_type and cxx_arglen == 1:
p_type = cxx_member.arglist[0].p_type
if cxx_member.name.startswith("get"):
recipe.what = "getter"
elif "*" in p_type and not p_type.startswith("const"):
recipe.what = "getter" # getter assigns to existing array
@ -505,8 +566,8 @@ class CLibSourceGenerator(SourceGenerator):
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,
uses = [self._doxygen_tags.cxx_member(uu) for uu in recipe.uses]
return CFunc(ret_param.p_type, func_name, ArgList(args), brief, cxx_member,
ret_param.description, None, uses)
def _write_header(self, headers: HeaderFile) -> None:
@ -528,11 +589,13 @@ class CLibSourceGenerator(SourceGenerator):
annotations=self._scaffold_annotation(c_func, recipe.what)))
declarations = "\n\n".join(declarations)
preamble = self._config.preambles.get(headers.base)
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)
name=filename.stem, guard=guard, preamble=preamble, prefix=headers.prefix,
declarations=declarations, base=headers.base, docstring=headers.docstring)
out = (Path(self._out_dir) /
"include" / "cantera" / "clib_experimental" / filename.name)

View File

@ -17,6 +17,7 @@ class Config:
"int": "int",
"size_t": "int",
"double": "double",
"const double": "double",
"shared_ptr<T>": "int",
"string": "char*",
"const string": "char*",
@ -34,6 +35,7 @@ class Config:
"double* const": "double*",
"const double*": "const double*",
"const double* const": "const double*",
"string": "const char*",
"const string&": "const char*",
"shared_ptr<T>": "int",
"const shared_ptr<T>": "int",
@ -41,9 +43,13 @@ class Config:
"const vector<shared_ptr<T>>&": "int[]",
}
includes: dict[str, list[str]]
preambles: dict[str, str] #: Preamble text for each header file
includes: dict[str, list[str]] #: Include directives for each implementation file
@classmethod
def from_parsed(cls: Self, *, includes: dict[str, list[str]] | None = None) -> Self:
def from_parsed(cls: Self, *,
preambles: dict[str, str] | None = None,
includes: dict[str, list[str]] | None = None) -> Self:
"""Create dataclass while including information parsed externally."""
return cls(includes or {})
return cls(preambles or {}, includes or {})

View File

@ -8,13 +8,18 @@ ignore_files: []
# Dictionary of file names and list of functions to ignore.
# Example: ctkin_auto.yaml: [phase]
ignore_funcs:
ct_auto.yaml: [setLogWriter, setLogCallback]
ignore_funcs: {}
# Cabinets with associated includes
# Cabinets with associated preambles (headers)
preambles:
"": |-
#include "../clib/clib_defs.h"
# Cabinets with associated includes (implementation files)
includes:
"":
- cantera/base/global.h
- cantera/base/ExternalLogger.h
Solution:
- cantera/base/Solution.h
Interface:
@ -32,5 +37,7 @@ includes:
- cantera/thermo/ThermoPhase.h
Reaction:
- cantera/kinetics/Reaction.h
ReactionPathDiagram:
- cantera/kinetics/ReactionPath.h
Func1:
- cantera/numerics/Func1Factory.h

View File

@ -53,6 +53,19 @@ clib-reserved-getCanteraError-h: |-
*/
int {{ prefix }}_getCanteraError(int bufLen, char* buf);
clib-reserved-setLogWriter-h: |-
/**
* Undocumented.
*/
int {{ prefix }}_setLogWriter(void* logger);
clib-reserved-setLogCallback-h: |-
/**
* Undocumented.
* @param writer Callback that is invoked to produce log output.
*/
int {{ prefix }}_setLogCallback(LogCallback writer);
clib-reserved-resetStorage-h: |-
/**
* Delete all objects and erase mapping.
@ -98,6 +111,10 @@ clib-header-file: |-
#ifndef {{ guard }}
#define {{ guard }}
{% if preamble %}
{{ preamble }}
{% endif %}
#ifdef __cplusplus
extern "C" {
#endif
@ -133,6 +150,81 @@ clib-function: |-
return handleAllExceptions({{ error[0] }}, {{ error[1] }});
}
clib-variable-getter: |-
// variable (getter): {{ cxx_implements }}
try {
{% if handle %}
## access class member variable
{% if cxx_base == base %}
## object can be accessed directly
{% if buffer and buffer[0] %}
{{ buffer[0] }} = {{ base }}Cabinet::at({{ handle }})->{{ cxx_name }};
## CLib/C++ variable crosswalk needed
{% if buffer[1] %}
{{ buffer[1] }}
{% endif %}
return {{ buffer[2] }};
{% else %}{# not buffer[0] #}
## no crosswalk needed
return {{ base }}Cabinet::at({{ handle }})->{{ cxx_name }};
{% endif %}{# buffer[0] #}
{% else %}{# base #}
## object needs a cast as method is defined for specialization
{% if buffer and buffer[0] %}
{{ buffer[0] }} = {{ base }}Cabinet::as<{{ cxx_base }}>({{ handle }})->{{ cxx_name }};
## CLib/C++ variable crosswalk needed
{% if buffer[1] %}
{{ buffer[1] }}
{% endif %}
return {{ buffer[2] }};
{% else %}{# not buffer[0] #}
## no crosswalk needed
return {{ base }}Cabinet::as<{{ cxx_base }}>({{ handle }})->{{ cxx_name }};
{% endif %}{# buffer[0] #}
{% endif %}{# base #}
{% else %}{# not handle #}
## variable is defined in root namespace
{% if buffer and buffer[0] %}
## CLib/C++ variable crosswalk needed
{{ buffer[0] }} = {{ cxx_name }};
{% if buffer[1] %}
{{ buffer[1] }}
{% endif %}
return {{ buffer[2] }};
{% else %}{# not buffer[0] #}
## no crosswalk needed
return {{ cxx_name }};
{% endif %}{# buffer[0] #}
{% endif %}{# handle #}
} catch (...) {
return handleAllExceptions({{ error[0] }}, {{ error[1] }});
}
clib-variable-setter: |-
// variable (setter): {{ cxx_implements }}
try {
{% for line in lines %}
## add lines used for CLib/C++ variable crosswalk
{{ line }}
{% endfor %}
{% if handle %}
## access class member variable
{% if cxx_base == base %}
## object can be accessed directly
{{ base }}Cabinet::at({{ handle }})->{{ cxx_name }} = {{ cxx_args[0] }};
{% else %}{# not base #}
## object needs a cast as method is defined for specialization
{{ base }}Cabinet::as<{{ cxx_base }}>({{ handle }})->{{ cxx_name }} = {{ cxx_args[0] }};
{% endif %}{# base #}
{% else %}{# not handle #}
## variable is defined in root namespace
{{ cxx_name }} = {{ cxx_args[0] }};
{% endif %}{# handle #}
return 0;
} catch (...) {
return handleAllExceptions(-1, ERR);
}
clib-constructor: |-
## CLib constructor template: instantiates new object
// constructor: {{ cxx_implements }}
@ -295,7 +387,7 @@ clib-array-getter: |-
// no size checking specified
{% endif %}
{% if buffer and buffer[0] %}
{{ buffer[0] }} = obj->{{ cxx_name }}({{ cxx_args[0] }});;
{{ buffer[0] }} = obj->{{ cxx_name }}({{ cxx_args[0] }});
{% if buffer[1] %}
{{ buffer[1] }}
{% endif %}
@ -374,6 +466,27 @@ clib-reserved-getCanteraError-cpp: |-
return handleAllExceptions(-1, ERR);
}
clib-reserved-setLogWriter-cpp: |-
// reserved: setLogger
try {
Logger* logwriter = (Logger*)logger;
setLogger(logwriter);
return 0;
} catch (...) {
return handleAllExceptions(-1, ERR);
}
clib-reserved-setLogCallback-cpp: |-
// reserved: setLogger
static unique_ptr<Logger> logwriter;
try {
logwriter = make_unique<ExternalLogger>(writer);
setLogger(logwriter.get());
return 0;
} catch (...) {
return handleAllExceptions(-1, ERR);
}
clib-reserved-resetStorage-cpp: |-
// reserved: void Cabinet<T>::reset()
try {

View File

@ -6,6 +6,7 @@
// 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/base/ctexceptions.h"
#include "cantera/kinetics/ReactionPath.h"
#include "cantera/kinetics/Reaction.h"
#include "cantera/thermo/ThermoPhase.h"
@ -78,6 +79,17 @@ void Path::writeLabel(ostream& s, double threshold)
}
}
ReactionPathDiagram::ReactionPathDiagram(
shared_ptr<Kinetics> kin, const string& element_) : element(element_), m_kin(kin)
{
if (!m_kin) {
throw CanteraError("ReactionPathDiagram::ReactionPathDiagram",
"Kinetics object must not be empty.");
}
m_builder = make_shared<ReactionPathBuilder>();
m_builder->init(m_log, *m_kin.get());
}
ReactionPathDiagram::~ReactionPathDiagram()
{
// delete the nodes
@ -125,6 +137,11 @@ void ReactionPathDiagram::add(ReactionPathDiagram& d)
}
}
void ReactionPathDiagram::add(shared_ptr<ReactionPathDiagram> d)
{
add(*d.get());
}
void ReactionPathDiagram::findMajorPaths(double athreshold, size_t lda, double* a)
{
double netmax = 0.0;
@ -148,6 +165,57 @@ void ReactionPathDiagram::findMajorPaths(double athreshold, size_t lda, double*
}
}
const string ReactionPathDiagram::flowType() const
{
if (flow_type == OneWayFlow) {
return "OneWayFlow";
}
return "NetFlow";
}
void ReactionPathDiagram::setFlowType(const string& fType)
{
if (fType == "OneWayFlow") {
flow_type = OneWayFlow;
} else if (fType == "NetFlow") {
flow_type = NetFlow;
} else {
throw CanteraError("ReactionPathDiagram::setFlowType",
"Unknown flow type '{}'", fType);
}
}
void ReactionPathDiagram::build()
{
m_builder->build(*m_kin.get(), element, m_log, *this, true);
m_isBuilt = true;
}
string ReactionPathDiagram::getDot()
{
if (!m_isBuilt) {
build();
}
std::stringstream out;
exportToDot(out);
return out.str();
}
string ReactionPathDiagram::getData()
{
if (!m_isBuilt) {
build();
}
std::stringstream out;
writeData(out);
return out.str();
}
string ReactionPathDiagram::getLog()
{
return m_log.str();
}
void ReactionPathDiagram::writeData(ostream& s)
{
s << title << endl;
@ -791,4 +859,11 @@ int ReactionPathBuilder::build(Kinetics& s, const string& element,
return 1;
}
shared_ptr<ReactionPathDiagram> newReactionPathDiagram(
shared_ptr<Kinetics> kin, const string& element)
{
return shared_ptr<ReactionPathDiagram>(
new ReactionPathDiagram(kin, element));
}
}

View File

@ -209,6 +209,13 @@ TEST(ct3, transport)
}
}
TEST(ct3, constants)
{
ASSERT_EQ(ct3_Avogadro(), Avogadro);
ASSERT_EQ(ct3_GasConstant(), GasConstant);
ASSERT_EQ(ct3_OneAtm(), OneAtm);
}
int main(int argc, char** argv)
{

View File

@ -10,9 +10,11 @@
#include "cantera/clib_experimental/ctthermo3.h"
#include "cantera/clib_experimental/ctkin3.h"
#include "cantera/clib_experimental/ctrxn3.h"
#include "cantera/clib_experimental/ctrdiag3.h"
using namespace Cantera;
using ::testing::HasSubstr;
using ::testing::StartsWith;
string reportError(); // forward declaration
@ -59,7 +61,7 @@ TEST(ctkin3, exceptions)
EXPECT_THAT(err, HasSubstr("IndexError: 998 outside valid range of 0 to 28."));
}
TEST(ctkin3, reaction)
TEST(ctrxn3, reaction)
{
int sol0 = sol3_newSolution("h2o2.yaml", "", "none");
int kin = sol3_kinetics(sol0);
@ -72,15 +74,76 @@ TEST(ctkin3, reaction)
int buflen = rxn3_type(rxn, 0, 0);
vector<char> buf(buflen);
rxn3_type(rxn, buflen, buf.data());
string rxnType = buf.data();
ASSERT_EQ(rxnType, reaction->type());
ASSERT_EQ(rxnType, "three-body-Arrhenius");
string text = buf.data();
ASSERT_EQ(text, reaction->type());
ASSERT_EQ(text, "three-body-Arrhenius");
buflen = rxn3_equation(rxn, 0, 0);
buf.resize(buflen);
rxn3_equation(rxn, buflen, buf.data());
string rxnEqn = buf.data();
ASSERT_EQ(rxnEqn, reaction->equation());
ASSERT_EQ(rxnEqn, "2 O + M <=> O2 + M");
text = buf.data();
ASSERT_EQ(text, reaction->equation());
ASSERT_EQ(text, "2 O + M <=> O2 + M");
ASSERT_EQ(rxn3_usesThirdBody(rxn), 1);
ASSERT_EQ(rxn3_allowNonreactantOrders(rxn), 0);
ASSERT_EQ(rxn3_allowNonreactantOrders(rxn),
int(reaction->allow_nonreactant_orders));
buflen = rxn3_id(rxn, 0, 0);
buf.resize(buflen);
rxn3_id(rxn, buflen, buf.data());
text = buf.data();
ASSERT_EQ(text, reaction->id);
ASSERT_EQ(text, "");
rxn3_setId(rxn, "spam");
buflen = rxn3_id(rxn, 0, 0);
buf.resize(buflen);
rxn3_id(rxn, buflen, buf.data());
text = buf.data();
ASSERT_EQ(text, "spam");
}
TEST(ctrdiag3, diagram)
{
int sol = sol3_newSolution("h2o2.yaml", "", "none");
int thermo = sol3_thermo(sol);
int kin = sol3_kinetics(sol);
thermo3_set_TP(thermo, 1000., ct3_OneAtm());
int diag = rdiag3_newReactionPathDiagram(kin, "H");
ASSERT_EQ(rdiag3_boldThreshold(diag), 0.2);
rdiag3_setFlowType(diag, "spam");
string text = reportError();
EXPECT_THAT(text, HasSubstr("Unknown flow type 'spam'"));
int buflen = rdiag3_flowType(diag, 0, 0);
vector<char> buf(buflen);
rdiag3_flowType(diag, buflen, buf.data());
text = buf.data();
ASSERT_EQ(text, "NetFlow");
rdiag3_setFlowType(diag, "OneWayFlow");
buflen = rdiag3_flowType(diag, 0, 0);
buf.resize(buflen);
rdiag3_flowType(diag, buflen, buf.data());
text = buf.data();
ASSERT_EQ(text, "OneWayFlow");
buflen = rdiag3_getLog(diag, 0, 0);
buf.resize(buflen);
rdiag3_getLog(diag, buflen, buf.data());
text = buf.data();
EXPECT_THAT(text, StartsWith("\nReaction 1:"));
buflen = rdiag3_getData(diag, 0, 0);
buf.resize(buflen);
rdiag3_getData(diag, buflen, buf.data());
text = buf.data();
EXPECT_THAT(text, StartsWith("\nH H2 \nH H2"));
buflen = rdiag3_getDot(diag, 0, 0);
buf.resize(buflen);
rdiag3_getDot(diag, buflen, buf.data());
text = buf.data();
EXPECT_THAT(text, StartsWith("digraph reaction_paths"));
}