Skip to content

Simpler way to create objects defining an interface #103

@speth

Description

@speth

Abstract

The currently-available methods for instantiating the set of ThermoPhase and Kinetics objects defining an interface is somewhat tedious, especially in C++. With appropriate information included in the input file, this could be simplified to a single function call to construct all of the objects and link them together.

Motivation

In C++, constructing a surface and its adjoining bulk phases requires something like the following:

shared_ptr<Solution> gas = newSolution("diamond.yaml", "gas");
shared_ptr<Solution> bulk = newSolution("diamond.yaml", "diamond");
shared_ptr<Solution> surfSoln = newSolution("diamond.yaml", "diamond_100", "None", {gas, solid});
auto surf = std::dynamic_pointer_cast<SurfPhase>(soln->thermo());
auto surfKin = std::dynamic_pointer_cast<InterfaceKinetics>(soln->kinetics());

Where the two somewhat arcane dynamic_pointer_cast calls are required in the relatively common case where the user needs to access methods that are defined only on these derived classes, e.g. setCoverages and advanceCoverages. This is not to mention some of the more convoluted and fragile ways of initializing surface phase objects which litter the test suite.

Possible Solutions

The removal of the old Interface class in Cantera 2.5 paves the way for a new Interface class that follows the structure of the Solution class and is not tied to a specific phase model for the surface phase (e.g. providing flexibility to use a non-ideal surface phase model as planned in #59). For this class the thermo() and kinetics() would return shared_ptr<SurfPhase> and shared_ptr<InterfaceKinetics>, respectively, removing the need for the user to invoke dynamic_pointer_cast.

We could also go a step further and include information in the YAML definition of the surface phase which specifies where the adjacent phases are defined in order to instantiate all of the phases at once. (While this information was included in CTI files, it was used only to require that these adjacent bulk phases were present during construction, but not to actually help construct them.)
For example:

phases:
- name: diamond_100
  adjacent-phases: [gas, diamond]
  ...
- name: gas
  ...
- name: diamond
  ...

Then, we could define the newInterface function so that the only required parameters were the input file name and the (optional) phase name. There are a couple of options for what to do with the adjacent phases. Perhaps the best would be to store those Solution objects as part of the Interface object. For example:

shared_ptr<Interface> surf = newInterface("diamond.yaml", "diamond_100");
shared_ptr<ThermoPhase> gas = surf->adjacent(0)->thermo();
shared_ptr<ThermoPhase> bulk = surf->adjacent("diamond")->thermo(); // accessible by name or index

In the case where the additional phase names were not in the input file (or where the user wanted to override those values), they could also be provided as an additional argument to the newInterface function.

A few other thoughts:

  • It would probably make sense to extend this idea to an Edge class as well, since instantiating the set of phases involved is even more complicated, with up to 3 bulk phases and 3 interfaces involved.
  • For the Python module, adding this should be pretty simple, as the generic Interface class already exists, and importing the adjacent phases could be done as part of the constructor for that class. One issue here would be how to transition, given that the Interface constructor doesn't currently import those phases.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    work-in-progressAn enhancement that someone is currently working on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions