2108 lines
84 KiB
Python
2108 lines
84 KiB
Python
#!/usr/bin/env python
|
|
# encoding: utf-8
|
|
|
|
# This file is part of Cantera. See License.txt in the top-level directory or
|
|
# at https://www.cantera.org/license.txt for license and copyright information.
|
|
|
|
"""
|
|
ck2yaml.py: Convert Chemkin-format mechanisms to Cantera YAML input files
|
|
|
|
Usage:
|
|
ck2yaml [--input=<filename>]
|
|
[--thermo=<filename>]
|
|
[--transport=<filename>]
|
|
[--surface=<filename>]
|
|
[--id=<phase-id>]
|
|
[--output=<filename>]
|
|
[--permissive]
|
|
[-d | --debug]
|
|
|
|
Example:
|
|
ck2yaml --input=chem.inp --thermo=therm.dat --transport=tran.dat
|
|
|
|
If the output file name is not given, an output file with the same name as the
|
|
input file, with the extension changed to '.yaml'.
|
|
|
|
An input file containing only species definitions (which can be referenced from
|
|
phase definitions in other input files) can be created by specifying only a
|
|
thermo file.
|
|
|
|
For the case of a surface mechanism, the gas phase input file should be
|
|
specified as 'input' and the surface phase input file should be specified as
|
|
'surface'.
|
|
|
|
The '--permissive' option allows certain recoverable parsing errors (e.g.
|
|
duplicate transport data) to be ignored.
|
|
"""
|
|
|
|
from collections import defaultdict, OrderedDict
|
|
import logging
|
|
import os.path
|
|
import sys
|
|
import numpy as np
|
|
import re
|
|
import itertools
|
|
import getopt
|
|
import textwrap
|
|
from email.utils import formatdate
|
|
|
|
try:
|
|
import ruamel_yaml as yaml
|
|
except ImportError:
|
|
from ruamel import yaml
|
|
|
|
BlockMap = yaml.comments.CommentedMap
|
|
|
|
def FlowMap(*args, **kwargs):
|
|
m = yaml.comments.CommentedMap(*args, **kwargs)
|
|
m.fa.set_flow_style()
|
|
return m
|
|
|
|
def FlowList(*args, **kwargs):
|
|
lst = yaml.comments.CommentedSeq(*args, **kwargs)
|
|
lst.fa.set_flow_style()
|
|
return lst
|
|
|
|
# Improved float formatting requires Numpy >= 1.14
|
|
if hasattr(np, 'format_float_positional'):
|
|
def float2string(data):
|
|
if data == 0:
|
|
return '0.0'
|
|
elif 0.01 <= abs(data) < 10000:
|
|
return np.format_float_positional(data, trim='0')
|
|
else:
|
|
return np.format_float_scientific(data, trim='0')
|
|
else:
|
|
def float2string(data):
|
|
return repr(data)
|
|
|
|
def represent_float(self, data):
|
|
# type: (Any) -> Any
|
|
if data != data:
|
|
value = '.nan'
|
|
elif data == self.inf_value:
|
|
value = '.inf'
|
|
elif data == -self.inf_value:
|
|
value = '-.inf'
|
|
else:
|
|
value = float2string(data)
|
|
|
|
return self.represent_scalar(u'tag:yaml.org,2002:float', value)
|
|
|
|
yaml.RoundTripRepresenter.add_representer(float, represent_float)
|
|
|
|
QUANTITY_UNITS = {'MOL': 'mol',
|
|
'MOLE': 'mol',
|
|
'MOLES': 'mol',
|
|
'MOLEC': 'molec',
|
|
'MOLECULES': 'molec'}
|
|
|
|
ENERGY_UNITS = {'CAL/': 'cal/mol',
|
|
'CAL/MOL': 'cal/mol',
|
|
'CAL/MOLE': 'cal/mol',
|
|
'EVOL': 'eV',
|
|
'EVOLTS': 'eV',
|
|
'JOUL': 'J/mol',
|
|
'JOULES/MOL': 'J/mol',
|
|
'JOULES/MOLE': 'J/mol',
|
|
'KCAL': 'kcal/mol',
|
|
'KCAL/MOL': 'kcal/mol',
|
|
'KCAL/MOLE': 'kcal/mol',
|
|
'KELV': 'K',
|
|
'KELVIN': 'K',
|
|
'KELVINS': 'K',
|
|
'KJOU': 'kJ/mol',
|
|
'KJOULES/MOL': 'kJ/mol',
|
|
'KJOULES/MOLE': 'kJ/mol'}
|
|
|
|
def strip_nonascii(s):
|
|
return s.encode('ascii', 'ignore').decode()
|
|
|
|
|
|
def compatible_quantities(quantity_basis, units):
|
|
if quantity_basis == 'mol':
|
|
return 'molec' not in units
|
|
elif quantity_basis == 'molec':
|
|
return 'molec' in units or 'mol' not in units
|
|
else:
|
|
raise ValueError('Unknown quantity basis: "{}"'.format(quantity_basis))
|
|
|
|
|
|
class InputError(Exception):
|
|
"""
|
|
An exception class for exceptional behavior involving Chemkin-format
|
|
mechanism files. Pass a string describing the circumstances that caused
|
|
the exceptional behavior.
|
|
"""
|
|
def __init__(self, message, *args, **kwargs):
|
|
if args or kwargs:
|
|
super().__init__(message.format(*args, **kwargs))
|
|
else:
|
|
super().__init__(message)
|
|
|
|
|
|
class Species:
|
|
def __init__(self, label, sites=None):
|
|
self.label = label
|
|
self.thermo = None
|
|
self.transport = None
|
|
self.sites = sites
|
|
self.composition = None
|
|
self.note = None
|
|
|
|
def __str__(self):
|
|
return self.label
|
|
|
|
@classmethod
|
|
def to_yaml(cls, representer, node):
|
|
out = BlockMap([('name', node.label),
|
|
('composition', FlowMap(node.composition.items()))])
|
|
if node.thermo:
|
|
out['thermo'] = node.thermo
|
|
if node.transport:
|
|
out['transport'] = node.transport
|
|
if node.sites:
|
|
out['sites'] = node.sites
|
|
if node.note:
|
|
out['note'] = node.note
|
|
return representer.represent_dict(out)
|
|
|
|
|
|
class Nasa7:
|
|
"""
|
|
Thermodynamic data parameterized as two seven-coefficient NASA
|
|
polynomials.
|
|
See https://cantera.org/science/science-species.html#the-nasa-7-coefficient-polynomial-parameterization
|
|
"""
|
|
def __init__(self, *, Tmin, Tmax, Tmid, low_coeffs, high_coeffs, note=''):
|
|
self.Tmin = Tmin
|
|
self.Tmax = Tmax
|
|
self.Tmid = Tmid
|
|
self.low_coeffs = low_coeffs
|
|
self.high_coeffs = high_coeffs
|
|
self.note = note
|
|
|
|
@classmethod
|
|
def to_yaml(cls, representer, node):
|
|
out = BlockMap([('model', 'NASA7')])
|
|
out['temperature-ranges'] = FlowList([node.Tmin, node.Tmid, node.Tmax])
|
|
out['data'] = [FlowList(node.low_coeffs), FlowList(node.high_coeffs)]
|
|
if node.note:
|
|
note = textwrap.dedent(node.note.rstrip())
|
|
if '\n' in note:
|
|
note = yaml.scalarstring.PreservedScalarString(note)
|
|
out['note'] = note
|
|
return representer.represent_dict(out)
|
|
|
|
|
|
class Nasa9:
|
|
"""
|
|
Thermodynamic data parameterized as any number of nine-coefficient NASA
|
|
polynomials.
|
|
See https://cantera.org/science/science-species.html#the-nasa-9-coefficient-polynomial-parameterization
|
|
|
|
:param data:
|
|
List of polynomials, where each polynomial is written as
|
|
```
|
|
[(T_low, T_high), [a_0, a_1, ..., a_8]]
|
|
```
|
|
"""
|
|
def __init__(self, *, data, note=''):
|
|
self.note = note
|
|
self.data = list(sorted(data))
|
|
self.Tranges = [self.data[0][0][0]]
|
|
for i in range(1, len(data)):
|
|
if abs(self.data[i-1][0][1] - self.data[i][0][0]) > 0.01:
|
|
raise ValueError('NASA9 polynomials contain non-adjacent temperature ranges')
|
|
self.Tranges.append(self.data[i][0][0])
|
|
self.Tranges.append(self.data[-1][0][1])
|
|
|
|
@classmethod
|
|
def to_yaml(cls, representer, node):
|
|
out = BlockMap([('model', 'NASA9')])
|
|
out['temperature-ranges'] = FlowList(node.Tranges)
|
|
out['data'] = [FlowList(poly) for (trange, poly) in node.data]
|
|
if node.note:
|
|
out['note'] = node.note
|
|
return representer.represent_dict(out)
|
|
|
|
|
|
class Reaction:
|
|
"""
|
|
:param index:
|
|
A unique nonnegative integer index
|
|
:param reactants:
|
|
A list of `(stoichiometry, species name)` tuples
|
|
:param products:
|
|
A list of `(stoichiometry, species name)` tuples
|
|
:param kinetics:
|
|
A `KineticsModel` instance which describes the rate constant
|
|
:param reversible:
|
|
Boolean indicating whether the reaction is reversible
|
|
:param duplicate:
|
|
Boolean indicating whether the reaction is a known (permitted) duplicate
|
|
:param forward_orders:
|
|
A dictionary specifying a non-default reaction order (value) for each
|
|
specified species (key)
|
|
:param third_body:
|
|
A string name used for the third-body species written in
|
|
pressure-dependent reaction types (usually "M")
|
|
"""
|
|
|
|
def __init__(self, parser, index=-1, reactants=None, products=None,
|
|
kinetics=None, reversible=True, duplicate=False,
|
|
forward_orders=None, third_body=None):
|
|
self.parser = parser
|
|
self.index = index
|
|
self.reactants = reactants # list of (stoichiometry, species) tuples
|
|
self.products = products # list of (stoichiometry, species) tuples
|
|
self.kinetics = kinetics
|
|
self.reversible = reversible
|
|
self.duplicate = duplicate
|
|
self.forward_orders = forward_orders or {}
|
|
self.third_body = ''
|
|
self.comment = ''
|
|
|
|
def _coeff_string(self, coeffs):
|
|
L = []
|
|
for stoichiometry, species in coeffs:
|
|
if stoichiometry != 1:
|
|
L.append('{0} {1}'.format(stoichiometry, species))
|
|
else:
|
|
L.append(str(species))
|
|
expression = ' + '.join(L)
|
|
expression += self.kinetics.reaction_string_suffix(self.third_body)
|
|
return expression
|
|
|
|
def __str__(self):
|
|
"""
|
|
Return a string representation of the reaction, e.g. 'A + B <=> C + D'.
|
|
"""
|
|
return '{}{}{}'.format(self._coeff_string(self.reactants),
|
|
' <=> ' if self.reversible else ' => ',
|
|
self._coeff_string(self.products))
|
|
|
|
@classmethod
|
|
def to_yaml(cls, representer, node):
|
|
out = BlockMap([('equation', str(node))])
|
|
out.yaml_add_eol_comment('Reaction {}'.format(node.index), 'equation')
|
|
if node.duplicate:
|
|
out['duplicate'] = True
|
|
node.kinetics.reduce(out)
|
|
if node.forward_orders:
|
|
out['orders'] = FlowMap(node.forward_orders)
|
|
if any((float(x) < 0 for x in node.forward_orders.values())):
|
|
out['negative-orders'] = True
|
|
node.parser.warn('Negative reaction order for reaction {} ({}).'.format(
|
|
node.index, str(node)))
|
|
reactant_names = {r[1].label for r in node.reactants}
|
|
if any((species not in reactant_names for species in node.forward_orders)):
|
|
out['nonreactant-orders'] = True
|
|
node.parser.warn('Non-reactant order for reaction {} ({}).'.format(
|
|
node.index, str(node)))
|
|
if node.comment:
|
|
comment = textwrap.dedent(node.comment.rstrip())
|
|
if '\n' in comment:
|
|
comment = yaml.scalarstring.PreservedScalarString(comment)
|
|
out['note'] = comment
|
|
return representer.represent_dict(out)
|
|
|
|
|
|
class KineticsModel:
|
|
"""
|
|
A base class for kinetics models
|
|
"""
|
|
pressure_dependent = None # overloaded in derived classes
|
|
|
|
def __init__(self):
|
|
self.efficiencies = {}
|
|
|
|
def reaction_string_suffix(self, species):
|
|
"""
|
|
Suffix for reactant and product strings, used for pressure-dependent
|
|
reactions
|
|
"""
|
|
return ''
|
|
|
|
def reduce(self, output):
|
|
"""
|
|
Assign data from this object to the YAML mapping ``output``
|
|
"""
|
|
raise InputError('reduce is not implemented for objects of class {}',
|
|
self.__class__.__name__)
|
|
|
|
|
|
class Arrhenius:
|
|
"""
|
|
Represent a modified Arrhenius rate.
|
|
|
|
:param A:
|
|
The pre-exponential factor, given as a tuple consisting of a floating
|
|
point value and a units string
|
|
:param b:
|
|
The temperature exponent
|
|
:param Ea:
|
|
The activation energy, given as a tuple consisting of a floating
|
|
point value and a units string
|
|
"""
|
|
def __init__(self, A=0.0, b=0.0, Ea=0.0, *, parser):
|
|
self.A = A
|
|
self.b = b
|
|
self.Ea = Ea
|
|
self.parser = parser
|
|
|
|
def as_yaml(self, extra=()):
|
|
out = FlowMap(extra)
|
|
if compatible_quantities(self.parser.output_quantity_units, self.A[1]):
|
|
out['A'] = self.A[0]
|
|
else:
|
|
out['A'] = "{0:e} {1}".format(*self.A)
|
|
|
|
out['b'] = self.b
|
|
|
|
if self.Ea[1] == self.parser.output_energy_units:
|
|
out['Ea'] = self.Ea[0]
|
|
else:
|
|
out['Ea'] = "{0} {1}".format(*self.Ea)
|
|
|
|
return out
|
|
|
|
|
|
class ElementaryRate(KineticsModel):
|
|
"""
|
|
A reaction rate described by a single Arrhenius expression.
|
|
See https://cantera.org/science/reactions.html#reactions-with-a-pressure-independent-rate
|
|
|
|
:param rate:
|
|
The Arrhenius expression describing this reaction rate.
|
|
"""
|
|
pressure_dependent = False
|
|
|
|
def __init__(self, rate, **kwargs):
|
|
KineticsModel.__init__(self, **kwargs)
|
|
self.rate = rate
|
|
|
|
def reduce(self, output):
|
|
output['rate-constant'] = self.rate.as_yaml()
|
|
if self.rate.A[0] < 0:
|
|
output['negative-A'] = True
|
|
|
|
class SurfaceRate(KineticsModel):
|
|
"""
|
|
An Arrhenius-like reaction occurring on a surface
|
|
See https://cantera.org/science/reactions.html#surface-reactions
|
|
|
|
:param rate:
|
|
The Arrhenius expression describing this reaction rate.
|
|
:param coverages:
|
|
A list of tuples where each tuple specifies the coverage dependencies
|
|
for a species, in the form `(species_name, a_k, m_k, E_k)`
|
|
:param is_sticking:
|
|
True if the Arrhenius expression is a parameterization of a sticking
|
|
coefficient, rather than the rate constant itself.
|
|
:param motz_wise:
|
|
True if the sticking coefficient should be translated into a rate
|
|
coefficient using the correction factor developed by Motz & Wise for
|
|
reactions with high (near-unity) sticking coefficients
|
|
"""
|
|
pressure_dependent = False
|
|
|
|
def __init__(self, *, rate, coverages, is_sticking, motz_wise, **kwargs):
|
|
KineticsModel.__init__(self, **kwargs)
|
|
self.rate = rate
|
|
self.coverages = coverages
|
|
self.is_sticking = is_sticking
|
|
self.motz_wise = motz_wise
|
|
|
|
def reduce(self, output):
|
|
if self.is_sticking:
|
|
output['sticking-coefficient'] = self.rate.as_yaml()
|
|
else:
|
|
output['rate-constant'] = self.rate.as_yaml()
|
|
|
|
if self.motz_wise is not None:
|
|
output['Motz-Wise'] = self.motz_wise
|
|
|
|
if self.coverages:
|
|
covdeps = BlockMap()
|
|
for species,A,m,E in self.coverages:
|
|
# Energy units for coverage modification match energy units for
|
|
# base reaction
|
|
if self.rate.Ea[1] != self.rate.parser.output_energy_units:
|
|
E = '{} {}'.format(E, self.rate.Ea[1])
|
|
covdeps[species] = FlowList([A, m, E])
|
|
output['coverage-dependencies'] = covdeps
|
|
|
|
|
|
class PDepArrhenius(KineticsModel):
|
|
"""
|
|
A rate calculated by interpolating between Arrhenius expressions at
|
|
various pressures.
|
|
See https://cantera.org/science/reactions.html#pressure-dependent-arrhenius-rate-expressions-p-log
|
|
|
|
:param pressures:
|
|
A list of pressures at which Arrhenius expressions are given.
|
|
:param pressure_units:
|
|
A string indicating the units used for the pressures
|
|
:param arrhenius:
|
|
A list of `Arrhenius` objects at each given pressure
|
|
"""
|
|
pressure_dependent = True
|
|
|
|
def __init__(self, *, pressures, pressure_units, arrhenius, **kwargs):
|
|
KineticsModel.__init__(self, **kwargs)
|
|
self.pressures = pressures
|
|
self.pressure_units = pressure_units
|
|
self.arrhenius = arrhenius or []
|
|
|
|
def reduce(self, output):
|
|
output['type'] = 'pressure-dependent-Arrhenius'
|
|
rates = []
|
|
for pressure, arrhenius in zip(self.pressures, self.arrhenius):
|
|
rates.append(arrhenius.as_yaml(
|
|
[('P', '{0} {1}'.format(pressure, self.pressure_units))]))
|
|
output['rate-constants'] = rates
|
|
|
|
|
|
class Chebyshev(KineticsModel):
|
|
"""
|
|
A rate calculated in terms of a bivariate Chebyshev polynomial.
|
|
See https://cantera.org/science/reactions.html#chebyshev-reaction-rate-expressions
|
|
|
|
:param coeffs:
|
|
Matrix of Chebyshev coefficients, dimension N_T by N_P
|
|
:param Tmin:
|
|
Minimum temperature for which the parameterization is valid
|
|
:param Tmax:
|
|
Maximum temperature for which the parameterization is valid
|
|
:param Pmin:
|
|
Minimum pressure for which the parameterization is valid, given as a
|
|
`(value, units)` tuple
|
|
:param Pmax:
|
|
Maximum pressure for which the parameterization is valid, given as a
|
|
`(value, units)` tuple
|
|
:param quantity_units:
|
|
Quantity units for the rate constant
|
|
"""
|
|
pressure_dependent = True
|
|
|
|
def __init__(self, coeffs, *, Tmin, Tmax, Pmin, Pmax, quantity_units,
|
|
**kwargs):
|
|
KineticsModel.__init__(self, **kwargs)
|
|
self.Tmin = Tmin
|
|
self.Tmax = Tmax
|
|
self.Pmin = Pmin
|
|
self.Pmax = Pmax
|
|
self.coeffs = coeffs
|
|
self.quantity_units = quantity_units
|
|
|
|
def reaction_string_suffix(self, species):
|
|
return ' (+{})'.format(species)
|
|
|
|
def reduce(self, output):
|
|
output['type'] = 'Chebyshev'
|
|
output['temperature-range'] = FlowList([self.Tmin, self.Tmax])
|
|
output['pressure-range'] = FlowList(['{0} {1}'.format(*self.Pmin),
|
|
'{0} {1}'.format(*self.Pmax)])
|
|
if self.quantity_units is not None:
|
|
output['units'] = FlowMap([('quantity', self.quantity_units)])
|
|
output['data'] = [FlowList(float(v) for v in row) for row in self.coeffs]
|
|
|
|
|
|
class ThreeBody(KineticsModel):
|
|
"""
|
|
A rate calculated for a reaction which includes a third-body collider.
|
|
See https://cantera.org/science/reactions.html#three-body-reactions
|
|
|
|
:param high_rate:
|
|
The Arrhenius kinetics (high-pressure limit)
|
|
:param efficiencies:
|
|
A mapping of species names to collider efficiencies
|
|
"""
|
|
pressure_dependent = True
|
|
|
|
def __init__(self, high_rate=None, efficiencies=None, **kwargs):
|
|
KineticsModel.__init__(self, **kwargs)
|
|
self.high_rate = high_rate
|
|
self.efficiencies = efficiencies or {}
|
|
|
|
def reaction_string_suffix(self, species):
|
|
return ' + M'
|
|
|
|
def reduce(self, output):
|
|
output['type'] = 'three-body'
|
|
output['rate-constant'] = self.high_rate.as_yaml()
|
|
if self.high_rate.A[0] < 0:
|
|
output['negative-A'] = True
|
|
if self.efficiencies:
|
|
output['efficiencies'] = FlowMap(self.efficiencies)
|
|
|
|
|
|
class Falloff(ThreeBody):
|
|
"""
|
|
A rate for a pressure-dependent falloff reaction.
|
|
See https://cantera.org/science/reactions.html#falloff-reactions
|
|
|
|
:param low_rate:
|
|
The Arrhenius kinetics at the low-pressure limit
|
|
:param high_rate:
|
|
The Arrhenius kinetics at the high-pressure limit
|
|
:param efficiencies:
|
|
A mapping of species names to collider efficiencies
|
|
:param F:
|
|
Falloff function parameterization
|
|
"""
|
|
def __init__(self, low_rate=None, F=None, **kwargs):
|
|
ThreeBody.__init__(self, **kwargs)
|
|
self.low_rate = low_rate
|
|
self.F = F
|
|
|
|
def reaction_string_suffix(self, species):
|
|
return ' (+{})'.format(species)
|
|
|
|
def reduce(self, output):
|
|
output['type'] = 'falloff'
|
|
output['low-P-rate-constant'] = self.low_rate.as_yaml()
|
|
output['high-P-rate-constant'] = self.high_rate.as_yaml()
|
|
if self.high_rate.A[0] < 0 and self.low_rate.A[0] < 0:
|
|
output['negative-A'] = True
|
|
if self.F:
|
|
self.F.reduce(output)
|
|
if self.efficiencies:
|
|
output['efficiencies'] = FlowMap(self.efficiencies)
|
|
|
|
|
|
class ChemicallyActivated(ThreeBody):
|
|
"""
|
|
A rate for a chemically-activated reaction.
|
|
See https://cantera.org/science/reactions.html#chemically-activated-reactions
|
|
|
|
:param low_rate:
|
|
The Arrhenius kinetics at the low-pressure limit
|
|
:param high_rate:
|
|
The Arrhenius kinetics at the high-pressure limit
|
|
:param efficiencies:
|
|
A mapping of species names to collider efficiencies
|
|
:param F:
|
|
Falloff function parameterization
|
|
"""
|
|
def __init__(self, low_rate=None, F=None, **kwargs):
|
|
ThreeBody.__init__(self, **kwargs)
|
|
self.low_rate = low_rate
|
|
self.F = F
|
|
|
|
def reaction_string_suffix(self, species):
|
|
return ' (+{})'.format(species)
|
|
|
|
def reduce(self, output):
|
|
output['type'] = 'chemically-activated'
|
|
output['low-P-rate-constant'] = self.low_rate.as_yaml()
|
|
output['high-P-rate-constant'] = self.high_rate.as_yaml()
|
|
if self.high_rate.A[0] < 0 and self.low_rate.A[0] < 0:
|
|
output['negative-A'] = True
|
|
if self.F:
|
|
self.F.reduce(output)
|
|
if self.efficiencies:
|
|
output['efficiencies'] = FlowMap(self.efficiencies)
|
|
|
|
|
|
class Troe:
|
|
"""
|
|
The Troe falloff function, described with either 3 or 4 parameters.
|
|
See https://cantera.org/science/reactions.html#the-troe-falloff-function
|
|
"""
|
|
def __init__(self, A=0.0, T3=0.0, T1=0.0, T2=None):
|
|
self.A = A
|
|
self.T3 = T3
|
|
self.T1 = T1
|
|
self.T2 = T2
|
|
|
|
def reduce(self, output):
|
|
troe = FlowMap([('A', self.A), ('T3', self.T3), ('T1', self.T1)])
|
|
if self.T2:
|
|
troe['T2'] = self.T2
|
|
output['Troe'] = troe
|
|
|
|
|
|
class Sri:
|
|
"""
|
|
The SRI falloff function, described with either 3 or 5 parameters.
|
|
See https://cantera.org/science/reactions.html#the-sri-falloff-function
|
|
"""
|
|
def __init__(self, *, A, B, C, D=None, E=None):
|
|
self.A = A
|
|
self.B = B
|
|
self.C = C
|
|
self.D = D
|
|
self.E = E
|
|
|
|
def reduce(self, output):
|
|
sri = FlowMap([('A', self.A), ('B', self.B), ('C', self.C)])
|
|
if self.D:
|
|
sri['D'] = self.D
|
|
if self.E:
|
|
sri['E'] = self.E
|
|
|
|
output['SRI'] = sri
|
|
|
|
|
|
class TransportData:
|
|
geometry_flags = ['atom', 'linear', 'nonlinear']
|
|
|
|
def __init__(self, label, geometry, well_depth, collision_diameter,
|
|
dipole_moment, polarizability, z_rot, note=''):
|
|
|
|
try:
|
|
geometry = int(geometry)
|
|
except ValueError:
|
|
raise InputError(
|
|
"Bad geometry flag '{}' for species '{}', is the flag a float "
|
|
"or character? It should be an integer.", geometry, label)
|
|
if geometry not in (0, 1, 2):
|
|
raise InputError("Bad geometry flag '{}' for species '{}'",
|
|
geometry, label)
|
|
|
|
self.geometry = self.geometry_flags[int(geometry)]
|
|
self.well_depth = float(well_depth)
|
|
self.collision_diameter = float(collision_diameter)
|
|
self.dipole_moment = float(dipole_moment)
|
|
self.polarizability = float(polarizability)
|
|
self.z_rot = float(z_rot)
|
|
self.note = note.strip()
|
|
|
|
@classmethod
|
|
def to_yaml(cls, representer, node):
|
|
out = BlockMap([('model', 'gas'),
|
|
('geometry', node.geometry),
|
|
('well-depth', node.well_depth),
|
|
('diameter', node.collision_diameter)])
|
|
if node.dipole_moment:
|
|
out['dipole'] = node.dipole_moment
|
|
if node.polarizability:
|
|
out['polarizability'] = node.polarizability
|
|
if node.z_rot:
|
|
out['rotational-relaxation'] = node.z_rot
|
|
if node.note:
|
|
out['note'] = node.note
|
|
return representer.represent_dict(out)
|
|
|
|
|
|
def fortFloat(s):
|
|
"""
|
|
Convert a string representation of a floating point value to a float,
|
|
allowing for some of the peculiarities of allowable Fortran representations.
|
|
"""
|
|
return float(s.strip().lower().replace('d', 'e').replace('e ', 'e+'))
|
|
|
|
|
|
def get_index(seq, value):
|
|
"""
|
|
Find the first location in *seq* which contains a case-insensitive,
|
|
whitespace-insensitive match for *value*. Returns *None* if no match is
|
|
found.
|
|
"""
|
|
if isinstance(seq, str):
|
|
seq = seq.split()
|
|
value = value.lower().strip()
|
|
for i, item in enumerate(seq):
|
|
if item.lower() == value:
|
|
return i
|
|
return None
|
|
|
|
|
|
def contains(seq, value):
|
|
if isinstance(seq, str):
|
|
return value.lower() in seq.lower()
|
|
else:
|
|
return get_index(seq, value) is not None
|
|
|
|
|
|
class Surface:
|
|
def __init__(self, name, site_density):
|
|
self.name = name
|
|
self.site_density = site_density
|
|
self.species_list = []
|
|
self.reactions = []
|
|
|
|
|
|
class Parser:
|
|
def __init__(self):
|
|
self.processed_units = False
|
|
self.energy_units = 'cal/mol' # for the current REACTIONS section
|
|
self.output_energy_units = 'cal/mol' # for the output file
|
|
self.quantity_units = 'mol' # for the current REACTIONS section
|
|
self.output_quantity_units = 'mol' # for the output file
|
|
self.motz_wise = None
|
|
self.warning_as_error = True
|
|
|
|
self.elements = []
|
|
self.element_weights = {} # for custom elements only
|
|
self.species_list = [] # bulk species only
|
|
self.species_dict = {} # bulk and surface species
|
|
self.surfaces = []
|
|
self.reactions = []
|
|
self.final_reaction_comment = ''
|
|
self.headerLines = []
|
|
self.files = [] # input file names
|
|
|
|
def warn(self, message):
|
|
if self.warning_as_error:
|
|
raise InputError(message)
|
|
else:
|
|
logging.warning(message)
|
|
|
|
@staticmethod
|
|
def parse_composition(elements, nElements, width):
|
|
"""
|
|
Parse the elemental composition from a 7 or 9 coefficient NASA polynomial
|
|
entry.
|
|
"""
|
|
composition = {}
|
|
for i in range(nElements):
|
|
symbol = elements[width*i:width*i+2].strip()
|
|
count = elements[width*i+2:width*i+width].strip()
|
|
if not symbol:
|
|
continue
|
|
try:
|
|
# Convert to float first for cases where ``count`` is a string
|
|
# like "2.00".
|
|
count = int(float(count))
|
|
if count:
|
|
composition[symbol.capitalize()] = count
|
|
except ValueError:
|
|
pass
|
|
return composition
|
|
|
|
@staticmethod
|
|
def get_rate_constant_units(length_dims, length_units, quantity_dims,
|
|
quantity_units, time_dims=1, time_units='s'):
|
|
|
|
units = ''
|
|
if length_dims:
|
|
units += length_units
|
|
if length_dims > 1:
|
|
units += '^' + str(length_dims)
|
|
if quantity_dims:
|
|
units += '/' + quantity_units
|
|
if quantity_dims > 1:
|
|
units += '^' + str(quantity_dims)
|
|
if time_dims:
|
|
units += '/' + time_units
|
|
if time_dims > 1:
|
|
units += '^' + str(time_dims)
|
|
if units.startswith('/'):
|
|
units = '1' + units
|
|
return units
|
|
|
|
def add_element(self, element_string):
|
|
if '/' in element_string:
|
|
name, weight, _ = element_string.split('/')
|
|
weight = fortFloat(weight)
|
|
name = name.capitalize()
|
|
self.elements.append(name)
|
|
self.element_weights[name] = weight
|
|
else:
|
|
self.elements.append(element_string.capitalize())
|
|
|
|
def read_NASA7_entry(self, lines, TintDefault, comments):
|
|
"""
|
|
Read a thermodynamics entry for one species in a Chemkin-format file
|
|
(consisting of two 7-coefficient NASA polynomials). Returns the label of
|
|
the species, the thermodynamics model as a :class:`Nasa7` object, and
|
|
the elemental composition of the species.
|
|
|
|
For more details on this format, see `Debugging common errors in CK files
|
|
<https://cantera.org/tutorials/ck2cti-tutorial.html#debugging-common-errors-in-ck-files>`__.
|
|
"""
|
|
identifier = lines[0][0:24].split()
|
|
species = identifier[0].strip()
|
|
|
|
if len(identifier) > 1:
|
|
note = ''.join(identifier[1:]).strip()
|
|
else:
|
|
note = ''
|
|
|
|
comments = '\n'.join(c.rstrip() for c in comments if c.strip())
|
|
if comments and note:
|
|
note = '\n'.join((note, comments))
|
|
elif comments:
|
|
note = comments
|
|
|
|
# Normal method for specifying the elemental composition
|
|
composition = self.parse_composition(lines[0][24:44], 4, 5)
|
|
|
|
# Chemkin-style extended elemental composition: additional lines
|
|
# indicated by '&' continuation character on preceding lines. Element
|
|
# names and abundances are separated by whitespace (not fixed width)
|
|
if lines[0].rstrip().endswith('&'):
|
|
complines = []
|
|
for i in range(len(lines)-1):
|
|
if lines[i].rstrip().endswith('&'):
|
|
complines.append(lines[i+1])
|
|
else:
|
|
break
|
|
lines = [lines[0]] + lines[i+1:]
|
|
comp = ' '.join(line.rstrip('&\n') for line in complines).split()
|
|
composition = {}
|
|
for i in range(0, len(comp), 2):
|
|
composition[comp[i].capitalize()] = int(comp[i+1])
|
|
|
|
# Non-standard extended elemental composition data may be located beyond
|
|
# column 80 on the first line of the thermo entry
|
|
if len(lines[0]) > 80:
|
|
elements = lines[0][80:]
|
|
composition2 = self.parse_composition(elements, len(elements)//10, 10)
|
|
composition.update(composition2)
|
|
|
|
if not composition:
|
|
raise InputError("Error parsing elemental composition for "
|
|
"species '{}'", species)
|
|
|
|
# Extract the NASA polynomial coefficients
|
|
# Remember that the high-T polynomial comes first!
|
|
Tmin = fortFloat(lines[0][45:55])
|
|
Tmax = fortFloat(lines[0][55:65])
|
|
try:
|
|
Tint = fortFloat(lines[0][65:75])
|
|
except ValueError:
|
|
Tint = TintDefault
|
|
|
|
high_coeffs = [fortFloat(lines[i][j:k])
|
|
for i,j,k in [(1,0,15), (1,15,30), (1,30,45), (1,45,60),
|
|
(1,60,75), (2,0,15), (2,15,30)]]
|
|
low_coeffs = [fortFloat(lines[i][j:k])
|
|
for i,j,k in [(2,30,45), (2,45,60), (2,60,75), (3,0,15),
|
|
(3,15,30), (3,30,45), (3,45,60)]]
|
|
|
|
# Duplicate the valid set of coefficients if only one range is provided
|
|
if all(c == 0 for c in low_coeffs) and Tmin == Tint:
|
|
low_coeffs = high_coeffs
|
|
elif all(c == 0 for c in high_coeffs) and Tmax == Tint:
|
|
high_coeffs = low_coeffs
|
|
|
|
# Construct and return the thermodynamics model
|
|
thermo = Nasa7(Tmin=Tmin, Tmax=Tmax, Tmid=Tint,
|
|
low_coeffs=low_coeffs, high_coeffs=high_coeffs,
|
|
note=note)
|
|
|
|
return species, thermo, composition
|
|
|
|
def read_NASA9_entry(self, entry, comments):
|
|
"""
|
|
Read a thermodynamics ``entry`` for one species given as one or more
|
|
9-coefficient NASA polynomials, written in the format described in
|
|
Appendix A of NASA Reference Publication 1311 (McBride and Gordon, 1996).
|
|
Returns the label of the species, the thermodynamics model as a
|
|
:class:`Nasa9` object, and the elemental composition of the species
|
|
"""
|
|
tokens = entry[0].split()
|
|
species = tokens[0]
|
|
note = ' '.join(tokens[1:])
|
|
N = int(entry[1][:2])
|
|
note2 = entry[1][3:9].strip()
|
|
if note and note2:
|
|
note = '{0} [{1}]'.format(note, note2)
|
|
elif note2:
|
|
note = note2
|
|
|
|
comments = '\n'.join(c.rstrip() for c in comments if c.strip())
|
|
if comments and note:
|
|
note = '\n'.join((note, comments))
|
|
elif comments:
|
|
note = comments
|
|
|
|
composition = self.parse_composition(entry[1][10:50], 5, 8)
|
|
|
|
polys = []
|
|
try:
|
|
for i in range(N):
|
|
A, B, C = entry[2+3*i:2+3*(i+1)]
|
|
Trange = [fortFloat(A[1:11]), fortFloat(A[11:21])]
|
|
coeffs = [fortFloat(B[0:16]), fortFloat(B[16:32]),
|
|
fortFloat(B[32:48]), fortFloat(B[48:64]),
|
|
fortFloat(B[64:80]), fortFloat(C[0:16]),
|
|
fortFloat(C[16:32]), fortFloat(C[48:64]),
|
|
fortFloat(C[64:80])]
|
|
polys.append((Trange, coeffs))
|
|
except (IndexError, ValueError) as err:
|
|
raise InputError('Error while reading thermo entry for species {}:\n{}',
|
|
species, err)
|
|
|
|
thermo = Nasa9(data=polys, note=note)
|
|
|
|
return species, thermo, composition
|
|
|
|
def setup_kinetics(self):
|
|
# We look for species including the next permissible character. '\n' is
|
|
# appended to the reaction string to identify the last species in the
|
|
# reaction string. Checking this character is necessary to correctly
|
|
# identify species with names ending in '+' or '='.
|
|
self.species_tokens = set()
|
|
for next_char in ('<', '=', '(', '+', '\n'):
|
|
self.species_tokens.update(k + next_char for k in self.species_dict)
|
|
self.other_tokens = {'M': 'third-body', 'm': 'third-body',
|
|
'(+M)': 'falloff3b', '(+m)': 'falloff3b',
|
|
'<=>': 'equal', '=>': 'equal', '=': 'equal',
|
|
'HV': 'photon', 'hv': 'photon'}
|
|
self.other_tokens.update(('(+{})'.format(k), 'falloff3b: {}'.format(k))
|
|
for k in self.species_dict)
|
|
self.Slen = max(map(len, self.other_tokens))
|
|
|
|
def read_kinetics_entry(self, entry, surface):
|
|
"""
|
|
Read a kinetics ``entry`` for a single reaction as loaded from a
|
|
Chemkin-format file. Returns a :class:`Reaction` object with the
|
|
reaction and its associated kinetics.
|
|
"""
|
|
|
|
# Handle non-default units which apply to this entry
|
|
energy_units = self.energy_units
|
|
quantity_units = self.quantity_units
|
|
if 'units' in entry.lower():
|
|
for units in sorted(QUANTITY_UNITS, key=lambda k: -len(k)):
|
|
pattern = re.compile(r'units *\/ *{} *\/'.format(re.escape(units)),
|
|
flags=re.IGNORECASE)
|
|
m = pattern.search(entry)
|
|
if m:
|
|
entry = pattern.sub('', entry)
|
|
quantity_units = QUANTITY_UNITS[units]
|
|
break
|
|
|
|
for units in sorted(ENERGY_UNITS, key=lambda k: -len(k)):
|
|
pattern = re.compile(r'units *\/ *{} *\/'.format(re.escape(units)),
|
|
re.IGNORECASE)
|
|
m = pattern.search(entry)
|
|
if m:
|
|
entry = pattern.sub('', entry)
|
|
energy_units = ENERGY_UNITS[units]
|
|
break
|
|
|
|
lines = entry.strip().splitlines()
|
|
|
|
# The first line contains the reaction equation and a set of modified Arrhenius parameters
|
|
tokens = lines[0].split()
|
|
A = float(tokens[-3])
|
|
b = float(tokens[-2])
|
|
Ea = float(tokens[-1])
|
|
reaction = ''.join(tokens[:-3]) + '\n'
|
|
original_reaction = reaction # for use in error messages
|
|
|
|
# Identify tokens in the reaction expression in order of
|
|
# decreasing length
|
|
locs = {}
|
|
for i in range(self.Slen, 0, -1):
|
|
for j in range(len(reaction)-i+1):
|
|
test = reaction[j:j+i]
|
|
if test in self.species_tokens:
|
|
reaction = reaction[:j] + ' '*(i-1) + reaction[j+i-1:]
|
|
locs[j] = test[:-1], 'species'
|
|
elif test in self.other_tokens:
|
|
reaction = reaction[:j] + '\n'*i + reaction[j+i:]
|
|
locs[j] = test, self.other_tokens[test]
|
|
|
|
# Anything that's left should be a stoichiometric coefficient or a '+'
|
|
# between species
|
|
for token in reaction.split():
|
|
j = reaction.find(token)
|
|
i = len(token)
|
|
reaction = reaction[:j] + ' '*i + reaction[j+i:]
|
|
if token == '+':
|
|
continue
|
|
|
|
try:
|
|
locs[j] = int(token), 'coeff'
|
|
except ValueError:
|
|
try:
|
|
locs[j] = float(token), 'coeff'
|
|
except ValueError:
|
|
raise InputError('Unexpected token "{}" in reaction expression "{}".',
|
|
token, original_reaction)
|
|
|
|
reactants = []
|
|
products = []
|
|
stoichiometry = 1
|
|
lhs = True
|
|
for token, kind in [v for k,v in sorted(locs.items())]:
|
|
if kind == 'equal':
|
|
reversible = token in ('<=>', '=')
|
|
lhs = False
|
|
elif kind == 'coeff':
|
|
stoichiometry = token
|
|
elif lhs:
|
|
reactants.append((stoichiometry, token, kind))
|
|
stoichiometry = 1
|
|
else:
|
|
products.append((stoichiometry, token, kind))
|
|
stoichiometry = 1
|
|
|
|
if lhs:
|
|
raise InputError("Failed to find reactant/product delimiter in reaction string.")
|
|
|
|
# Create a new Reaction object for this reaction
|
|
reaction = Reaction(reactants=[], products=[], reversible=reversible,
|
|
parser=self)
|
|
|
|
def parse_expression(expression, dest):
|
|
falloff3b = None
|
|
third_body = False # simple third body reaction (non-falloff)
|
|
photon = False
|
|
for stoichiometry, species, kind in expression:
|
|
if kind == 'third-body':
|
|
third_body = True
|
|
elif kind == 'falloff3b':
|
|
falloff3b = 'M'
|
|
elif kind.startswith('falloff3b:'):
|
|
falloff3b = kind.split()[1]
|
|
elif kind == 'photon':
|
|
photon = True
|
|
else:
|
|
dest.append((stoichiometry, self.species_dict[species]))
|
|
|
|
return falloff3b, third_body, photon
|
|
|
|
falloff_3b_r, third_body, photon_r = parse_expression(reactants, reaction.reactants)
|
|
falloff_3b_p, third_body, photon_p = parse_expression(products, reaction.products)
|
|
|
|
if falloff_3b_r != falloff_3b_p:
|
|
raise InputError('Third bodies do not match: "{}" and "{}" in'
|
|
' reaction entry:\n\n{}', falloff_3b_r, falloff_3b_p, entry)
|
|
|
|
if photon_r:
|
|
raise InputError('Reactant photon not supported. '
|
|
'Found in reaction:\n{}', entry.strip())
|
|
if photon_p and reversible:
|
|
self.warn('Found reversible reaction containing a product photon:'
|
|
'\n{0}\nIf the "--permissive" option was specified, this will '
|
|
'be converted to an irreversible reaction with the photon '
|
|
'removed.'.format(entry.strip()))
|
|
reaction.reversible = False
|
|
|
|
reaction.third_body = falloff_3b_r
|
|
|
|
# Determine the appropriate units for k(T) and k(T,P) based on the number of reactants
|
|
# This assumes elementary kinetics for all reactions
|
|
rStoich = sum(r[0] for r in reaction.reactants) + (1 if third_body else 0)
|
|
if rStoich < 1:
|
|
raise InputError('No reactant species for reaction {}.', reaction)
|
|
|
|
length_dim = 3 * (rStoich - 1)
|
|
quantity_dim = rStoich - 1
|
|
kunits = self.get_rate_constant_units(length_dim, 'cm',
|
|
quantity_dim, quantity_units)
|
|
klow_units = self.get_rate_constant_units(length_dim + 3, 'cm',
|
|
quantity_dim + 1, quantity_units)
|
|
|
|
# The rest of the first line contains Arrhenius parameters
|
|
arrhenius = Arrhenius(
|
|
A=(A, kunits),
|
|
b=b,
|
|
Ea=(Ea, energy_units),
|
|
parser=self
|
|
)
|
|
|
|
low_rate = None
|
|
high_rate = None
|
|
falloff = None
|
|
pdep_arrhenius = []
|
|
efficiencies = {}
|
|
coverages = []
|
|
cheb_coeffs = []
|
|
revReaction = None
|
|
is_sticking = None
|
|
motz_wise = None
|
|
Tmin = Tmax = Pmin = Pmax = None # Chebyshev parameters
|
|
degreeT = degreeP = None
|
|
|
|
# Note that the subsequent lines could be in any order
|
|
for line in lines[1:]:
|
|
if not line.strip():
|
|
continue
|
|
tokens = line.split('/')
|
|
parsed = False
|
|
|
|
if 'stick' in line.lower():
|
|
parsed = True
|
|
is_sticking = True
|
|
|
|
if 'mwon' in line.lower():
|
|
parsed = True
|
|
motz_wise = True
|
|
|
|
if 'mwoff' in line.lower():
|
|
parsed = True
|
|
motz_wise = False
|
|
|
|
if 'dup' in line.lower():
|
|
# Duplicate reaction
|
|
parsed = True
|
|
reaction.duplicate = True
|
|
|
|
if 'low' in line.lower():
|
|
# Low-pressure-limit Arrhenius parameters for "falloff" reaction
|
|
parsed = True
|
|
tokens = tokens[1].split()
|
|
low_rate = Arrhenius(
|
|
A=(float(tokens[0].strip()), klow_units),
|
|
b=float(tokens[1].strip()),
|
|
Ea=(float(tokens[2].strip()), energy_units),
|
|
parser=self
|
|
)
|
|
|
|
elif 'high' in line.lower():
|
|
# High-pressure-limit Arrhenius parameters for "chemically
|
|
# activated" reaction
|
|
parsed = True
|
|
tokens = tokens[1].split()
|
|
high_rate = Arrhenius(
|
|
A=(float(tokens[0].strip()), kunits),
|
|
b=float(tokens[1].strip()),
|
|
Ea=(float(tokens[2].strip()), energy_units),
|
|
parser=self
|
|
)
|
|
# Need to fix units on the base reaction:
|
|
arrhenius.A = (arrhenius.A[0], klow_units)
|
|
|
|
elif 'rev' in line.lower():
|
|
parsed = True
|
|
reaction.reversible = False
|
|
tokens = tokens[1].split()
|
|
# If the A factor in the rev line is zero, don't create the reverse reaction
|
|
if float(tokens[0].strip()) != 0.0:
|
|
# Create a reaction proceeding in the opposite direction
|
|
revReaction = Reaction(reactants=reaction.products,
|
|
products=reaction.reactants,
|
|
third_body=reaction.third_body,
|
|
reversible=False,
|
|
parser=self)
|
|
|
|
rev_rate = Arrhenius(
|
|
A=(float(tokens[0].strip()), klow_units),
|
|
b=float(tokens[1].strip()),
|
|
Ea=(float(tokens[2].strip()), energy_units),
|
|
parser=self
|
|
)
|
|
if third_body:
|
|
revReaction.kinetics = ThreeBody(rev_rate)
|
|
else:
|
|
revReaction.kinetics = ElementaryRate(rev_rate)
|
|
|
|
elif 'ford' in line.lower():
|
|
parsed = True
|
|
tokens = tokens[1].split()
|
|
reaction.forward_orders[tokens[0].strip()] = float(tokens[1])
|
|
|
|
elif 'troe' in line.lower():
|
|
# Troe falloff parameters
|
|
parsed = True
|
|
tokens = tokens[1].split()
|
|
falloff = Troe(A=float(tokens[0].strip()),
|
|
T3=float(tokens[1].strip()),
|
|
T1=float(tokens[2].strip()),
|
|
T2=float(tokens[3].strip()) if len(tokens) > 3 else None)
|
|
elif 'sri' in line.lower():
|
|
# SRI falloff parameters
|
|
parsed = True
|
|
tokens = tokens[1].split()
|
|
A = float(tokens[0].strip())
|
|
B = float(tokens[1].strip())
|
|
C = float(tokens[2].strip())
|
|
try:
|
|
D = float(tokens[3].strip())
|
|
E = float(tokens[4].strip())
|
|
except (IndexError, ValueError):
|
|
D = None
|
|
E = None
|
|
|
|
if D is None or E is None:
|
|
falloff = Sri(A=A, B=B, C=C)
|
|
else:
|
|
falloff = Sri(A=A, B=B, C=C, D=D, E=E)
|
|
|
|
elif 'cov' in line.lower():
|
|
parsed = True
|
|
C = tokens[1].split()
|
|
coverages.append(
|
|
[C[0], fortFloat(C[1]), fortFloat(C[2]), fortFloat(C[3])])
|
|
|
|
elif 'cheb' in line.lower():
|
|
# Chebyshev parameters
|
|
parsed = True
|
|
tokens = [t.strip() for t in tokens]
|
|
if contains(tokens, 'TCHEB'):
|
|
index = get_index(tokens, 'TCHEB')
|
|
tokens2 = tokens[index+1].split()
|
|
Tmin = float(tokens2[0].strip())
|
|
Tmax = float(tokens2[1].strip())
|
|
if contains(tokens, 'PCHEB'):
|
|
index = get_index(tokens, 'PCHEB')
|
|
tokens2 = tokens[index+1].split()
|
|
Pmin = (float(tokens2[0].strip()), 'atm')
|
|
Pmax = (float(tokens2[1].strip()), 'atm')
|
|
if contains(tokens, 'TCHEB') or contains(tokens, 'PCHEB'):
|
|
pass
|
|
elif degreeT is None or degreeP is None:
|
|
tokens2 = tokens[1].split()
|
|
degreeT = int(float(tokens2[0].strip()))
|
|
degreeP = int(float(tokens2[1].strip()))
|
|
cheb_coeffs.extend([float(t.strip()) for t in tokens2[2:]])
|
|
else:
|
|
tokens2 = tokens[1].split()
|
|
cheb_coeffs.extend([float(t.strip()) for t in tokens2])
|
|
|
|
elif 'plog' in line.lower():
|
|
# Pressure-dependent Arrhenius parameters
|
|
parsed = True
|
|
tokens = tokens[1].split()
|
|
pdep_arrhenius.append([float(tokens[0].strip()), Arrhenius(
|
|
A=(float(tokens[1].strip()), kunits),
|
|
b=float(tokens[2].strip()),
|
|
Ea=(float(tokens[3].strip()), energy_units),
|
|
parser=self
|
|
)])
|
|
elif len(tokens) >= 2:
|
|
# Assume a list of collider efficiencies
|
|
parsed = True
|
|
for collider, efficiency in zip(tokens[0::2], tokens[1::2]):
|
|
efficiencies[collider.strip()] = float(efficiency.strip())
|
|
|
|
if not parsed:
|
|
raise InputError('Unparsable line:\n"""\n{}\n"""', line)
|
|
|
|
# Decide which kinetics to keep and store them on the reaction object.
|
|
# At most one of the special cases should be true
|
|
tests = [cheb_coeffs, pdep_arrhenius, low_rate, high_rate, third_body,
|
|
surface]
|
|
if sum(bool(t) for t in tests) > 1:
|
|
raise InputError('Reaction entry contains parameters for more than '
|
|
'one reaction type.')
|
|
|
|
if cheb_coeffs:
|
|
if Tmin is None or Tmax is None:
|
|
raise InputError('Missing TCHEB line for reaction {}', reaction)
|
|
if Pmin is None or Pmax is None:
|
|
raise InputError('Missing PCHEB line for reaction {}', reaction)
|
|
if len(cheb_coeffs) != degreeT * degreeP:
|
|
raise InputError('Incorrect number of Chebyshev coefficients. '
|
|
'Expected {}*{} = {} but got {}', degreeT, degreeP,
|
|
degreeT * degreeP, len(cheb_coeffs))
|
|
if quantity_units == self.quantity_units:
|
|
quantity_units = None
|
|
reaction.kinetics = Chebyshev(
|
|
Tmin=Tmin, Tmax=Tmax, Pmin=Pmin, Pmax=Pmax,
|
|
quantity_units=quantity_units,
|
|
coeffs=np.array(cheb_coeffs, np.float64).reshape((degreeT, degreeP)))
|
|
elif pdep_arrhenius:
|
|
reaction.kinetics = PDepArrhenius(
|
|
pressures=[P for P, arrh in pdep_arrhenius],
|
|
pressure_units="atm",
|
|
arrhenius=[arrh for P, arrh in pdep_arrhenius]
|
|
)
|
|
elif low_rate is not None:
|
|
reaction.kinetics = Falloff(high_rate=arrhenius,
|
|
low_rate=low_rate,
|
|
F=falloff,
|
|
efficiencies=efficiencies)
|
|
elif high_rate is not None:
|
|
reaction.kinetics = ChemicallyActivated(high_rate=high_rate,
|
|
low_rate=arrhenius,
|
|
F=falloff,
|
|
efficiencies=efficiencies)
|
|
elif third_body:
|
|
reaction.kinetics = ThreeBody(high_rate=arrhenius,
|
|
efficiencies=efficiencies)
|
|
elif reaction.third_body:
|
|
raise InputError('Reaction equation implies pressure '
|
|
'dependence but no alternate rate parameters (i.e. HIGH or '
|
|
'LOW) were given for reaction {}', reaction)
|
|
elif surface:
|
|
reaction.kinetics = SurfaceRate(rate=arrhenius,
|
|
coverages=coverages,
|
|
is_sticking=is_sticking,
|
|
motz_wise=motz_wise)
|
|
else:
|
|
reaction.kinetics = ElementaryRate(arrhenius)
|
|
|
|
if revReaction:
|
|
revReaction.duplicate = reaction.duplicate
|
|
revReaction.kinetics.efficiencies = reaction.kinetics.efficiencies
|
|
|
|
return reaction, revReaction
|
|
|
|
def load_chemkin_file(self, path, skip_undeclared_species=True, surface=False):
|
|
"""
|
|
Load a Chemkin-format input file from ``path`` on disk.
|
|
"""
|
|
transportLines = []
|
|
self.line_number = 0
|
|
|
|
with open(path, 'r', errors='ignore') as ck_file:
|
|
|
|
def readline():
|
|
self.line_number += 1
|
|
line = strip_nonascii(ck_file.readline())
|
|
if '!' in line:
|
|
return line.split('!', 1)
|
|
elif line:
|
|
return line, ''
|
|
else:
|
|
return None, None
|
|
|
|
# @TODO: This loop is a bit of a mess, and could probably be cleaned
|
|
# up by refactoring it into a set of methods for processing each
|
|
# input file section.
|
|
line, comment = readline()
|
|
advance = True
|
|
inHeader = True
|
|
while line is not None:
|
|
tokens = line.split() or ['']
|
|
if inHeader and not line.strip():
|
|
self.headerLines.append(comment.rstrip())
|
|
|
|
if tokens[0].upper().startswith('ELEM'):
|
|
inHeader = False
|
|
tokens = tokens[1:]
|
|
while line is not None and get_index(line, 'END') is None:
|
|
# Grudging support for implicit end of section
|
|
start = line.strip().upper().split()
|
|
if start and start[0] in ('SPEC', 'SPECIES'):
|
|
self.warn('"ELEMENTS" section implicitly ended by start of '
|
|
'next section on line {0}.'.format(self.line_number))
|
|
advance = False
|
|
tokens.pop()
|
|
break
|
|
|
|
line, comment = readline()
|
|
# Normalize custom atomic weights
|
|
line = re.sub(r'\s*/\s*([0-9\.EeDd+-]+)\s*/', r'/\1/ ', line)
|
|
tokens.extend(line.split())
|
|
|
|
for token in tokens:
|
|
if token.upper() == 'END':
|
|
break
|
|
self.add_element(token)
|
|
|
|
elif tokens[0].upper().startswith('SPEC'):
|
|
# List of species identifiers
|
|
species = tokens[1:]
|
|
inHeader = False
|
|
comments = {}
|
|
while line is not None and get_index(line, 'END') is None:
|
|
# Grudging support for implicit end of section
|
|
start = line.strip().upper().split()
|
|
if start and start[0] in ('REAC', 'REACTIONS', 'TRAN',
|
|
'TRANSPORT', 'THER', 'THERMO'):
|
|
self.warn('"SPECIES" section implicitly ended by start of '
|
|
'next section on line {0}.'.format(self.line_number))
|
|
advance = False
|
|
species.pop()
|
|
# Fix the case where there THERMO ALL or REAC UNITS
|
|
# ends the species section
|
|
if (species[-1].upper().startswith('THER') or
|
|
species[-1].upper().startswith('REAC')):
|
|
species.pop()
|
|
break
|
|
|
|
line, comment = readline()
|
|
comment = comment.strip()
|
|
line_species = line.split()
|
|
if len(line_species) == 1 and comment:
|
|
comments[line_species[0]] = comment
|
|
species.extend(line_species)
|
|
|
|
for token in species:
|
|
if token.upper() == 'END':
|
|
break
|
|
if token in self.species_dict:
|
|
species = self.species_dict[token]
|
|
self.warn('Found additional declaration of species {}'.format(species))
|
|
else:
|
|
species = Species(label=token)
|
|
if token in comments:
|
|
species.note = comments[token]
|
|
self.species_dict[token] = species
|
|
self.species_list.append(species)
|
|
|
|
elif tokens[0].upper().startswith('SITE'):
|
|
# List of species identifiers for surface species
|
|
if '/' in tokens[0]:
|
|
surf_name = tokens[0].split('/')[1]
|
|
else:
|
|
surf_name = 'surface{}'.format(len(self.surfaces)+1)
|
|
tokens = tokens[1:]
|
|
site_density = None
|
|
for token in tokens[:]:
|
|
if token.upper().startswith('SDEN/'):
|
|
site_density = fortFloat(token.split('/')[1])
|
|
tokens.remove(token)
|
|
|
|
if site_density is None:
|
|
raise InputError('SITE section defined with no site density')
|
|
self.surfaces.append(Surface(name=surf_name,
|
|
site_density=site_density))
|
|
surf = self.surfaces[-1]
|
|
|
|
inHeader = False
|
|
while line is not None and get_index(line, 'END') is None:
|
|
# Grudging support for implicit end of section
|
|
start = line.strip().upper().split()
|
|
if start and start[0] in ('REAC', 'REACTIONS', 'THER',
|
|
'THERMO'):
|
|
self.warn('"SITE" section implicitly ended by start of '
|
|
'next section on line {}.'.format(self.line_number))
|
|
advance = False
|
|
tokens.pop()
|
|
# Fix the case where there THERMO ALL or REAC UNITS
|
|
# ends the species section
|
|
if (tokens[-1].upper().startswith('THER') or
|
|
tokens[-1].upper().startswith('REAC')):
|
|
tokens.pop()
|
|
break
|
|
|
|
line, comment = readline()
|
|
tokens.extend(line.split())
|
|
|
|
for token in tokens:
|
|
if token.upper() == 'END':
|
|
break
|
|
if token.count('/') == 2:
|
|
# species occupies a specific number of sites
|
|
token, sites, _ = token.split('/')
|
|
sites = float(sites)
|
|
else:
|
|
sites = None
|
|
if token in self.species_dict:
|
|
species = self.species_dict[token]
|
|
self.warn('Found additional declaration of species {0}'.format(species))
|
|
else:
|
|
species = Species(label=token, sites=sites)
|
|
self.species_dict[token] = species
|
|
surf.species_list.append(species)
|
|
|
|
elif tokens[0].upper().startswith('THER') and contains(line, 'NASA9'):
|
|
inHeader = False
|
|
entryLength = None
|
|
entry = []
|
|
# Gather comments on lines preceding and within this entry
|
|
comments = []
|
|
while line is not None and get_index(line, 'END') != 0:
|
|
# Grudging support for implicit end of section
|
|
start = line.strip().upper().split()
|
|
if start and start[0] in ('REAC', 'REACTIONS', 'TRAN', 'TRANSPORT'):
|
|
self.warn('"THERMO" section implicitly ended by start of '
|
|
'next section on line {0}.'.format(self.line_number))
|
|
advance = False
|
|
tokens.pop()
|
|
break
|
|
|
|
line, comment = readline()
|
|
comments.append(comment)
|
|
if not line:
|
|
continue
|
|
|
|
if entryLength is None:
|
|
entryLength = 0
|
|
# special case if (redundant) temperature ranges are
|
|
# given as the first line
|
|
try:
|
|
s = line.split()
|
|
float(s[0]), float(s[1]), float(s[2])
|
|
continue
|
|
except (IndexError, ValueError):
|
|
pass
|
|
|
|
entry.append(line)
|
|
if len(entry) == 2:
|
|
entryLength = 2 + 3 * int(line.split()[0])
|
|
|
|
if len(entry) == entryLength:
|
|
label, thermo, comp = self.read_NASA9_entry(entry, comments)
|
|
comments = []
|
|
entry = []
|
|
if label not in self.species_dict:
|
|
if skip_undeclared_species:
|
|
logging.info('Skipping unexpected species "{0}" while reading thermodynamics entry.'.format(label))
|
|
continue
|
|
else:
|
|
# Add a new species entry
|
|
species = Species(label=label)
|
|
self.species_dict[label] = species
|
|
self.species_list.append(species)
|
|
else:
|
|
species = self.species_dict[label]
|
|
|
|
# use the first set of thermo data found
|
|
if species.thermo is not None:
|
|
self.warn('Found additional thermo entry for species {0}. '
|
|
'If --permissive was given, the first entry is used.'.format(label))
|
|
else:
|
|
species.thermo = thermo
|
|
species.composition = comp
|
|
|
|
elif tokens[0].upper().startswith('THER'):
|
|
# List of thermodynamics (hopefully one per species!)
|
|
inHeader = False
|
|
line, comment = readline()
|
|
if line is not None and get_index(line, 'END') is None:
|
|
TintDefault = float(line.split()[1])
|
|
thermo = []
|
|
current = []
|
|
# Gather comments on lines preceding and within this entry
|
|
comments = [comment]
|
|
while line is not None and get_index(line, 'END') != 0:
|
|
# Grudging support for implicit end of section
|
|
start = line.strip().upper().split()
|
|
if start and start[0] in ('REAC', 'REACTIONS', 'TRAN', 'TRANSPORT'):
|
|
self.warn('"THERMO" section implicitly ended by start of '
|
|
'next section on line {0}.'.format(self.line_number))
|
|
advance = False
|
|
tokens.pop()
|
|
break
|
|
|
|
if comment:
|
|
current.append('!'.join((line, comment)))
|
|
else:
|
|
current.append(line)
|
|
if len(line) >= 80 and line[79] in ['1', '2', '3', '4']:
|
|
thermo.append(line)
|
|
if line[79] == '4':
|
|
try:
|
|
label, thermo, comp = self.read_NASA7_entry(thermo, TintDefault, comments)
|
|
except Exception as e:
|
|
error_line_number = self.line_number - len(current) + 1
|
|
error_entry = ''.join(current).rstrip()
|
|
logging.info(
|
|
'Error while reading thermo entry starting on line {0}:\n'
|
|
'"""\n{1}\n"""'.format(error_line_number, error_entry)
|
|
)
|
|
raise
|
|
|
|
if label not in self.species_dict:
|
|
if skip_undeclared_species:
|
|
logging.info('Skipping unexpected species "{0}" while reading thermodynamics entry.'.format(label))
|
|
thermo = []
|
|
line, comment = readline()
|
|
current = []
|
|
comments = [comment]
|
|
continue
|
|
else:
|
|
# Add a new species entry
|
|
species = Species(label=label)
|
|
self.species_dict[label] = species
|
|
self.species_list.append(species)
|
|
else:
|
|
species = self.species_dict[label]
|
|
|
|
# use the first set of thermo data found
|
|
if species.thermo is not None:
|
|
self.warn('Found additional thermo entry for species {0}. '
|
|
'If --permissive was given, the first entry is used.'.format(label))
|
|
else:
|
|
species.thermo = thermo
|
|
species.composition = comp
|
|
|
|
thermo = []
|
|
current = []
|
|
comments = []
|
|
elif thermo and thermo[-1].rstrip().endswith('&'):
|
|
# Include Chemkin-style extended elemental composition
|
|
thermo.append(line)
|
|
line, comment = readline()
|
|
comments.append(comment)
|
|
|
|
elif tokens[0].upper().startswith('REAC'):
|
|
# Reactions section
|
|
inHeader = False
|
|
for token in tokens[1:]:
|
|
token = token.upper()
|
|
if token in ENERGY_UNITS:
|
|
self.energy_units = ENERGY_UNITS[token]
|
|
if not self.processed_units:
|
|
self.output_energy_units = ENERGY_UNITS[token]
|
|
elif token in QUANTITY_UNITS:
|
|
self.quantity_units = QUANTITY_UNITS[token]
|
|
if not self.processed_units:
|
|
self.output_quantity_units = QUANTITY_UNITS[token]
|
|
elif token == 'MWON':
|
|
self.motz_wise = True
|
|
elif token == 'MWOFF':
|
|
self.motz_wise = False
|
|
else:
|
|
raise InputError("Unrecognized token on REACTIONS line, {0!r}", token)
|
|
|
|
self.processed_units = True
|
|
|
|
kineticsList = []
|
|
commentsList = []
|
|
startLines = []
|
|
kinetics = ''
|
|
comments = ''
|
|
|
|
line, comment = readline()
|
|
if surface:
|
|
reactions = self.surfaces[-1].reactions
|
|
else:
|
|
reactions = self.reactions
|
|
while line is not None and get_index(line, 'END') is None:
|
|
# Grudging support for implicit end of section
|
|
start = line.strip().upper().split()
|
|
if start and start[0] in ('TRAN', 'TRANSPORT'):
|
|
self.warn('"REACTIONS" section implicitly ended by start of '
|
|
'next section on line {0}.'.format(self.line_number))
|
|
advance = False
|
|
break
|
|
|
|
lineStartsWithComment = not line and comment
|
|
line = line.rstrip()
|
|
comment = comment.rstrip()
|
|
|
|
if '=' in line and not lineStartsWithComment:
|
|
# Finish previous record
|
|
if comment:
|
|
# End of line comment belongs with this reaction
|
|
comments += comment + '\n'
|
|
comment = ''
|
|
kineticsList.append(kinetics)
|
|
commentsList.append(comments)
|
|
startLines.append(self.line_number)
|
|
kinetics = ''
|
|
comments = ''
|
|
|
|
if line.strip():
|
|
kinetics += line + '\n'
|
|
if comment:
|
|
comments += comment + '\n'
|
|
|
|
line, comment = readline()
|
|
|
|
# Don't forget the last reaction!
|
|
if kinetics.strip() != '':
|
|
kineticsList.append(kinetics)
|
|
commentsList.append(comments)
|
|
|
|
# We don't actually know whether comments belong to the
|
|
# previous or next reaction, but to keep them positioned
|
|
# correctly, we associate them with the next reaction (and
|
|
# keep track of the final trailing comment separately)
|
|
if kineticsList and kineticsList[0] == '':
|
|
kineticsList.pop(0)
|
|
self.final_reaction_comment = commentsList.pop()
|
|
|
|
self.setup_kinetics()
|
|
for kinetics, comment, line_number in zip(kineticsList, commentsList, startLines):
|
|
try:
|
|
reaction, revReaction = self.read_kinetics_entry(kinetics, surface)
|
|
except Exception as e:
|
|
self.line_number = line_number
|
|
logging.info('Error reading reaction starting on '
|
|
'line {0}:\n"""\n{1}\n"""'.format(
|
|
line_number, kinetics.rstrip()))
|
|
raise
|
|
reaction.line_number = line_number
|
|
reaction.comment = comment
|
|
reactions.append(reaction)
|
|
if revReaction is not None:
|
|
revReaction.line_number = line_number
|
|
reactions.append(revReaction)
|
|
|
|
elif tokens[0].upper().startswith('TRAN'):
|
|
inHeader = False
|
|
line, comment = readline()
|
|
transport_start_line = self.line_number
|
|
while line is not None and get_index(line, 'END') is None:
|
|
# Grudging support for implicit end of section
|
|
start = line.strip().upper().split()
|
|
if start and start[0] in ('REAC', 'REACTIONS'):
|
|
self.warn('"TRANSPORT" section implicitly ended by start of '
|
|
'next section on line {0}.'.format(self.line_number))
|
|
advance = False
|
|
tokens.pop()
|
|
break
|
|
|
|
if comment:
|
|
transportLines.append('!'.join((line, comment)))
|
|
else:
|
|
transportLines.append(line)
|
|
line, comment = readline()
|
|
|
|
elif line.strip():
|
|
raise InputError('Section starts with unrecognized keyword',
|
|
'\n"""\n{}\n"""', line.rstrip())
|
|
|
|
if advance:
|
|
line, comment = readline()
|
|
else:
|
|
advance = True
|
|
|
|
self.check_duplicate_reactions()
|
|
|
|
for index, reaction in enumerate(self.reactions):
|
|
reaction.index = index + 1
|
|
|
|
if transportLines:
|
|
self.parse_transport_data(transportLines, path, transport_start_line)
|
|
|
|
def check_duplicate_reactions(self):
|
|
"""
|
|
Check for marked (and unmarked!) duplicate reactions. Raise exception
|
|
for unmarked duplicate reactions.
|
|
|
|
Pressure-independent and pressure-dependent reactions are treated as
|
|
different, so they don't need to be marked as duplicate.
|
|
"""
|
|
possible_duplicates = defaultdict(list)
|
|
for r in self.reactions:
|
|
k = (tuple(r.reactants), tuple(r.products), r.kinetics.pressure_dependent)
|
|
possible_duplicates[k].append(r)
|
|
|
|
for reactions in possible_duplicates.values():
|
|
for r1,r2 in itertools.combinations(reactions, 2):
|
|
if r1.duplicate and r2.duplicate:
|
|
pass # marked duplicate reaction
|
|
elif (r1.third_body.upper() == 'M' and
|
|
r1.kinetics.efficiencies.get(r2.third_body) == 0):
|
|
pass # explicit zero efficiency
|
|
elif (r2.third_body.upper() == 'M' and
|
|
r2.kinetics.efficiencies.get(r1.third_body) == 0):
|
|
pass # explicit zero efficiency
|
|
elif r1.third_body != r2.third_body:
|
|
pass # distinct third bodies
|
|
else:
|
|
raise InputError(
|
|
'Encountered unmarked duplicate reaction {} '
|
|
'(See lines {} and {} of the input file.).',
|
|
r1, r1.line_number, r2.line_number)
|
|
|
|
def parse_transport_data(self, lines, filename, line_offset):
|
|
"""
|
|
Parse the Chemkin-format transport data in ``lines`` (a list of strings)
|
|
and add that transport data to the previously-loaded species.
|
|
"""
|
|
|
|
for i,line in enumerate(lines):
|
|
original_line = line
|
|
line = line.strip()
|
|
if not line or line.startswith('!'):
|
|
continue
|
|
if get_index(line, 'END') == 0:
|
|
break
|
|
|
|
if '!' in line:
|
|
line, comment = line.split('!', 1)
|
|
else:
|
|
comment = ''
|
|
|
|
data = line.split()
|
|
|
|
speciesName = data[0]
|
|
if speciesName in self.species_dict:
|
|
if len(data) != 7:
|
|
raise InputError('Unable to parse line {} of {}:\n"""\n{}"""\n'
|
|
'6 transport parameters expected, but found {}.',
|
|
line_offset + i, filename, original_line, len(data)-1)
|
|
|
|
if self.species_dict[speciesName].transport is None:
|
|
self.species_dict[speciesName].transport = TransportData(*data, note=comment)
|
|
else:
|
|
self.warn('Ignoring duplicate transport data'
|
|
' for species "{}" on line {} of "{}".'.format(
|
|
speciesName, line_offset + i, filename))
|
|
|
|
|
|
def write_yaml(self, name='gas', out_name='mech.yaml'):
|
|
emitter = yaml.YAML()
|
|
emitter.width = 70
|
|
|
|
emitter.register_class(Species)
|
|
emitter.register_class(Nasa7)
|
|
emitter.register_class(Nasa9)
|
|
emitter.register_class(TransportData)
|
|
emitter.register_class(Reaction)
|
|
|
|
with open(out_name, 'w') as dest:
|
|
have_transport = True
|
|
for s in self.species_list:
|
|
if not s.transport:
|
|
have_transport = False
|
|
|
|
surface_names = []
|
|
n_reacting_phases = 0
|
|
if self.reactions:
|
|
n_reacting_phases += 1
|
|
for surf in self.surfaces:
|
|
surface_names.append(surf.name)
|
|
if surf.reactions:
|
|
n_reacting_phases += 1
|
|
|
|
# header from original file
|
|
desc = '\n'.join(line.rstrip() for line in self.headerLines)
|
|
desc = desc.strip('\n')
|
|
desc = textwrap.dedent(desc)
|
|
if desc.strip():
|
|
emitter.dump({'description': yaml.scalarstring.PreservedScalarString(desc)}, dest)
|
|
|
|
# Additional information regarding conversion
|
|
files = [os.path.basename(f) for f in self.files]
|
|
metadata = BlockMap([
|
|
('generator', 'ck2yaml'),
|
|
('input-files', FlowList(files)),
|
|
('cantera-version', '2.5.0a2'),
|
|
('date', formatdate(localtime=True)),
|
|
])
|
|
if desc.strip():
|
|
metadata.yaml_set_comment_before_after_key('generator', before='\n')
|
|
emitter.dump(metadata, dest)
|
|
|
|
units = FlowMap([('length', 'cm'), ('time', 's')])
|
|
units['quantity'] = self.output_quantity_units
|
|
units['activation-energy'] = self.output_energy_units
|
|
units_map = BlockMap([('units', units)])
|
|
units_map.yaml_set_comment_before_after_key('units', before='\n')
|
|
emitter.dump(units_map, dest)
|
|
|
|
phases = []
|
|
reactions = []
|
|
if name is not None:
|
|
phase = BlockMap()
|
|
phase['name'] = name
|
|
phase['thermo'] = 'ideal-gas'
|
|
phase['elements'] = FlowList(self.elements)
|
|
phase['species'] = FlowList(S.label for S in self.species_list)
|
|
if self.reactions:
|
|
phase['kinetics'] = 'gas'
|
|
if n_reacting_phases == 1:
|
|
reactions.append(('reactions', self.reactions))
|
|
else:
|
|
rname = '{}-reactions'.format(name)
|
|
phase['reactions'] = [rname]
|
|
reactions.append((rname, self.reactions))
|
|
if have_transport:
|
|
phase['transport'] = 'mixture-averaged'
|
|
phase['state'] = FlowMap([('T', 300.0), ('P', '1 atm')])
|
|
phases.append(phase)
|
|
|
|
for surf in self.surfaces:
|
|
# Write definitions for surface phases
|
|
phase = BlockMap()
|
|
phase['name'] = surf.name
|
|
phase['thermo'] = 'ideal-surface'
|
|
phase['elements'] = FlowList(self.elements)
|
|
phase['species'] = FlowList(S.label for S in surf.species_list)
|
|
phase['site-density'] = surf.site_density
|
|
if self.motz_wise is not None:
|
|
phase['Motz-Wise'] = self.motz_wise
|
|
if surf.reactions:
|
|
phase['kinetics'] = 'surface'
|
|
if n_reacting_phases == 1:
|
|
reactions.append(('reactions', surf.reactions))
|
|
else:
|
|
rname = '{}-reactions'.format(surf.name)
|
|
phase['reactions'] = [rname]
|
|
reactions.append((rname, surf.reactions))
|
|
phase['state'] = FlowMap([('T', 300.0), ('P', '1 atm')])
|
|
phases.append(phase)
|
|
|
|
if phases:
|
|
phases_map = BlockMap([('phases', phases)])
|
|
phases_map.yaml_set_comment_before_after_key('phases', before='\n')
|
|
emitter.dump(phases_map, dest)
|
|
|
|
# Write data on custom elements
|
|
if self.element_weights:
|
|
elements = []
|
|
for name, weight in sorted(self.element_weights.items()):
|
|
E = BlockMap([('symbol', name), ('atomic-weight', weight)])
|
|
elements.append(E)
|
|
elementsMap = BlockMap([('elements', elements)])
|
|
elementsMap.yaml_set_comment_before_after_key('elements', before='\n')
|
|
emitter.dump(elementsMap, dest)
|
|
|
|
# Write the individual species data
|
|
all_species = list(self.species_list)
|
|
for surf in self.surfaces:
|
|
all_species.extend(surf.species_list)
|
|
speciesMap = BlockMap([('species', all_species)])
|
|
speciesMap.yaml_set_comment_before_after_key('species', before='\n')
|
|
emitter.dump(speciesMap, dest)
|
|
|
|
# Write the reactions section(s)
|
|
for label, R in reactions:
|
|
reactionsMap = BlockMap([(label, R)])
|
|
reactionsMap.yaml_set_comment_before_after_key(label, before='\n')
|
|
emitter.dump(reactionsMap, dest)
|
|
|
|
# Names of surface phases need to be returned so they can be imported as
|
|
# part of mechanism validation
|
|
return surface_names
|
|
|
|
@staticmethod
|
|
def convert_mech(input_file, thermo_file=None, transport_file=None,
|
|
surface_file=None, phase_name='gas', out_name=None,
|
|
quiet=False, permissive=None):
|
|
|
|
parser = Parser()
|
|
if quiet:
|
|
logging.basicConfig(level=logging.ERROR)
|
|
else:
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
if permissive is not None:
|
|
parser.warning_as_error = not permissive
|
|
|
|
if input_file:
|
|
parser.files.append(input_file)
|
|
input_file = os.path.expanduser(input_file)
|
|
if not os.path.exists(input_file):
|
|
raise IOError('Missing input file: {0!r}'.format(input_file))
|
|
try:
|
|
# Read input mechanism files
|
|
parser.load_chemkin_file(input_file)
|
|
except Exception as err:
|
|
logging.warning("\nERROR: Unable to parse '{0}' near line {1}:\n{2}\n".format(
|
|
input_file, parser.line_number, err))
|
|
raise
|
|
else:
|
|
phase_name = None
|
|
|
|
if thermo_file:
|
|
parser.files.append(thermo_file)
|
|
thermo_file = os.path.expanduser(thermo_file)
|
|
if not os.path.exists(thermo_file):
|
|
raise IOError('Missing thermo file: {0!r}'.format(thermo_file))
|
|
try:
|
|
parser.load_chemkin_file(thermo_file,
|
|
skip_undeclared_species=bool(input_file))
|
|
except Exception:
|
|
logging.warning("\nERROR: Unable to parse '{0}' near line {1}:\n".format(
|
|
thermo_file, parser.line_number))
|
|
raise
|
|
|
|
if transport_file:
|
|
parser.files.append(transport_file)
|
|
transport_file = os.path.expanduser(transport_file)
|
|
if not os.path.exists(transport_file):
|
|
raise IOError('Missing transport file: {0!r}'.format(transport_file))
|
|
with open(transport_file, 'r', errors='ignore') as f:
|
|
lines = [strip_nonascii(line) for line in f]
|
|
parser.parse_transport_data(lines, transport_file, 1)
|
|
|
|
# Transport validation: make sure all species have transport data
|
|
for s in parser.species_list:
|
|
if s.transport is None:
|
|
raise InputError("No transport data for species '{}'.", s)
|
|
|
|
if surface_file:
|
|
parser.files.append(surface_file)
|
|
surface_file = os.path.expanduser(surface_file)
|
|
if not os.path.exists(surface_file):
|
|
raise IOError('Missing input file: {0!r}'.format(surface_file))
|
|
try:
|
|
# Read input mechanism files
|
|
parser.load_chemkin_file(surface_file, surface=True)
|
|
except Exception as err:
|
|
logging.warning("\nERROR: Unable to parse '{0}' near line {1}:\n{2}\n".format(
|
|
surface_file, parser.line_number, err))
|
|
raise
|
|
|
|
if out_name:
|
|
out_name = os.path.expanduser(out_name)
|
|
else:
|
|
out_name = os.path.splitext(input_file)[0] + '.yaml'
|
|
|
|
# Write output file
|
|
surface_names = parser.write_yaml(name=phase_name, out_name=out_name)
|
|
if not quiet:
|
|
nReactions = len(parser.reactions) + sum(len(surf.reactions) for surf in parser.surfaces)
|
|
print('Wrote YAML mechanism file to {0!r}.'.format(out_name))
|
|
print('Mechanism contains {0} species and {1} reactions.'.format(len(parser.species_list), nReactions))
|
|
return surface_names
|
|
|
|
|
|
def convert_mech(input_file, thermo_file=None, transport_file=None, surface_file=None,
|
|
phase_name='gas', out_name=None, quiet=False, permissive=None):
|
|
return Parser.convert_mech(input_file, thermo_file, transport_file, surface_file,
|
|
phase_name, out_name, quiet, permissive)
|
|
|
|
def main(argv):
|
|
|
|
longOptions = ['input=', 'thermo=', 'transport=', 'surface=', 'id=',
|
|
'output=', 'permissive', 'help', 'debug', 'quiet',
|
|
'no-validate']
|
|
|
|
try:
|
|
optlist, args = getopt.getopt(argv, 'dh', longOptions)
|
|
options = dict()
|
|
for o,a in optlist:
|
|
options[o] = a
|
|
|
|
if args:
|
|
raise getopt.GetoptError('Unexpected command line option: ' +
|
|
repr(' '.join(args)))
|
|
|
|
except getopt.GetoptError as e:
|
|
print('ck2yaml.py: Error parsing arguments:')
|
|
print(e)
|
|
print('Run "ck2yaml.py --help" to see usage help.')
|
|
sys.exit(1)
|
|
|
|
if not options or '-h' in options or '--help' in options:
|
|
print(__doc__)
|
|
sys.exit(0)
|
|
|
|
input_file = options.get('--input')
|
|
thermo_file = options.get('--thermo')
|
|
permissive = '--permissive' in options
|
|
quiet = '--quiet' in options
|
|
transport_file = options.get('--transport')
|
|
surface_file = options.get('--surface')
|
|
phase_name = options.get('--id', 'gas')
|
|
|
|
if not input_file and not thermo_file:
|
|
print('At least one of the arguments "--input=..." or "--thermo=..."'
|
|
' must be provided.\nRun "ck2yaml.py --help" to see usage help.')
|
|
sys.exit(1)
|
|
|
|
if '--output' in options:
|
|
out_name = options['--output']
|
|
if not out_name.endswith('.yaml') and not out_name.endswith('.yml'):
|
|
out_name += '.yaml'
|
|
elif input_file:
|
|
out_name = os.path.splitext(input_file)[0] + '.yaml'
|
|
else:
|
|
out_name = os.path.splitext(thermo_file)[0] + '.yaml'
|
|
|
|
surfaces = Parser.convert_mech(input_file, thermo_file, transport_file,
|
|
surface_file, phase_name, out_name,
|
|
quiet, permissive)
|
|
|
|
# Do full validation by importing the resulting mechanism
|
|
if not input_file:
|
|
# Can't validate input files that don't define a phase
|
|
return
|
|
|
|
if '--no-validate' in options:
|
|
return
|
|
|
|
try:
|
|
import cantera as ct
|
|
except ImportError:
|
|
print('WARNING: Unable to import Cantera Python module. Output '
|
|
'mechanism has not been validated')
|
|
sys.exit(0)
|
|
|
|
try:
|
|
print('Validating mechanism...', end='')
|
|
gas = ct.Solution(out_name)
|
|
for surf_name in surfaces:
|
|
phase = ct.Interface(out_name, surf_name, [gas])
|
|
print('PASSED.')
|
|
except RuntimeError as e:
|
|
print('FAILED.')
|
|
print(e)
|
|
sys.exit(1)
|
|
|
|
|
|
def script_entry_point():
|
|
main(sys.argv[1:])
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv[1:])
|