[1D/Python] Add adjoint laminar flame speed sensitivity calculation

This is approximately an order of magnitude faster than the 'forward' method for
calculating these sensitivities. It also eliminates the need to adjust the
solver tolerances.
This commit is contained in:
Ray Speth 2016-11-11 22:47:05 -05:00
parent 0bfdf146c0
commit 4753ae4a86
3 changed files with 53 additions and 23 deletions

View file

@ -15,8 +15,6 @@ Tin = 300.0 # unburned gas temperature [K]
reactants = 'CH4:0.45, O2:1.0, N2:3.76'
width = 0.03 # m
tol_ss = [1.0e-9, 1.0e-14] # [rtol atol] for steady-state problem
tol_ts = [1.0e-5, 1.0e-14] # [rtol atol] for time stepping
# IdealGasMix object used to compute mixture properties
gas = ct.Solution('gri30.xml', 'gri30_mix')
@ -24,35 +22,17 @@ gas.TPX = Tin, p, reactants
# Flame object
f = ct.FreeFlame(gas, width=width)
f.flame.set_steady_tolerances(default=tol_ss)
f.flame.set_transient_tolerances(default=tol_ts)
f.set_refine_criteria(ratio=3, slope=0.07, curve=0.14)
f.solve(loglevel=1, auto=True)
Su0 = f.u[0]
print('\nmixture-averaged flamespeed = {:7f} m/s\n'.format(f.u[0]))
print('Initial Solution:')
f.show_stats()
# Perturbation size. This must be large compared to the steady-state relative
# tolerance (tol_ss[0]. Sensitivities less than approximately tol_ss[0] / dk
# are not reliable.
dk = 1e-2
# Use the adjoint method to calculate sensitivities
sens = f.get_flame_speed_reaction_sensitivities()
print()
print('Rxn # k/S*dS/dk Reaction Equation')
print('----- ---------- ----------------------------------')
for m in range(gas.n_reactions):
gas.set_multiplier(1.0) # reset all multipliers
gas.set_multiplier(1+dk, m) # perturb reaction m
f.solve(loglevel=0, refine_grid=False)
Su = f.u[0]
print('{: 5d} {: 10.3e} {}'.format(
m, (Su-Su0)/(Su0*dk), gas.reaction_equation(m)))
# Sensitivity analysis requires additional function evaluations on the final
# grid, but no additional Jacobian evaluations.
print('\nInitial Solution + Sensitivity calculations:')
f.show_stats()
m, sens[m], gas.reaction_equation(m)))

View file

@ -453,6 +453,34 @@ class FreeFlame(FlameBase):
self.set_profile(self.gas.species_name(n),
locs, [Y0[n], Y0[n], Yeq[n], Yeq[n]])
def get_flame_speed_reaction_sensitivities(self):
r"""
Compute the normalized sensitivities of the laminar flame speed
:math:`S_u` with respect to the reaction rate constants :math:`k_i`:
.. math::
s_i = \frac{k_i}{S_u} \frac{dS_u}{dk_i}
"""
def g(sim):
return sim.u[0]
Nvars = sum(D.n_components * D.n_points for D in self.domains)
# Index of u[0] in the global solution vector
i_Su = self.inlet.n_components + self.flame.component_index('u')
dgdx = np.zeros(Nvars)
dgdx[i_Su] = 1
Su0 = g(self)
def perturb(sim, i, dp):
sim.gas.set_multiplier(1+dp, i)
return self.solve_adjoint(perturb, self.gas.n_reactions, dgdx) / Su0
class BurnerFlame(FlameBase):
"""A burner-stabilized flat flame."""

View file

@ -227,6 +227,28 @@ class TestFreeFlame(utilities.CanteraTest):
def test_mixture_averaged_case8(self):
self.run_mix(phi=2.0, T=400, width=2.0, p=5.0, refine=False)
def test_adjoint_sensitivities(self):
self.run_mix(phi=0.5, T=300, width=0.1, p=1.0, refine=True)
self.sim.flame.set_steady_tolerances(default=(1e-10, 1e-15))
self.sim.solve(loglevel=0, refine_grid=False)
# Adjoint sensitivities
dSdk_adj = self.sim.get_flame_speed_reaction_sensitivities()
# Forward sensitivities
dk = 1e-4
Su0 = self.sim.u[0]
for m in range(self.gas.n_reactions):
self.gas.set_multiplier(1.0) # reset all multipliers
self.gas.set_multiplier(1+dk, m) # perturb reaction m
self.sim.solve(loglevel=0, refine_grid=False)
Suplus = self.sim.u[0]
self.gas.set_multiplier(1-dk, m) # perturb reaction m
self.sim.solve(loglevel=0, refine_grid=False)
Suminus = self.sim.u[0]
fwd = (Suplus-Suminus)/(2*Su0*dk)
self.assertNear(fwd, dSdk_adj[m], 5e-3)
# @utilities.unittest.skip('sometimes slow')
def test_multicomponent(self):
reactants = 'H2:1.1, O2:1, AR:5.3'