import cantera as ct from . import utilities import numpy as np from .utilities import allow_deprecated import pytest from pytest import approx class TestOnedim(utilities.CanteraTest): def test_instantiate(self): gas = ct.Solution("h2o2.yaml") free = ct.FreeFlow(gas) axi = ct.AxisymmetricFlow(gas) def test_badInstantiate(self): solid = ct.Solution("diamond.yaml", "diamond") with pytest.raises(ct.CanteraError, match="An appropriate transport model\nshould be set when instantiating"): ct.FreeFlow(solid) def test_instantiateSurface(self): gas = ct.Solution("diamond.yaml", "gas") solid = ct.Solution("diamond.yaml", "diamond") interface = ct.Solution("diamond.yaml", "diamond_100", (gas, solid)) surface = ct.ReactingSurface1D(phase=interface) def test_boundaryProperties(self): gas1 = ct.Solution("h2o2.yaml") gas2 = ct.Solution("h2o2.yaml") inlet = ct.Inlet1D(name='something', phase=gas1) flame = ct.FreeFlow(gas1) sim = ct.Sim1D((inlet, flame)) self.assertEqual(inlet.name, 'something') gas2.TPX = 400, 101325, 'H2:0.3, O2:0.5, AR:0.2' Xref = gas2.X Yref = gas2.Y inlet.Y = Yref self.assertArrayNear(inlet.Y, Yref) self.assertArrayNear(inlet.X, Xref) gas2.TPX = 400, 101325, 'H2:0.5, O2:0.2, AR:0.3' Xref = gas2.X Yref = gas2.Y inlet.X = Xref self.assertArrayNear(inlet.X, Xref) self.assertArrayNear(inlet.Y, Yref) inlet.X = {'H2':0.3, 'O2':0.5, 'AR':0.2} self.assertNear(inlet.X[gas2.species_index('H2')], 0.3) def test_grid_check(self): gas = ct.Solution("h2o2.yaml") flame = ct.FreeFlow(gas) with self.assertRaisesRegex(ct.CanteraError, 'monotonically'): flame.grid = [0, 0.1, 0.1, 0.2] with self.assertRaisesRegex(ct.CanteraError, 'monotonically'): flame.grid = [0, 0.1, 0.2, 0.05] def test_unpicklable(self): import pickle gas = ct.Solution("h2o2.yaml") flame = ct.FreeFlow(gas) with self.assertRaises(NotImplementedError): pickle.dumps(flame) def test_uncopyable(self): import copy gas = ct.Solution("h2o2.yaml") flame = ct.FreeFlow(gas) with self.assertRaises(NotImplementedError): copy.copy(flame) def test_exceptions(self): with pytest.raises(TypeError, match="Argument 'phase' has incorrect type"): ct.Inlet1D(None) gas = ct.Solution("h2o2.yaml") with pytest.raises(ct.CanteraError, match="incompatible ThermoPhase type"): ct.ReactingSurface1D(gas) interface = ct.Solution("diamond.yaml", "diamond_100") with pytest.raises(TypeError, match="unexpected keyword"): ct.ReactingSurface1D(interface, foo="bar") surf = ct.ReactingSurface1D(interface) def test_invalid_property(self): gas1 = ct.Solution("h2o2.yaml") inlet = ct.Inlet1D(name='something', phase=gas1) flame = ct.FreeFlow(gas1) sim = ct.Sim1D((inlet, flame)) for x in (inlet, flame, sim): with self.assertRaises(AttributeError): x.foobar = 300 with self.assertRaises(AttributeError): x.foobar def test_tolerances(self): gas = ct.Solution("h2o2.yaml") left = ct.Inlet1D(gas) flame = ct.FreeFlow(gas) right = ct.Outlet1D(gas) # Some things don't work until the domains have been added to a Sim1D sim = ct.Sim1D((left, flame, right)) with self.assertRaisesRegex(ct.CanteraError, 'no component'): flame.set_steady_tolerances(foobar=(3e-4, 3e-6)) flame.set_steady_tolerances(default=(5e-3, 5e-5), T=(3e-4, 3e-6), Y=(7e-7, 7e-9)) flame.set_transient_tolerances(default=(6e-3, 6e-5), T=(4e-4, 4e-6), Y=(2e-7, 2e-9)) # Flow domain atol_ss = set(flame.steady_abstol()) atol_ts = set(flame.transient_abstol()) rtol_ss = set(flame.steady_reltol()) rtol_ts = set(flame.transient_reltol()) self.assertEqual(atol_ss, set((5e-5, 3e-6, 7e-9))) self.assertEqual(atol_ts, set((6e-5, 4e-6, 2e-9))) self.assertEqual(rtol_ss, set((5e-3, 3e-4, 7e-7))) self.assertEqual(rtol_ts, set((6e-3, 4e-4, 2e-7))) def test_switch_transport(self): gas = ct.Solution('h2o2.yaml') gas.set_equivalence_ratio(0.9, 'H2', 'O2:0.21, N2:0.79') flame = ct.FreeFlame(gas, width=0.1) flame.set_initial_guess() assert gas.transport_model == flame.transport_model == 'mixture-averaged' flame.transport_model = 'unity-Lewis-number' assert gas.transport_model == flame.transport_model == 'unity-Lewis-number' Dkm_flame = flame.mix_diff_coeffs assert all(Dkm_flame[1,:] == Dkm_flame[2,:]) gas.transport_model = 'multicomponent' assert flame.transport_model == 'multicomponent' class TestFreeFlame(utilities.CanteraTest): tol_ss = [1.0e-5, 1.0e-14] # [rtol atol] for steady-state problem tol_ts = [1.0e-4, 1.0e-11] # [rtol atol] for time stepping def create_sim(self, p, Tin, reactants, width=0.05, mech="h2o2.yaml"): # Solution object used to compute mixture properties self.gas = ct.Solution(mech) self.gas.TPX = Tin, p, reactants # Flame object self.sim = ct.FreeFlame(self.gas, width=width) self.sim.flame.set_steady_tolerances(default=self.tol_ss) self.sim.flame.set_transient_tolerances(default=self.tol_ts) # Set properties of the upstream fuel-air mixture self.sim.inlet.T = Tin self.sim.inlet.X = reactants def solve_fixed_T(self): # Solve with the energy equation disabled self.sim.energy_enabled = False self.sim.solve(loglevel=0, refine_grid=False) self.assertFalse(self.sim.energy_enabled) def solve_mix(self, ratio=3.0, slope=0.3, curve=0.2, prune=0.0, refine=True): # Solve with the energy equation enabled self.sim.set_refine_criteria(ratio=ratio, slope=slope, curve=curve, prune=prune) self.sim.energy_enabled = True self.sim.solve(loglevel=0, refine_grid=refine) self.assertTrue(self.sim.energy_enabled) self.assertEqual(self.sim.transport_model, 'mixture-averaged') def solve_multi(self): self.sim.transport_model = 'multicomponent' self.sim.solve(loglevel=0, refine_grid=True) self.assertEqual(self.sim.transport_model, 'multicomponent') def test_flow_type(self): Tin = 300 p = ct.one_atm reactants = 'H2:0.65, O2:0.5, AR:2' self.create_sim(p, Tin, reactants, width=0.0001) assert self.sim.flame.domain_type == "free-flow" def test_fixed_temperature(self): # test setting of fixed temperature Tin = 300 p = ct.one_atm reactants = 'H2:0.65, O2:0.5, AR:2' self.create_sim(p, Tin, reactants, width=0.0001) self.sim.set_initial_guess() zvec = self.sim.grid tvec = self.sim.T tfixed = 900. self.sim.fixed_temperature = tfixed zfixed = np.interp(tfixed, tvec, zvec) self.assertNear(self.sim.fixed_temperature, tfixed) self.assertNear(self.sim.fixed_temperature_location, zfixed) @utilities.slow_test def test_auto_width(self): Tin = 300 p = ct.one_atm reactants = 'H2:0.65, O2:0.5, AR:2' self.create_sim(p, Tin, reactants, width=0.0001) self.sim.set_refine_criteria(ratio=3, slope=0.3, curve=0.2) self.sim.solve(loglevel=0, refine_grid=True, auto=True) self.gas.TPX = Tin, p, reactants self.gas.equilibrate('HP') Tad = self.gas.T self.assertNear(Tad, self.sim.T[-1], 2e-2) # Re-solving with auto=False should not trigger a DomainTooNarrow # exception, and should leave domain width constant self.sim.flame.grid *= 0.3 old_width = self.sim.grid[-1] self.sim.solve(loglevel=0, refine_grid=True, auto=False) self.assertNear(self.sim.grid[-1], old_width) def test_auto_width2(self): self.create_sim(p=ct.one_atm, Tin=400, reactants='H2:0.8, O2:0.5', width=0.1) self.sim.set_refine_criteria(ratio=4, slope=0.8, curve=0.8) self.sim.flame.set_steady_tolerances(T=(2e-4, 1e-8)) self.sim.solve(refine_grid=True, auto=True, loglevel=0) self.assertNear(self.sim.velocity[0], 17.02, 1e-1) self.assertLess(self.sim.grid[-1], 2.0) # grid should not be too wide self.assertEqual(self.sim.flame.tolerances("T"), (2e-4, 1e-8)) @utilities.slow_test def test_converge_adiabatic(self): # Test that the adiabatic flame temperature and species profiles # converge to the correct equilibrium values as the grid is refined reactants = 'H2:1.1, O2:1, AR:5' p = ct.one_atm Tin = 300 self.create_sim(p, Tin, reactants) self.solve_fixed_T() self.gas.TPX = Tin, p, reactants self.gas.equilibrate('HP') Tad = self.gas.T Xad = self.gas.X self.solve_mix(slope=0.5, curve=0.3) T1 = self.sim.T[-1] X1 = self.sim.X[:,-1] self.solve_mix(slope=0.2, curve=0.1) T2 = self.sim.T[-1] X2 = self.sim.X[:,-1] self.solve_mix(slope=0.1, curve=0.05) T3 = self.sim.T[-1] X3 = self.sim.X[:,-1] self.assertLess(abs(T2-Tad), abs(T1-Tad)) self.assertLess(abs(T3-Tad), abs(T2-Tad)) for k in range(self.gas.n_species): self.assertLessEqual(abs(X2[k]-Xad[k]), abs(X1[k]-Xad[k])) self.assertLessEqual(abs(X3[k]-Xad[k]), abs(X2[k]-Xad[k])) def run_mix(self, phi, T, width, p, refine): reactants = {'H2': phi, 'O2': 0.5, 'AR': 2} self.create_sim(p * ct.one_atm, T, reactants, width) self.solve_mix(refine=refine) rhou = self.sim.inlet.mdot # Check continuity for rhou_j in self.sim.density * self.sim.velocity: self.assertNear(rhou_j, rhou, 1e-4) def test_solution_array_output(self): self.run_mix(phi=1.0, T=300, width=2.0, p=1.0, refine=False) flow = self.sim.to_array(normalize=True) self.assertArrayNear(self.sim.grid, flow.grid) self.assertArrayNear(self.sim.T, flow.T) f2 = ct.FreeFlame(self.gas) f2.from_array(flow) self.assertArrayNear(self.sim.grid, f2.grid) self.assertArrayNear(self.sim.T, f2.T) inlet = self.sim.to_array(self.sim.inlet) f2.from_array(inlet, f2.inlet) self.assertNear(self.sim.inlet.T, f2.inlet.T) self.assertNear(self.sim.inlet.mdot, f2.inlet.mdot) self.assertArrayNear(self.sim.inlet.Y, f2.inlet.Y) def test_restart_array(self): # restart from SolutionArray self.run_restart("array") def test_restart_csv(self): # restart from CSV format self.run_restart("csv") def test_restart_yaml(self): # restart from YAML format self.run_restart("yaml") @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") def test_restart_hdf(self): # restart from HDF format self.run_restart("h5") def run_restart(self, mode): self.run_mix(phi=1.0, T=300, width=2.0, p=1.0, refine=False) group = "restart" if mode == "array": data = self.sim.to_array() else: data = self.test_work_path / f"freeflame_restart.{mode}" data.unlink(missing_ok=True) if mode == "csv": self.sim.save(data, basis="mole") else: self.sim.save(data, group) reactants = {'H2': 0.9, 'O2': 0.5, 'AR': 2} self.create_sim(1.1 * ct.one_atm, 500, reactants, 2.0) self.sim.set_initial_guess(data=data, group=group) self.solve_mix(refine=False) # Check continuity rhou = self.sim.inlet.mdot for rhou_j in self.sim.density * self.sim.velocity: self.assertNear(rhou_j, rhou, 1e-4) def test_settings(self): self.create_sim(p=ct.one_atm, Tin=400, reactants='H2:0.8, O2:0.5', width=0.1) self.sim.set_initial_guess() sim = self.sim # new implementation new_keys = { "type", "points", "tolerances", "transport-model", "phase", "radiation-enabled", "energy-enabled", "Soret-enabled", "species-enabled", "refine-criteria", "fixed-point"} settings = sim.flame.settings for k in new_keys: assert k in settings assert settings["radiation-enabled"] == False # Apply settings using individual setters sim.flame.boundary_emissivities = 0.12, 0.34 sim.flame.radiation_enabled = True sim.flame.set_steady_tolerances(default=(3e-4, 7e-9)) sim.transport_model = 'unity-Lewis-number' # Check that the aggregated settings reflect the changes new_settings = sim.flame.settings assert new_settings["radiation-enabled"] == True assert new_settings["emissivity-left"] == 0.12 assert new_settings["emissivity-right"] == 0.34 assert new_settings["transport-model"] == "unity-Lewis-number" tolerances = new_settings["tolerances"] assert tolerances["steady-reltol"] == approx(3e-4) assert tolerances["steady-abstol"] == approx(7e-9) assert "fixed-point" in new_settings assert "location" in new_settings["fixed-point"] def test_mixture_averaged_case1(self): self.run_mix(phi=0.65, T=300, width=0.03, p=1.0, refine=True) @utilities.slow_test def test_mixture_averaged_case2(self): self.run_mix(phi=0.5, T=300, width=2.0, p=1.0, refine=False) @utilities.slow_test def test_mixture_averaged_case3(self): self.run_mix(phi=0.5, T=500, width=0.05, p=1.0, refine=False) @utilities.slow_test def test_mixture_averaged_case4(self): self.run_mix(phi=0.7, T=400, width=2.0, p=5.0, refine=False) @utilities.slow_test def test_mixture_averaged_case5(self): self.run_mix(phi=1.0, T=300, width=2.0, p=5.0, refine=False) @utilities.slow_test def test_mixture_averaged_case6(self): self.run_mix(phi=1.5, T=300, width=0.2, p=1.0, refine=True) @utilities.slow_test def test_mixture_averaged_case7(self): self.run_mix(phi=1.5, T=500, width=2.0, p=0.1, refine=False) @utilities.slow_test 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.velocity[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.velocity[0] self.gas.set_multiplier(1-dk, m) # perturb reaction m self.sim.solve(loglevel=0, refine_grid=False) Suminus = self.sim.velocity[0] fwd = (Suplus-Suminus)/(2*Su0*dk) self.assertNear(fwd, dSdk_adj[m], rtol=5e-3, atol=1e-7) # @utilities.unittest.skip('sometimes slow') def test_multicomponent(self): reactants = 'H2:1.1, O2:1, AR:5.3' p = ct.one_atm Tin = 300 self.create_sim(p, Tin, reactants) self.solve_fixed_T() self.solve_mix(ratio=5, slope=0.5, curve=0.3) Su_mix = self.sim.velocity[0] # Multicomponent flame speed should be similar but not identical to # the mixture-averaged flame speed. self.solve_multi() Su_multi = self.sim.velocity[0] self.assertFalse(self.sim.soret_enabled) self.assertNear(Su_mix, Su_multi, 5e-2) self.assertNotEqual(Su_mix, Su_multi) # Flame speed with Soret effect should be similar but not identical to # the multicomponent flame speed self.sim.soret_enabled = True self.sim.solve(loglevel=0, refine_grid=True) self.assertTrue(self.sim.soret_enabled) Su_soret = self.sim.velocity[0] self.assertNear(Su_multi, Su_soret, 2e-1) self.assertNotEqual(Su_multi, Su_soret) def test_unity_lewis(self): self.create_sim(ct.one_atm, 300, 'H2:1.1, O2:1, AR:5.3') self.sim.transport_model = 'unity-Lewis-number' self.sim.set_refine_criteria(ratio=3.0, slope=0.08, curve=0.12) self.sim.solve(loglevel=0, auto=True) dh_unity_lewis = self.sim.enthalpy_mass.ptp() self.sim.transport_model = 'mixture-averaged' self.sim.solve(loglevel=0) dh_mix = self.sim.enthalpy_mass.ptp() # deviation of enthalpy should be much lower for unity Le model (tends # towards zero as grid is refined) self.assertLess(dh_unity_lewis, 0.1 * dh_mix) def test_flux_gradient_mass(self): self.create_sim(ct.one_atm, 300, 'H2:1.1, O2:1, AR:5.3') self.sim.transport_model = 'mixture-averaged' self.sim.set_refine_criteria(ratio=3.0, slope=0.08, curve=0.12) assert self.sim.flux_gradient_basis == 'molar' self.sim.solve(loglevel=0, auto=True) sl_mole = self.sim.velocity[0] self.sim.flux_gradient_basis = 'mass' assert self.sim.flux_gradient_basis == 'mass' self.sim.solve(loglevel=0) sl_mass = self.sim.velocity[0] # flame speeds should not be exactly equal assert sl_mole != sl_mass # but they should be close assert sl_mole == approx(sl_mass, rel=0.1) def test_soret_with_mix(self): # Test that enabling Soret diffusion without # multicomponent transport results in an error self.create_sim(101325, 300, 'H2:1.0, O2:1.0') self.assertFalse(self.sim.soret_enabled) self.assertFalse(self.sim.transport_model == 'multicomponent') with self.assertRaisesRegex(ct.CanteraError, 'requires.*multicomponent'): self.sim.soret_enabled = True self.sim.solve(loglevel=0, auto=False) @utilities.slow_test def test_soret_with_auto(self): # Test that auto solving with Soret enabled works self.create_sim(101325, 300, 'H2:2.0, O2:1.0') self.sim.soret_enabled = True self.sim.transport_model = 'multicomponent' self.sim.solve(loglevel=0, auto=True) def test_set_soret_multi_mix(self): # Test that the transport model and Soret diffusion # can be set in any order without raising errors self.create_sim(101325, 300, 'H2:1.0, O2:1.0') self.sim.transport_model = 'multicomponent' self.sim.soret_enabled = True self.sim.transport_model = 'mixture-averaged' self.sim.soret_enabled = False self.sim.soret_enabled = True self.sim.transport_model = 'multicomponent' def test_prune(self): reactants = 'H2:1.1, O2:1, AR:5' p = ct.one_atm Tin = 300 self.create_sim(p, Tin, reactants) self.solve_fixed_T() self.solve_mix(slope=0.2, curve=0.1, prune=0.0) N1 = len(self.sim.grid) self.solve_mix(slope=0.5, curve=0.3, prune=0.1) N2 = len(self.sim.grid) self.assertLess(N2, N1) # TODO: check that the solution is actually correct (that is, that the # residual satisfies the error tolerances) on the new grid. @pytest.mark.filterwarnings("ignore:.*legacy YAML format.*:UserWarning") def test_restore_legacy_yaml(self): reactants = 'H2:1.1, O2:1, AR:5' p = 5 * ct.one_atm Tin = 600 self.create_sim(p, Tin, reactants) meta = self.sim.restore("adiabatic_flame_legacy.yaml", "setup") assert meta["generator"] == "Cantera Sim1D" assert meta["cantera-version"] == "2.6.0" assert self.sim.inlet.T == 300 assert self.sim.P == pytest.approx(ct.one_atm) assert len(self.sim.grid) == 9 def test_fixed_restore_yaml(self): # save and restore using YAML format self.run_fixed_restore("yaml") @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") def test_fixed_restore_hdf(self): # save and restore using HDF format self.run_fixed_restore("h5") def run_fixed_restore(self, mode): reactants = "H2:1.1, O2:1, AR:5" p = 2 * ct.one_atm Tin = 400 self.create_sim(p, Tin, reactants) T_rtol = 1.1e-4 T_atol = 2e-6 self.sim.flame.set_steady_tolerances(T=(T_rtol, T_atol)) self.solve_fixed_T() filename = self.test_work_path / f"onedim_fixed_T.{mode}" filename.unlink(missing_ok=True) Y1 = self.sim.Y u1 = self.sim.velocity V1 = self.sim.spread_rate P1 = self.sim.P T1 = self.sim.T self.sim.save(filename, "test") # Save a second solution to the same file self.sim.radiation_enabled = True self.sim.boundary_emissivities = 0.3, 0.8 self.sim.save(filename, "test2") # Create flame object with dummy initial grid self.sim = ct.FreeFlame(self.gas) self.sim.restore(filename, "test") # Sim is initially in "steady-state" mode, so this returns the # steady-state tolerances rtol, atol = self.sim.flame.tolerances("T") self.assertNear(rtol, T_rtol) self.assertNear(atol, T_atol) self.assertFalse(self.sim.energy_enabled) P2a = self.sim.P self.assertNear(p, P1) self.assertNear(P1, P2a) Y2 = self.sim.Y u2 = self.sim.velocity V2 = self.sim.spread_rate T2 = self.sim.T self.assertArrayNear(Y1, Y2) self.assertArrayNear(u1, u2) self.assertArrayNear(V1, V2) self.assertArrayNear(T1, T2) self.solve_fixed_T() Y3 = self.sim.Y u3 = self.sim.velocity V3 = self.sim.spread_rate # TODO: These tolerances seem too loose, but the tests fail on some # systems with tighter tolerances. self.assertArrayNear(Y1, Y3, 3e-3) self.assertArrayNear(u1, u3, 1e-3) self.assertArrayNear(V1, V3, 1e-3) self.assertFalse(self.sim.radiation_enabled) self.assertFalse(self.sim.soret_enabled) self.sim.restore(filename, "test2") self.assertTrue(self.sim.radiation_enabled) self.assertEqual(self.sim.boundary_emissivities, (0.3, 0.8)) self.sim.solve(loglevel=0) @pytest.mark.filterwarnings("ignore:.*reaction_phase_index.*:DeprecationWarning") def test_array_properties(self): self.create_sim(ct.one_atm, 300, 'H2:1.1, O2:1, AR:5') grid_shape = self.sim.grid.shape skip = { # Skipped because they are constant, irrelevant, or otherwise not desired "P", "Te", "atomic_weights", "charges", "electric_potential", "max_temp", "min_temp", "molecular_weights", "product_stoich_coeffs", "product_stoich_coeffs", "product_stoich_coeffs_reversible", "reactant_stoich_coeffs", "reactant_stoich_coeffs", "reference_pressure", "state", "u", "v", # Skipped because they are 2D (conversion not implemented) "binary_diff_coeffs", "creation_rates_ddX", "destruction_rates_ddX", "forward_rates_of_progress_ddX", "net_production_rates_ddX", "net_rates_of_progress_ddX", "reverse_rates_of_progress_ddX", "net_rates_of_progress_ddCi", "forward_rates_of_progress_ddCi", "reverse_rates_of_progress_ddCi", "creation_rates_ddCi", "destruction_rates_ddCi", "net_production_rates_ddCi" } for attr in dir(self.gas): if attr.startswith("_") or attr in skip: continue try: soln_value = getattr(self.gas, attr) except (ct.CanteraError, ct.ThermoModelMethodError, NotImplementedError): continue if not isinstance(soln_value, (float, np.ndarray)): continue flame_value = getattr(self.sim, attr) assert flame_value.shape == np.asarray(soln_value).shape + grid_shape @utilities.slow_test def test_save_restore_add_species_yaml(self): reactants = "H2:1.1, O2:1, AR:5" p = 2 * ct.one_atm Tin = 400 filename = self.test_work_path / "onedim-add-species.yaml" # In Python >= 3.8, this can be replaced by the missing_ok argument if filename.is_file(): filename.unlink() self.create_sim(p, Tin, reactants, mech="h2o2.yaml") gas1 = self.gas self.sim.max_grid_points = 234 self.solve_fixed_T() self.solve_mix(ratio=5, slope=0.5, curve=0.3) self.sim.transport_model = "multicomponent" self.sim.soret_enabled = True self.sim.save(filename, "test") T1 = self.sim.T Y1 = self.sim.Y gas2 = ct.Solution("h2o2-plus.yaml", transport_model="multicomponent") self.sim = ct.FreeFlame(gas2) self.sim.restore(filename, "test") T2 = self.sim.T Y2 = self.sim.Y self.assertTrue(self.sim.soret_enabled) self.assertEqual(self.sim.max_grid_points, 234) self.assertArrayNear(T1, T2) for k1, species in enumerate(gas1.species_names): k2 = gas2.species_index(species) self.assertArrayNear(Y1[k1], Y2[k2]) @utilities.slow_test def test_save_restore_remove_species_yaml(self): reactants = "H2:1.1, O2:1, AR:5" p = 2 * ct.one_atm Tin = 400 filename = self.test_work_path / "onedim-remove-species.yaml" # In Python >= 3.8, this can be replaced by the missing_ok argument if filename.is_file(): filename.unlink() self.create_sim(p, Tin, reactants, mech="h2o2-plus.yaml") gas1 = self.gas self.solve_fixed_T() self.solve_mix(ratio=5, slope=0.5, curve=0.3) self.sim.save(filename, "test") T1 = self.sim.T Y1 = self.sim.Y gas2 = ct.Solution("h2o2.yaml") self.sim = ct.FreeFlame(gas2) self.sim.restore(filename, "test") T2 = self.sim.T Y2 = self.sim.Y self.assertArrayNear(T1, T2) for k2, species in enumerate(gas2.species_names): k1 = gas1.species_index(species) self.assertArrayNear(Y1[k1], Y2[k2]) def test_write_csv(self): filename = self.test_work_path / "onedim-save.csv" filename.unlink(missing_ok=True) self.create_sim(2e5, 350, 'H2:1.0, O2:2.0', mech="h2o2.yaml") self.sim.save(filename, basis="mole") data = ct.SolutionArray(self.gas) data.read_csv(filename) self.assertArrayNear(data.grid, self.sim.grid) self.assertArrayNear(data.T, self.sim.T) k = self.gas.species_index('H2') self.assertArrayNear(data.X[:, k], self.sim.X[k, :]) @pytest.mark.usefixtures("allow_deprecated") @pytest.mark.filterwarnings("ignore:.*legacy HDF.*:UserWarning") @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") def test_restore_legacy_hdf(self): # Legacy input file was created using the Cantera 2.6 Python test suite: # - restore_legacy.h5 -> test_onedim.py::TestFreeFlame::test_write_hdf filename = self.test_data_path / f"freeflame_legacy.h5" self.run_mix(phi=1.1, T=350, width=2.0, p=2.0, refine=False) desc = 'mixture-averaged simulation' f = ct.FreeFlame(self.gas) meta = f.restore(filename, "group0") assert meta['description'] == desc assert meta['cantera_version'] == "2.6.0" # check with relaxed tolerances to account for differences between # Cantera 2.6 and Cantera 3.1 self.check_save_restore(f, tol_T=1e-3, tol_X=1e-1) @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") def test_save_restore_hdf(self): # save and restore with native format (HighFive only) self.run_save_restore("h5") def test_save_restore_yaml(self): self.run_save_restore("yaml") def run_save_restore(self, mode): filename = self.test_work_path / f"freeflame.{mode}" filename.unlink(missing_ok=True) self.run_mix(phi=1.1, T=350, width=2.0, p=2.0, refine=False) desc = 'mixture-averaged simulation' self.sim.save(filename, "group0", description=desc) f = ct.FreeFlame(self.gas) meta = f.restore(filename, "group0") assert meta['description'] == desc assert meta['cantera-version'] == ct.__version__ assert meta['git-commit'] == f"'{ct.__git_commit__}'" self.check_save_restore(f) def check_save_restore(self, f, tol_T=None, tol_X=None): # pytest.approx is used as equality for floats cannot be guaranteed for loaded # HDF5 files if they were created on a different OS and/or architecture assert list(f.grid) == pytest.approx(list(self.sim.grid)) assert list(f.T) == pytest.approx(list(self.sim.T), rel=tol_T) k = self.gas.species_index('H2') assert list(f.X[k, :]) == pytest.approx(list(self.sim.X[k, :]), rel=tol_X) assert list(f.inlet.X) == pytest.approx(list(self.sim.inlet.X)) def check_settings(one, two): for k, v in one.items(): assert k in two if isinstance(v, dict): for kk, vv in v.items(): if isinstance(vv, float): assert two[k][kk] == pytest.approx(vv) else: assert two[k][kk] == vv elif isinstance(v, float): assert two[k] == pytest.approx(v) else: assert two[k] == v check_settings(self.sim.flame.settings, f.flame.settings) f.solve(loglevel=0) def test_refine_criteria_boundscheck(self): self.create_sim(ct.one_atm, 300.0, 'H2:1.1, O2:1, AR:5') good = [3.0, 0.1, 0.2, 0.05] bad = [1.2, 1.1, -2, 0.3] self.sim.set_refine_criteria(*good) for i in range(4): with self.assertRaisesRegex(ct.CanteraError, 'Refiner::setCriteria'): vals = list(good) vals[i] = bad[i] self.sim.set_refine_criteria(*vals) def test_refine_criteria(self): self.create_sim(ct.one_atm, 300.0, 'H2:1.1, O2:1, AR:5') vals = {'ratio': 3.0, 'slope': 0.1, 'curve': 0.2, 'prune': 0.05} self.sim.set_refine_criteria(**vals) check = self.sim.get_refine_criteria() self.assertEqual(vals, check) def test_replace_grid(self): self.create_sim(ct.one_atm, 300.0, 'H2:1.1, O2:1, AR:5') self.solve_fixed_T() ub = self.sim.velocity[-1] # add some points to the grid grid = list(self.sim.grid) for i in (7,5,4,2): grid.insert(i, 0.5 * (grid[i-1] + grid[i])) self.sim.flame.grid = grid self.sim.set_initial_guess() self.solve_fixed_T() self.assertNear(self.sim.velocity[-1], ub, 1e-2) def test_exceed_max_grid_points(self): self.create_sim(ct.one_atm, 400.0, 'H2:1.1, O2:1, AR:5') # Set the maximum grid points to be a low number that should # be exceeded by the refinement self.sim.max_grid_points = 10 with self.assertRaisesRegex(ct.CanteraError, 'max number of grid points'): self.sim.solve(loglevel=0, refine_grid=True) def test_set_max_grid_points(self): self.create_sim(ct.one_atm, 400.0, 'H2:1.1, O2:1, AR:5') self.assertEqual(self.sim.max_grid_points, 1000) self.sim.max_grid_points = 10 self.assertEqual(self.sim.max_grid_points, 10) class TestDiffusionFlame(utilities.CanteraTest): # Note: to re-create the reference files: # (1) set PYTHONPATH to build/python. # (2) Start Python and run: # >>> import cantera.test # >>> t = cantera.test.test_onedim.TestDiffusionFlame() # >>> t.setUpClass() # >>> t.test_mixture_averaged(True) # >>> t.test_auto(True) # >>> t.test_mixture_averaged_rad(True) # (3) Compare the reference files created in the current working directory with # the ones in test/data and replace them if needed. def create_sim(self, p, fuel='H2:1.0, AR:1.0', T_fuel=300, mdot_fuel=0.24, oxidizer='O2:0.2, AR:0.8', T_ox=300, mdot_ox=0.72, width=0.02): # Solution object used to compute mixture properties self.gas = ct.Solution("h2o2.yaml", "ohmech") self.gas.TP = T_fuel, p # Flame object self.sim = ct.CounterflowDiffusionFlame(self.gas, width=width) # Set properties of the fuel and oxidizer mixtures self.sim.fuel_inlet.mdot = mdot_fuel self.sim.fuel_inlet.X = fuel self.sim.fuel_inlet.T = T_fuel self.sim.oxidizer_inlet.mdot = mdot_ox self.sim.oxidizer_inlet.X = oxidizer self.sim.oxidizer_inlet.T = T_ox self.sim.set_initial_guess() def solve_fixed_T(self): # Solve with the energy equation disabled self.sim.energy_enabled = False self.sim.solve(loglevel=0, refine_grid=False) self.assertFalse(self.sim.energy_enabled) def solve_mix(self, ratio=3.0, slope=0.1, curve=0.12, prune=0.0): # Solve with the energy equation enabled self.sim.set_refine_criteria(ratio=ratio, slope=slope, curve=curve, prune=prune) self.sim.energy_enabled = True self.sim.solve(loglevel=0, refine_grid=True) self.assertTrue(self.sim.energy_enabled) self.assertEqual(self.sim.transport_model, 'mixture-averaged') @utilities.slow_test def test_mixture_averaged(self, saveReference=False): referenceFile = "DiffusionFlameTest-h2-mix.csv" self.create_sim(p=ct.one_atm) nPoints = len(self.sim.grid) Tfixed = self.sim.T self.solve_fixed_T() self.assertEqual(nPoints, len(self.sim.grid)) self.assertArrayNear(Tfixed, self.sim.T) self.solve_mix() data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid data[:,1] = self.sim.velocity data[:,2] = self.sim.spread_rate data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T if saveReference: np.savetxt(referenceFile, data, '%11.6e', ', ') else: bad = utilities.compareProfiles(self.test_data_path / referenceFile, data, rtol=1e-2, atol=1e-8, xtol=1e-2) self.assertFalse(bad, bad) def test_auto(self, saveReference=False): referenceFile = "DiffusionFlameTest-h2-auto.csv" self.create_sim(p=ct.one_atm, mdot_fuel=2, mdot_ox=3) nPoints = [] timesteps = [] def steady_func(x): nPoints.append(len(self.sim.T)) return 0 def time_step_func(dt): timesteps.append(dt) self.assertGreater(dt, 0) return 0 self.sim.set_steady_callback(steady_func) self.sim.set_time_step_callback(time_step_func) self.sim.set_refine_criteria(ratio=3.0, slope=0.1, curve=0.12, prune=0.0) self.sim.solve(loglevel=0, auto=True) self.assertNotEqual(len(nPoints), 0) self.assertNotEqual(len(timesteps), 0) data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid data[:,1] = self.sim.velocity data[:,2] = self.sim.spread_rate data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T if saveReference: np.savetxt(referenceFile, data, '%11.6e', ', ') else: bad = utilities.compareProfiles(self.test_data_path / referenceFile, data, rtol=1e-2, atol=1e-8, xtol=1e-2) self.assertFalse(bad, bad) def run_extinction(self, mdot_fuel, mdot_ox, T_ox, width, P): self.create_sim(fuel='H2:1.0', oxidizer='O2:1.0', p=ct.one_atm*P, mdot_fuel=mdot_fuel, mdot_ox=mdot_ox, T_ox=T_ox, width=width) self.sim.solve(loglevel=0, auto=True) self.assertFalse(self.sim.extinct()) def test_extinction_case1(self): self.run_extinction(mdot_fuel=0.5, mdot_ox=3.0, T_ox=300, width=0.018, P=1.0) @utilities.slow_test def test_extinction_case2(self): self.run_extinction(mdot_fuel=0.5, mdot_ox=1.0, T_ox=300, width=0.01, P=5.0) @utilities.slow_test def test_extinction_case3(self): self.run_extinction(mdot_fuel=1.0, mdot_ox=0.5, T_ox=500, width=0.02, P=5.0) @utilities.slow_test def test_extinction_case4(self): self.run_extinction(mdot_fuel=1.0, mdot_ox=3.0, T_ox=400, width=0.05, P=2.0) @utilities.slow_test def test_extinction_case5(self): self.run_extinction(mdot_fuel=1.0, mdot_ox=3.0, T_ox=300, width=0.1, P=1.0) @utilities.slow_test def test_extinction_case6(self): self.run_extinction(mdot_fuel=0.5, mdot_ox=0.5, T_ox=600, width=0.2, P=0.05) @utilities.slow_test def test_extinction_case7(self): self.run_extinction(mdot_fuel=0.2, mdot_ox=2.0, T_ox=600, width=0.2, P=0.05) @utilities.slow_test def test_restart(self): self.run_extinction(mdot_fuel=0.5, mdot_ox=3.0, T_ox=300, width=0.018, P=1.0) arr = self.sim.to_array() self.create_sim(mdot_fuel=5.5, mdot_ox=3.3, T_ox=400, width=0.018, p=ct.one_atm*1.1) self.sim.set_initial_guess(data=arr) self.sim.solve(loglevel=0, auto=True) # Check inlet mdot = self.sim.density * self.sim.velocity self.assertNear(mdot[0], self.sim.fuel_inlet.mdot, 1e-4) self.assertNear(self.sim.T[0], self.sim.fuel_inlet.T, 1e-4) self.assertNear(mdot[-1], -self.sim.oxidizer_inlet.mdot, 1e-4) def test_mixture_averaged_rad(self, saveReference=False): referenceFile = "DiffusionFlameTest-h2-mix-rad.csv" self.create_sim(p=ct.one_atm) nPoints = len(self.sim.grid) Tfixed = self.sim.T self.solve_fixed_T() self.assertEqual(nPoints, len(self.sim.grid)) self.assertArrayNear(Tfixed, self.sim.T) self.assertFalse(self.sim.radiation_enabled) self.sim.radiation_enabled = True self.assertTrue(self.sim.radiation_enabled) self.sim.flame.boundary_emissivities = 0.25, 0.15 self.solve_mix() data = np.empty((self.sim.flame.n_points, self.gas.n_species + 4)) data[:,0] = self.sim.grid data[:,1] = self.sim.velocity data[:,2] = self.sim.spread_rate data[:,3] = self.sim.T data[:,4:] = self.sim.Y.T qdot = self.sim.flame.radiative_heat_loss self.assertEqual(len(qdot), self.sim.flame.n_points) if saveReference: np.savetxt(referenceFile, data, '%11.6e', ', ') else: bad = utilities.compareProfiles(self.test_data_path / referenceFile, data, rtol=1e-2, atol=1e-8, xtol=1e-2) self.assertFalse(bad, bad) filename = self.test_work_path / "DiffusionFlameTest-h2-mix-rad.csv" # In Python >= 3.8, this can be replaced by the missing_ok argument if filename.is_file(): filename.unlink() self.sim.save(filename, basis="mole") # check output self.assertTrue(filename.is_file()) csv_data = np.genfromtxt(filename, dtype=float, delimiter=',', names=True) self.assertIn('radiativeheatloss', csv_data.dtype.names) def test_strain_rate(self): # This doesn't test that the values are correct, just that they can be # computed without error self.create_sim(p=ct.one_atm) self.solve_fixed_T() a_max = self.sim.strain_rate('max') a_mean = self.sim.strain_rate('mean') a_pf_fuel = self.sim.strain_rate('potential_flow_fuel') a_pf_oxidizer = self.sim.strain_rate('potential_flow_oxidizer') a_stoich1 = self.sim.strain_rate('stoichiometric', fuel='H2') a_stoich2 = self.sim.strain_rate('stoichiometric', fuel='H2', stoich=0.5) self.assertLessEqual(a_mean, a_max) self.assertLessEqual(a_pf_fuel, a_max) self.assertLessEqual(a_pf_oxidizer, a_max) self.assertLessEqual(a_stoich1, a_max) self.assertEqual(a_stoich1, a_stoich2) with self.assertRaises(ValueError): self.sim.strain_rate('bad_keyword') with self.assertRaises(KeyError): # missing 'fuel' self.sim.strain_rate('stoichiometric') with self.assertRaises(KeyError): # missing 'stoich' self.sim.strain_rate('stoichiometric', fuel='H2', oxidizer='H2O2') def test_mixture_fraction(self): self.create_sim(p=ct.one_atm) Z = self.sim.mixture_fraction('H') self.assertNear(Z[0], 1.0) self.assertNear(Z[-1], 0.0) self.assertTrue(all(Z >= 0)) self.assertTrue(all(Z <= 1.0)) Z = self.sim.mixture_fraction('Bilger') self.assertNear(Z[0], 1.0) self.assertNear(Z[-1], 0.0) self.assertTrue(all(Z >= 0)) self.assertTrue(all(Z <= 1.0)) def test_equivalence_ratio(self): self.create_sim(p=ct.one_atm) phi = self.sim.equivalence_ratio assert phi[0] == np.inf assert np.isclose(phi[-1], 0.0) class TestCounterflowPremixedFlame(utilities.CanteraTest): # Note: to re-create the reference file: # (1) set PYTHONPATH to build/python. # (2) Start Python and run: # >>> import cantera.test # >>> t = cantera.test.test_onedim.TestCounterflowPremixedFlame() # >>> t.setUpClass() # >>> t.test_mixture_averaged(True) # (3) Compare the reference files created in the current working directory with # the ones in test/data and replace them if needed. def test_mixture_averaged(self, saveReference=False): T_in = 373.0 # inlet temperature comp = 'H2:1.6, O2:1, AR:7' # premixed gas composition gas = ct.Solution("h2o2.yaml") gas.TPX = T_in, 0.05 * ct.one_atm, comp width = 0.2 # m sim = ct.CounterflowPremixedFlame(gas=gas, width=width) # set the properties at the inlets sim.reactants.mdot = 0.12 # kg/m^2/s sim.reactants.X = comp sim.reactants.T = T_in sim.products.mdot = 0.06 # kg/m^2/s sim.flame.set_steady_tolerances(default=[1.0e-5, 1.0e-11]) sim.flame.set_transient_tolerances(default=[1.0e-5, 1.0e-11]) sim.set_initial_guess() # assume adiabatic equilibrium products sim.energy_enabled = False sim.solve(loglevel=0, refine_grid=False) sim.set_refine_criteria(ratio=3, slope=0.2, curve=0.4, prune=0.02) sim.energy_enabled = True self.assertFalse(sim.radiation_enabled) sim.solve(loglevel=0, refine_grid=True) data = np.empty((sim.flame.n_points, gas.n_species + 4)) data[:,0] = sim.grid data[:,1] = sim.velocity data[:,2] = sim.spread_rate data[:,3] = sim.T data[:,4:] = sim.Y.T referenceFile = "CounterflowPremixedFlame-h2-mix.csv" if saveReference: np.savetxt(referenceFile, data, '%11.6e', ', ') else: bad = utilities.compareProfiles(self.test_data_path / referenceFile, data, rtol=1e-2, atol=1e-8, xtol=1e-2) self.assertFalse(bad, bad) filename = self.test_work_path / "CounterflowPremixedFlame-h2-mix.csv" filename.unlink(missing_ok=True) sim.save(filename) # check output self.assertTrue(filename.is_file()) csv_data = np.genfromtxt(filename, dtype=float, delimiter=',', names=True) self.assertNotIn('qdot', csv_data.dtype.names) def run_case(self, phi, T, width, P): gas = ct.Solution("h2o2.yaml") gas.TPX = T, P * ct.one_atm, {'H2':phi, 'O2':0.5, 'AR':2} sim = ct.CounterflowPremixedFlame(gas=gas, width=width) sim.reactants.mdot = 10 * gas.density sim.products.mdot = 5 * gas.density sim.set_refine_criteria(ratio=6, slope=0.7, curve=0.8, prune=0.4) sim.solve(loglevel=0, auto=True) self.assertTrue(all(sim.T >= T - 1e-3)) self.assertTrue(all(sim.spread_rate >= -1e-9)) assert np.allclose(sim.L, sim.L[0]) return sim @utilities.slow_test def test_solve_case1(self): self.run_case(phi=0.4, T=400, width=0.05, P=10.0) @utilities.slow_test def test_solve_case2(self): self.run_case(phi=0.5, T=500, width=0.03, P=2.0) @utilities.slow_test def test_solve_case3(self): self.run_case(phi=0.7, T=300, width=0.05, P=2.0) @utilities.slow_test def test_solve_case4(self): self.run_case(phi=1.5, T=400, width=0.03, P=0.02) @utilities.slow_test def test_solve_case5(self): self.run_case(phi=2.0, T=300, width=0.2, P=0.2) @utilities.slow_test def test_restart(self): sim = self.run_case(phi=2.0, T=300, width=0.2, P=0.2) arr = sim.to_array() sim.reactants.mdot *= 1.1 sim.products.mdot *= 1.1 sim.set_initial_guess(data=arr) sim.solve(loglevel=0, auto=True) # Check inlet / outlet mdot = sim.density * sim.velocity self.assertNear(mdot[0], sim.reactants.mdot, 1e-4) self.assertNear(mdot[-1], -sim.products.mdot, 1e-4) class TestCounterflowPremixedFlameNonIdeal(utilities.CanteraTest): # Note: to re-create the reference file: # (1) set PYTHONPATH to build/python. # (2) Start Python and run: # >>> import cantera.test # >>> t = cantera.test.test_onedim.TestCounterflowPremixedFlameNonIdeal() # >>> t.setUpClass() # >>> t.test_mixture_averaged(True) # (3) Compare the reference files created in the current working directory with # the ones in test/data and replace them if needed. def test_mixture_averaged(self, saveReference=False): T_in = 373.0 # inlet temperature comp = 'H2:1.6, O2:1, AR:7' # premixed gas composition gas = ct.Solution("h2o2.yaml", "ohmech-RK") gas.TPX = T_in, 10 * ct.one_atm, comp width = 0.005 # m sim = ct.CounterflowPremixedFlame(gas=gas, width=width) # set the properties at the inlets sim.reactants.mdot = 0.12 # kg/m^2/s sim.reactants.X = comp sim.reactants.T = T_in sim.products.mdot = 0.06 # kg/m^2/s sim.flame.set_steady_tolerances(default=[1.0e-5, 1.0e-11]) sim.flame.set_transient_tolerances(default=[1.0e-5, 1.0e-11]) sim.set_initial_guess() # assume adiabatic equilibrium products sim.energy_enabled = False sim.solve(loglevel=0, refine_grid=False) sim.set_refine_criteria(ratio=3, slope=0.2, curve=0.4, prune=0.02) sim.energy_enabled = True self.assertFalse(sim.radiation_enabled) sim.solve(loglevel=0, refine_grid=True) data = np.empty((sim.flame.n_points, gas.n_species + 4)) data[:,0] = sim.grid data[:,1] = sim.velocity data[:,2] = sim.spread_rate data[:,3] = sim.T data[:,4:] = sim.Y.T referenceFile = "CounterflowPremixedFlame-h2-mix-RK.csv" if saveReference: np.savetxt(referenceFile, data, '%11.6e', ', ') else: bad = utilities.compareProfiles(self.test_data_path / referenceFile, data, rtol=1e-2, atol=1e-8, xtol=1e-2) self.assertFalse(bad, bad) filename = self.test_work_path / "CounterflowPremixedFlame-h2-mix-RK.csv" filename.unlink(missing_ok=True) sim.save(filename) # check output self.assertTrue(filename.is_file()) csv_data = np.genfromtxt(filename, dtype=float, delimiter=',', names=True) self.assertNotIn('qdot', csv_data.dtype.names) def run_case(self, phi, T, width, P): gas = ct.Solution("h2o2.yaml", "ohmech-RK") gas.TPX = T, P * ct.one_atm, {'H2':phi, 'O2':0.5, 'AR':2} sim = ct.CounterflowPremixedFlame(gas=gas, width=width) sim.reactants.mdot = 10 * gas.density sim.products.mdot = 5 * gas.density sim.set_refine_criteria(ratio=6, slope=0.7, curve=0.8, prune=0.4) sim.solve(loglevel=0, auto=True) self.assertTrue(all(sim.T >= T - 1e-3)) self.assertTrue(all(sim.spread_rate >= -1e-9)) assert np.allclose(sim.L, sim.L[0]) return sim @utilities.slow_test def test_solve_case1(self): self.run_case(phi=0.4, T=400, width=0.05, P=10.0) @utilities.slow_test def test_solve_case2(self): self.run_case(phi=0.5, T=500, width=0.03, P=2.0) @utilities.slow_test def test_solve_case3(self): self.run_case(phi=0.7, T=300, width=0.05, P=2.0) @utilities.slow_test def test_solve_case4(self): self.run_case(phi=1.5, T=400, width=0.03, P=0.02) @utilities.slow_test def test_solve_case5(self): self.run_case(phi=2.0, T=300, width=0.2, P=0.2) @utilities.slow_test def test_restart(self): sim = self.run_case(phi=2.0, T=300, width=0.2, P=0.2) arr = sim.to_array() sim.reactants.mdot *= 1.1 sim.products.mdot *= 1.1 sim.set_initial_guess(data=arr) sim.solve(loglevel=0, auto=True) # Check inlet / outlet mdot = sim.density * sim.velocity self.assertNear(mdot[0], sim.reactants.mdot, 1e-4) self.assertNear(mdot[-1], -sim.products.mdot, 1e-4) class TestBurnerFlame(utilities.CanteraTest): def solve(self, phi, T, width, P): gas = ct.Solution("h2o2.yaml") gas.TPX = T, ct.one_atm*P, {'H2':phi, 'O2':0.5, 'AR':1.5} sim = ct.BurnerFlame(gas=gas, width=width) sim.burner.mdot = gas.density * 0.15 sim.solve(loglevel=0, auto=True) self.assertGreater(sim.T[1], T) assert np.allclose(sim.L, 0) def test_case1(self): self.solve(phi=0.5, T=500, width=2.0, P=0.1) @utilities.slow_test def test_case2(self): self.solve(phi=2.0, T=400, width=0.05, P=1.0) @utilities.slow_test def test_case3(self): self.solve(phi=1.7, T=300, width=0.05, P=1.0) @utilities.slow_test def test_case4(self): self.solve(phi=0.5, T=300, width=1.0, P=5.0) @utilities.slow_test def test_case5(self): self.solve(phi=1.0, T=400, width=0.2, P=0.01) def test_fixed_temp(self): gas = ct.Solution("h2o2.yaml") gas.TPX = 400, 2*ct.one_atm, {'H2':0.7, 'O2':0.5, 'AR':1.5} sim = ct.BurnerFlame(gas=gas, width=0.05) sim.burner.mdot = gas.density * 0.15 sim.flame.set_fixed_temp_profile([0, 0.1, 0.9, 1], [400, 1100, 1100, 500]) sim.energy_enabled = False sim.solve(loglevel=0, refine_grid=True) self.assertNear(sim.T[0], 400) self.assertNear(sim.T[-1], 500) self.assertNear(max(sim.T), 1100) def test_blowoff(self): gas = ct.Solution("h2o2.yaml") gas.set_equivalence_ratio(0.4, 'H2', 'O2:1.0, AR:5') gas.TP = 300, ct.one_atm sim = ct.BurnerFlame(gas=gas, width=0.1) sim.burner.mdot = 1.2 sim.set_refine_criteria(ratio=3, slope=0.3, curve=0.5, prune=0) sim.solve(loglevel=0, auto=True) # nonreacting solution self.assertNear(sim.T[-1], sim.T[0], 1e-6) self.assertNear(sim.velocity[-1], sim.velocity[0], 1e-6) self.assertArrayNear(sim.Y[:,0], sim.Y[:,-1], 1e-6, atol=1e-6) def test_restart(self): gas = ct.Solution("h2o2.yaml") gas.set_equivalence_ratio(0.8, 'H2', 'O2:1.0, AR:5') gas.TP = 300, ct.one_atm sim = ct.BurnerFlame(gas=gas, width=0.1) sim.burner.mdot = 1.2 sim.set_refine_criteria(ratio=3, slope=0.3, curve=0.5, prune=0) sim.solve(loglevel=0, auto=True) arr = sim.to_array() sim.burner.mdot = 1.1 sim.set_initial_guess(data=arr) sim.solve(loglevel=0, auto=True) # Check continuity rhou = sim.burner.mdot for rhou_j in sim.density * sim.velocity: self.assertNear(rhou_j, rhou, 1e-4) class TestStagnationFlame(utilities.CanteraTest): def setUp(self): self.gas = ct.Solution("h2o2.yaml") def create_stagnation(self, comp, tsurf, tinlet, mdot, width): p = 0.05 * ct.one_atm # pressure self.gas.TPX = tinlet, p, comp sim = ct.ImpingingJet(gas=self.gas, width=width) sim.inlet.mdot = mdot sim.surface.T = tsurf return sim def run_stagnation(self, xh2, mdot, width): # Simplified version of the example 'stagnation_flame.py' tburner = 373.0 # burner temperature tsurf = 500.0 comp = {'H2': xh2, 'O2': 1, 'AR': 7} sim = self.create_stagnation(comp, tsurf, tburner, mdot, width) sim.set_grid_min(1e-4) sim.set_refine_criteria(3., 0.1, 0.2, 0.06) sim.set_initial_guess(products='equil') # assume adiabatic equilibrium products sim.solve(loglevel=0, auto=True) assert sim.T.max() > tburner + tsurf assert np.allclose(sim.L, sim.L[0]) self.sim = sim def test_stagnation_case1(self): self.run_stagnation(xh2=1.8, mdot=0.06, width=0.2) @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") def test_restore_hdf(self): self.run_save_restore("h5") def test_restore_yaml(self): self.run_save_restore("yaml") def run_save_restore(self, mode): filename = self.test_work_path / f"stagnation.{mode}" filename.unlink(missing_ok=True) self.run_stagnation(xh2=1.8, mdot=0.06, width=0.1) self.sim.save(filename, "test") jet = ct.ImpingingJet(gas=self.gas) jet.restore(filename, "test") self.check_save_restore(jet) def check_save_restore(self, jet): # pytest.approx is used as equality for floats cannot be guaranteed for loaded # HDF5 files if they were created on a different OS and/or architecture assert list(jet.grid) == pytest.approx(list(self.sim.grid)) assert list(jet.T) == pytest.approx(list(self.sim.T), 1e-3) k = self.sim.gas.species_index('H2') assert list(jet.X[k, :]) == pytest.approx(list(self.sim.X[k, :]), 1e-4) settings = self.sim.flame.settings for k, v in jet.flame.settings.items(): assert k in settings if k == "fixed_temperature": # fixed temperature is NaN continue if isinstance(v, dict): for kk, vv in v.items(): if isinstance(vv, float): assert settings[k][kk] == pytest.approx(vv) else: assert settings[k][kk] == vv if isinstance(k, float): assert settings[k] == pytest.approx(v) else: assert settings[k] == v jet.solve(loglevel=0) class TestImpingingJet(utilities.CanteraTest): def setUp(self): self.gas = ct.Solution("ptcombust-simple.yaml", "gas") self.surf_phase = ct.Interface("ptcombust-simple.yaml", "Pt_surf", [self.gas]) def create_reacting_surface(self, comp, tsurf, tinlet, width): self.gas.TPX = tinlet, ct.one_atm, comp self.surf_phase.TP = tsurf, ct.one_atm # integrate the coverage equations holding the gas composition fixed # to generate a good starting estimate for the coverages. self.surf_phase.advance_coverages(1.) return ct.ImpingingJet(gas=self.gas, width=width, surface=self.surf_phase) def run_reacting_surface(self, xch4, tsurf, mdot, width): # Simplified version of the example 'catalytic_combustion.py' tinlet = 300.0 # inlet temperature comp = {'CH4': xch4, 'O2': 0.21, 'N2': 0.79} sim = self.create_reacting_surface(comp, tsurf, tinlet, width) sim.set_refine_criteria(10.0, 0.3, 0.4, 0.0) sim.inlet.mdot = mdot sim.inlet.T = tinlet sim.inlet.X = comp sim.surface.T = tsurf sim.solve(loglevel=0, auto=True) self.assertTrue(all(np.diff(sim.T) > 0)) self.assertTrue(all(np.diff(sim.Y[sim.gas.species_index('CH4')]) < 0)) self.assertTrue(all(np.diff(sim.Y[sim.gas.species_index('CO2')]) > 0)) self.sim = sim def test_reacting_surface_case1(self): self.run_reacting_surface(xch4=0.095, tsurf=900.0, mdot=0.06, width=0.1) @utilities.slow_test def test_reacting_surface_case2(self): self.run_reacting_surface(xch4=0.07, tsurf=1200.0, mdot=0.2, width=0.05) @utilities.slow_test def test_reacting_surface_case3(self): self.run_reacting_surface(xch4=0.2, tsurf=800.0, mdot=0.1, width=0.2) @pytest.mark.usefixtures("allow_deprecated") @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") @pytest.mark.filterwarnings("ignore:.*legacy HDF.*:UserWarning") def test_restore_legacy_hdf(self): # Legacy input file was created using the Cantera 2.6 Python test suite: # - restore_legacy.h5 -> test_onedim.py::TestImpingingJet::test_write_hdf filename = self.test_data_path / f"impingingjet_legacy.h5" self.run_reacting_surface(xch4=0.095, tsurf=900.0, mdot=0.06, width=0.1) jet = ct.ImpingingJet(gas=self.gas, surface=self.surf_phase) jet.restore(filename, "group0") # check with relaxed tolerances to account for differences between # Cantera 2.6 and Cantera 3.1 self.check_save_restore(jet, tol_T=1e-3, tol_X=1e-1) @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") def test_restore_hdf(self): self.run_save_restore("h5") def test_restore_yaml(self): self.run_save_restore("yaml") def run_save_restore(self, mode): filename = self.test_work_path / f"impingingjet.{mode}" filename.unlink(missing_ok=True) self.run_reacting_surface(xch4=0.095, tsurf=900.0, mdot=0.06, width=0.1) self.sim.save(filename, "test") comp = {'CH4': 0.095, 'O2':0.21, 'N2':0.79} jet = self.create_reacting_surface(comp, 700.0, 500., width=0.2) jet.restore(filename, "test") self.check_save_restore(jet) def check_save_restore(self, jet, tol_T=None, tol_X=None): # pytest.approx is used as equality for floats cannot be guaranteed for loaded # HDF5 files if they were created on a different OS and/or architecture assert list(jet.grid) == pytest.approx(list(self.sim.grid)) assert list(jet.T) == pytest.approx(list(self.sim.T), tol_T) k = self.sim.gas.species_index('H2') assert list(jet.X[k, :]) == pytest.approx(list(self.sim.X[k, :]), tol_X) settings = self.sim.flame.settings for k, v in jet.flame.settings.items(): assert k in settings if k == "fixed_temperature": # fixed temperature is NaN continue if isinstance(v, dict): for kk, vv in v.items(): if isinstance(vv, float): assert settings[k][kk] == pytest.approx(vv) else: assert settings[k][kk] == vv if isinstance(k, float): assert settings[k] == pytest.approx(v) else: assert settings[k] == v assert list(jet.surface.surface.X) == pytest.approx(list(self.sim.surface.surface.X)) for i in range(self.sim.surface.n_components): assert self.sim.value("surface", i, 0) == \ pytest.approx(jet.value("surface", i, 0), tol_X) jet.solve(loglevel=0) class TestTwinFlame(utilities.CanteraTest): def solve(self, phi, T, width, P): gas = ct.Solution("h2o2.yaml") gas.TP = T, ct.one_atm gas.set_equivalence_ratio(phi, 'H2', 'O2:1.0, AR:4.0') sim = ct.CounterflowTwinPremixedFlame(gas=gas, width=width) sim.set_refine_criteria(ratio=5, slope=0.8, curve=1.0, prune=0.4) axial_velocity = 2.0 sim.reactants.mdot = gas.density * axial_velocity sim.solve(loglevel=0, auto=True) self.assertGreater(sim.T[-1], T + 100) assert np.allclose(sim.L, sim.L[0]) return sim def test_restart(self): sim = self.solve(phi=0.4, T=300, width=0.05, P=0.1) arr = sim.to_array() axial_velocity = 2.2 sim.reactants.mdot *= 1.1 sim.reactants.T = sim.reactants.T + 100 sim.set_initial_guess(data=arr) sim.solve(loglevel=0, auto=True) # Check inlet mdot = sim.density * sim.velocity self.assertNear(mdot[0], sim.reactants.mdot, 1e-4) self.assertNear(sim.T[0], sim.reactants.T, 1e-4) def test_save_restore_yaml(self): # save and restore using YAML format self.run_save_restore("yaml") @pytest.mark.skipif("native" not in ct.hdf_support(), reason="Cantera compiled without HDF support") def test_save_restore_hdf(self): # save and restore using HDF format self.run_save_restore("h5") def run_save_restore(self, mode): filename = self.test_work_path / f"twinflame.{mode}" filename.unlink(missing_ok=True) sim = self.solve(phi=0.4, T=300, width=0.05, P=0.1) sim.save(filename, compression=7) gas = ct.Solution("h2o2.yaml") sim2 = ct.CounterflowTwinPremixedFlame(gas=gas) sim2.restore(filename) self.assertArrayNear(sim.grid, sim2.grid) self.assertArrayNear(sim.Y, sim2.Y) sim2.solve(loglevel=0) class TestIonFreeFlame(utilities.CanteraTest): @utilities.slow_test def test_ion_profile(self): reactants = 'CH4:0.216, O2:2' p = ct.one_atm Tin = 300 width = 0.03 # Solution object used to compute mixture properties self.gas = ct.Solution('ch4_ion.yaml') self.gas.TPX = Tin, p, reactants self.sim = ct.FreeFlame(self.gas, width=width) assert self.sim.transport_model == 'ionized-gas' self.sim.set_refine_criteria(ratio=4, slope=0.8, curve=1.0) # Ionized species may require tighter absolute tolerances self.sim.flame.set_steady_tolerances(Y=(1e-4, 1e-12)) # stage one self.sim.solve(loglevel=0, auto=True) #stage two self.sim.solve(loglevel=0, stage=2) # Regression test self.assertNear(max(self.sim.E), 149.63179056676853, 1e-3) class TestIonBurnerFlame(utilities.CanteraTest): def test_ion_profile(self): reactants = 'CH4:1.0, O2:2.0, N2:7.52' p = ct.one_atm Tburner = 400 width = 0.01 # Solution object used to compute mixture properties self.gas = ct.Solution('ch4_ion.yaml') self.gas.TPX = Tburner, p, reactants self.sim = ct.BurnerFlame(self.gas, width=width) assert self.sim.transport_model == 'ionized-gas' self.sim.set_refine_criteria(ratio=4, slope=0.1, curve=0.1) self.sim.burner.mdot = self.gas.density * 0.15 self.sim.solve(loglevel=0, stage=2, auto=True) # Regression test self.assertNear(max(self.sim.E), 591.76, 1e-2) self.assertNear(max(self.sim.X[self.gas.species_index('E')]), 8.024e-9, 1e-2)