Cantera / enhancements

Repository for proposed and ongoing enhancements to Cantera
11 stars 5 forks source link

Additional parameters for set_equivalence_ratio #108

Closed corykinney closed 2 years ago

corykinney commented 3 years ago

Abstract

Currently the ThermoPhase.set_equivalence_ratio function includes parameters for phi, fuel, and oxidizer. The proposed change would add optional parameters to increase utility.

Motivation

In shock tube/combustion research it is common to define mixtures using equivalence ratio, fuel concentration, and a corresponding bath gas/diluent. This is very straightforward to do manually with simple mixtures, but with blends of fuels (e.g. natural gas) and oxidizers (e.g. air) a built-in method would be helpful.

Possible Solutions

Add additional parameters

An example of the suggested implementation to make a mixture with a desired fuel concentration would be:

>>> gas.set_equivalence_ratio(1, "CH4", "O2", "Ar", fuel_concentration=0.04)
>>> gas.mole_fraction_dict()
{'CH4': 0.04, 'O2': 0.08, 'Ar': 0.88}

and for a desired bath gas concentration:

>>> gas.set_equivalence_ratio(1, "CH4", "O2", "Ar", bath_concentration=0.70)
>>> gas.mole_fraction_dict()
{'CH4': 0.10, 'O2': 0.20, 'Ar': 0.70}

Don't add anything

If this doesn't seem that it would be useful enough for inclusion, obviously it is possible to do this with the existing function and a bit of extra manipulation:

  1. Call set_equivalence_ratio as is
  2. Get mole fraction dict
  3. Scale mole fractions
  4. Add bath gas to normalize

It would just be useful to have this functionality standard.

ischoegl commented 3 years ago

Hi @corykinney. I believe you can already more or less do this manually, e.g.

gas.set_equivalence_ratio(1, fuel='CH4:1.', oxidizer='O2:0.2, Ar:0.7')

where you specify the composition of the oxidizer (including the diluent) directly.

corykinney commented 3 years ago

@ischoegl you could try to specify the diluent directly in the oxidizer, but you can only give mole fractions relative to the other components of the oxidizer and not relative to the resultant mixture. So while your example works with phi of 1:

>>> gas.set_equivalence_ratio(1, fuel='CH4:1.', oxidizer='O2:0.2, Ar:0.7')
>>> gas.mole_fraction_dict()
{'AR': 0.6999999999999998, 'CH4': 0.1, 'O2': 0.2}

it only happens to work because for an O2 concentration of 0.2, the corresponding CH4 concentration is 0.1, which cancels out the normalization of the oxidizer. If I were to use a different equivalence ratio (or diluent concentration) it would give an unintended result:

>>> gas.set_equivalence_ratio(0.5, fuel='CH4:1.', oxidizer='O2:0.2, Ar:0.7')
>>> gas.mole_fraction_dict()
{'AR': 0.736842105263158, 'CH4': 0.052631578947368425, 'O2': 0.2105263157894737}
ischoegl commented 3 years ago

@corrykinney ... got it. Personally, I'd have two suggestions to tweak your original proposal to

>>> gas.set_equivalence_ratio(0.5, fuel='CH4', oxidizer='O2', diluent='Ar', mole_fraction_diluent=0.7)

as diluent is somewhat more descriptive, and 'concentration' is not sufficiently specific (a corresponding mass_fraction_diluent would also be possible). I also used key-word arguments for clarity.

corykinney commented 3 years ago

@ischoegl that makes sense. I would suppose my revised proposal for the Python function signature would be:

set_equivalence_ratio(phi, fuel, oxidizer, diluent=None, *, fuel_mole_fraction=None, diluent_mole_fraction=None)

Maybe with the fuel_mass_fraction and diluent_mass_fraction as you suggested as well. Where the diluent argument is optional and its use would require a single keyword argument to be specified (since they're all mutually exclusive). This is just a suggested Python function signature since I'm not sure how to write the underlying C++ function.

decaluwe commented 3 years ago

This is just a suggested Python function signature since I'm not sure how to write the underlying C++ function.

One first step (I think) could be to just do the conversion in the Python function to calculate the oxidizer mole fraction that includes the diluent. Since this is mainly a convenience feature to offload the calculation from the user. Then the python function could call the underlying C++, using the same signature as before?

If this is the approach, we'd want to make a parallel change in the Matlab toolbox.

ischoegl commented 3 years ago

I agree that things could be limited to Python. As an alternative, I could also see a new function set_dilution with a signature

set_dilution(diluent, diluent_mole_fraction=None)

This has the advantage that it would keep set_equivalence_ratio simple, but the caveat that specifying fuel_mole_fraction would be more complex.

Regarding MATLAB, I don't think that an implementation is necessary. The MATLAB interface is incomplete as it stands, and (as far as I am aware of) there are some ongoing attempts to replace it by a new approach, making any new feature implementation potentially short-lived.

bryanwweber commented 3 years ago

I have a couple ideas here...

I think the way that CHEMKIN and possibly also CEA do this is by allowing you to specify a set of "extra" species whose mole fractions are known. That would solve the diluent issue, and should be pretty easy to implement. A Python interface like:

set_equivalence_ratio(1.0, 'CH4:1,C3H8:1', 'O2:1,N2:3.76', extra='AR:0.7')

would work nicely, IMO. That would set the mole fraction (by default) of argon to 0.7 and scale everything else accordingly. This should be relatively simple to do in C++ so it can probably be added to every interface.

As far as setting the fuel concentration, that API seems a little trickier to fit in with set_equivalence_ratio(). One option is to allow the extra parameter to include the fuel species, which would set the fuel concentration appropriately. That would require a means to specify the diluent/balance species, perhaps with another keyword argument? Something like

set_equivalence_ratio(1.0, 'CH4:1', 'O2:1,N2:3.76', extra='CH4:0.04', diluent='AR')

or perhaps a specific name like fuel to allow multicomponent mixtures:

set_equivalence_ratio(1.0, 'CH4:1,C3H8:1', 'O2:1,N2:3.76', extra='fuel:0.7', diluent='AR')
corykinney commented 3 years ago

@bryanwweber Yeah I'm not 100% sure how to get the features to fit with the existing set_equivalence_ratio function, and even then the function name wouldn't be fully descriptive of the extra functionality.

If we went the route of using the extra keyword parameter, we would definitely need the specific name fuel as you suggested for multicomponent mixtures. Would it be possible to use an interface like this, or would it be inconsistent with how Cantera code is normally called:

set_equivalence_ratio(1.0, 'CH4: 1', 'O2: 1, N2: 3.76', extra='fuel:0.04, CO2: 0.50, Ar')

with the mole fraction for Ar left out to indicate dilution? With

set_equivalence_ratio(1.0, 'CH4: 1', 'O2: 1, N2: 3.76', extra='CO2, 0.30, Ar: 0.40')

to make a stoichiometric mixture out of the remaining 30%.

bryanwweber commented 3 years ago

Hi @corykinney! I don't understand your first case... The CO2 should have a mole fraction of 0.4, the fuel (in this case methane) at 0.04, o2 and n2 in the air proportion, and with their mole fraction set to the stoichiometric ratio with the fuel, and the balance argon? Honestly, that seems too complicated for this function, and since it's meant to support a particular class of experiments, I'd suggest creating a separate toolbox based on Cantera to do that math.

The second case, though, fits neatly in what I was thinking, in terms of the extra parameter. That's a relatively simple scaling of the existing computation.

corykinney commented 3 years ago

@bryanwweber I can understand that it would be beyond the scope of the function. For my purposes, I'll just have to make some custom functionality for that. But I do think the extra parameter with specified mole fractions would be helpful and more within the scope of the set_equivalence_ratio function!

ischoegl commented 3 years ago

@corykinney and @bryanwweber ... getting the interface right is probably the most difficult aspect of this. To throw yet another suggestion into the mix, what about

set_equivalence_ratio(1.0, fuel="CH4", oxidizer="O2: 1, N2: 3.76", diluent="Ar", 
    fraction="diluent:0.7", basis="mass")

This would just add two additional parameters, and cover all cases (fraction could use fuel, diluent and even oxidizer).

speth commented 3 years ago

I like @ischoegl's last suggestion, as I think it offers a lot of flexibility without adding too many specialized arguments. I'm not sure what the best format for the fraction argument is. The string-based option shown is easy to use, but I think in that case we'd also want to accept a dict like {"diluent": 0.7} as we do for composition strings.

g3bk47 commented 3 years ago

If I understand correctly, with @ischoegl 's interface, the only change (at least in the python interface) would be to add the following code snippet at the end of the current set_equivalence_ratio function (after https://github.com/Cantera/cantera/blob/38bbc66e24a34bd9650a553e2d96994020890aac/interfaces/cython/cantera/thermo.pyx#L779):

if fraction is not None and diluent is not None:

    # parse the fraction argument
    if isinstance(fraction,str):
        if ':' not in fraction:
             raise ValueError("The fraction argument requires a value in the form of e.g. fraction='Ar:0.7'")
        colon_pos = fraction.rfind(":")
        fraction_type  = fraction[:colon_pos]
        fraction_value = float(fraction[colon_pos+1:])
    elif isinstance(fraction,dict):
        fraction_type  = fraction.keys()[0]
        fraction_value = float(fraction.values()[0])
    else:
        raise ValueError("fraction argument must be provided as string or dictionary.")

    # if 'fraction' is specified for diluent, just scale the mass or mole fractions of the fuel/ox mixture accordingly
    if fraction_type == 'diluent':
        if basis == 'mole': # is there a nicer way to avoid code duplication?
            X_fuelox = self.X
            self.X = diluent
            self.X = (1-fraction_value)*X_fuelox + fraction_value*self.X
        else:
            Y_fuelox = self.Y
            self.Y = diluent
            self.Y = (1-fraction_value)*Y_fuelox + fraction_value*self.Y
        return

    if fraction_type not 'fuel' and fraction_type not 'oxidizer':
        raise ValueError("fraction must be related to 'diluent', 'fuel' or 'oxidizer'.")

    # get the mixture fraction before scaling / diluent addition
    Z_fuel = self.mixture_fraction(gas,oxidizer,basis)

    if basis == 'mass': # for mass basis, it is quite straight forward
        if fraction_type == 'fuel':
            Z = Z_fuel
        else:
            Z = 1-Z_fuel
        if fraction_value > Z:
            raise ValueError("fraction cannot be higher than fraction in the mixture.")
        Y_mix = self.Y
        self.Y = diluent
        factor = fraction_value / Z
        self.Y = factor*Y_mix + (1-factor)*self.Y 

    else:
        # convert mass based mixture fraction to molar one, Z = kg fuel / kg mixture
        X_mix = self.X
        M_mix = self.mean_molecular_weight
        self.X = fuel
        M_fuel = self.mean_molecular_weight
        Z_fuel_mole = Z * M_mix / M_fuel          # mol fuel / mol mix
        if fraction_type == 'fuel':
            Z = Z_fuel_mole
        else:
            Z = 1-Z_fuel_mole
        if fraction_value > Z:
            raise ValueError("fraction cannot be higher than fraction in the mixture.")
        self.X = diluent
        factor = fraction_value / Z
        self.X = factor*X_mix + (1-factor)*self.X

if (fraction is None and diluent is not None) or (fraction is not None and diluent is None):
    raise ValueError("if dilution is used, both fraction and diluent parameters are required.")

I didn't test the code above, but conceptionally, this should do the requested calculations, right?

ischoegl commented 2 years ago

Closed via Cantera/cantera#1206