-
-
Notifications
You must be signed in to change notification settings - Fork 407
Expand file tree
/
Copy pathtest_onedim.py
More file actions
2089 lines (1688 loc) · 78 KB
/
test_onedim.py
File metadata and controls
2089 lines (1688 loc) · 78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import numpy as np
import pytest
from pytest import approx
import re
pd = pytest.importorskip("pandas")
import cantera as ct
from .utilities import yaml
from .utilities import (
compareProfiles
)
class TestOnedim:
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))
assert 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
assert inlet.Y == approx(Yref)
assert inlet.X == approx(Xref)
gas2.TPX = 400, 101325, 'H2:0.5, O2:0.2, AR:0.3'
Xref = gas2.X
Yref = gas2.Y
inlet.X = Xref
assert inlet.X == approx(Xref)
assert inlet.Y == approx(Yref)
inlet.X = {'H2':0.3, 'O2':0.5, 'AR':0.2}
assert inlet.X[gas2.species_index('H2')] == approx(0.3)
def test_grid_check(self):
gas = ct.Solution("h2o2.yaml")
flame = ct.FreeFlow(gas)
with pytest.raises(ct.CanteraError, match='monotonically'):
flame.grid = [0, 0.1, 0.1, 0.2]
with pytest.raises(ct.CanteraError, match='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 pytest.raises(NotImplementedError):
pickle.dumps(flame)
def test_uncopyable(self):
import copy
gas = ct.Solution("h2o2.yaml")
flame = ct.FreeFlow(gas)
with pytest.raises(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 pytest.raises(AttributeError):
x.foobar = 300
with pytest.raises(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 pytest.raises(ct.CanteraError, match='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())
assert atol_ss == set((5e-5, 3e-6, 7e-9))
assert atol_ts == set((6e-5, 4e-6, 2e-9))
assert rtol_ss == set((5e-3, 3e-4, 7e-7))
assert 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'
with pytest.raises(ct.CanteraError, match="Invalid Transport model"):
flame.transport_model = 'none'
gas.transport_model = 'unity-Lewis-number'
with pytest.raises(ct.CanteraError, match="Invalid Transport model"):
gas.transport_model = 'none'
def test_width_grid(self):
gas = ct.Solution('h2o2.yaml')
for cls in ct.FlameBase.__subclasses__():
with pytest.raises(ValueError, match="mutually exclusive"):
sim = cls(gas, grid=[0, 0.1, 0.2], width=0.4)
def check_component_order(fname: str, group: str):
with fname.open("r", encoding="utf-8") as fid:
reader = yaml.YAML(typ="safe")
contents = reader.load(fid)
components = []
index = -1
for key, value in contents[group]["flame"].items():
if components:
index += 1
field = components.pop()
msg = f"Found mismatch of components at index {index}: expected {field!r}"
assert key == field, msg
if key == "components":
components = value[::-1]
class TestFreeFlame:
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, loglevel=0):
# Solve with the energy equation disabled
self.sim.energy_enabled = False
self.sim.solve(loglevel=loglevel, refine_grid=False)
assert not self.sim.energy_enabled
def solve_mix(self, ratio=3.0, slope=0.3, curve=0.2, prune=0.0, refine=True,
auto=False):
# 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, auto=auto)
assert self.sim.energy_enabled
assert self.sim.transport_model == 'mixture-averaged'
def solve_multi(self):
self.sim.transport_model = 'multicomponent'
self.sim.solve(loglevel=0, refine_grid=True)
assert self.sim.transport_model == 'multicomponent'
data = self.test_work_path / f"multicomponent.yaml"
data.unlink(missing_ok=True)
group = "multicomponent"
self.sim.save(data, group)
arr = ct.SolutionArray(self.sim.gas)
arr.restore(data, "multicomponent/flame")
assert arr.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)
assert self.sim.fixed_temperature == approx(tfixed)
assert self.sim.fixed_temperature_location == approx(zfixed)
@pytest.mark.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
assert Tad == approx(self.sim.T[-1], rel=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)
assert self.sim.grid[-1] == approx(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)
assert self.sim.velocity[0] == approx(17.02, rel=1e-1)
assert self.sim.grid[-1] < 2.0 # grid should not be too wide
assert self.sim.flame.tolerances("T") == (2e-4, 1e-8)
@pytest.mark.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]
assert abs(T2-Tad) < abs(T1-Tad)
assert abs(T3-Tad) < abs(T2-Tad)
for k in range(self.gas.n_species):
if Xad[k] > self.tol_ss[1]:
assert abs(X2[k]-Xad[k]) <= abs(X1[k]-Xad[k])
assert 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:
assert rhou_j == approx(rhou, rel=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)
assert self.sim.grid == approx(flow.grid)
assert self.sim.T == approx(flow.T)
f2 = ct.FreeFlame(self.gas)
f2.from_array(flow)
assert self.sim.grid == approx(f2.grid)
assert self.sim.T == approx(f2.T)
inlet = self.sim.to_array(self.sim.inlet)
f2.from_array(inlet, f2.inlet)
assert self.sim.inlet.T == approx(f2.inlet.T)
assert self.sim.inlet.mdot == approx(f2.inlet.mdot)
assert self.sim.inlet.Y == approx(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
filename, group = self.run_restart("yaml")
check_component_order(filename, group)
@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")
@pytest.mark.skipif(not pd, reason="Pandas not installed")
def test_restart_pandas(self):
# restart from Pandas DataFrame
self.run_restart("pandas", auto=True)
def run_restart(self, mode, auto=False):
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()
elif mode == "pandas":
data = self.sim.to_pandas()
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, auto=auto)
# Check continuity
rhou = self.sim.inlet.mdot
for rhou_j in self.sim.density * self.sim.velocity:
assert rhou_j == approx(rhou, rel=1e-4)
return data, group
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",
"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_log_solution_bounds(self, capsys):
self.create_sim(p=ct.one_atm, Tin=300, reactants="H2:1.1, O2:1, AR:5")
self.solve_fixed_T(loglevel=4)
out = capsys.readouterr().out
assert "Undamped Newton step takes solution out of bounds" in out
m = re.search(r" ={60,}\n(.*?)\n ={60,}\n(.*?)\n ={60,}", out,
re.DOTALL)
assert m is not None
header = m.group(1).splitlines()
body = m.group(2).splitlines()
assert re.match(" +Value +Min +Max +$", header[0])
assert re.match(" +Domain +Pt. Component +Value +Change +Bound +Bound +$",
header[1])
assert len(body) > 0
for line in body:
assert re.match(r" +flame +\d+ +[A-Z0-9]+ +([0-9.e+\-]+ *){4}$", line)
def test_timestep_jacobian_limits(self, capsys):
# Use log output to confirm that maximum timestep count and Jacobian age
# limits are active
self.create_sim(p=ct.one_atm, Tin=300, reactants="H2:1.1, O2:1, AR:5")
self.sim.set_time_step(1e-8, [2, 3, 10])
self.sim.set_max_jac_age(3, 5)
self.solve_fixed_T(loglevel=3)
out = capsys.readouterr().out
assert "Maximum Jacobian age reached (3)" in out
assert "Maximum Jacobian age reached (5)" in out
assert "Attempt 2 timesteps" in out
assert "Attempt 3 timesteps" in out
assert "Attempt 10 timesteps" in out
def test_mixture_averaged_case1(self):
self.run_mix(phi=0.65, T=300, width=0.03, p=1.0, refine=True)
@pytest.mark.slow_test
def test_mixture_averaged_case2(self):
self.run_mix(phi=0.5, T=300, width=2.0, p=1.0, refine=False)
@pytest.mark.slow_test
def test_mixture_averaged_case3(self):
self.run_mix(phi=0.5, T=500, width=0.05, p=1.0, refine=False)
@pytest.mark.slow_test
def test_mixture_averaged_case4(self):
self.run_mix(phi=0.7, T=400, width=2.0, p=5.0, refine=False)
@pytest.mark.slow_test
def test_mixture_averaged_case5(self):
self.run_mix(phi=1.0, T=300, width=2.0, p=5.0, refine=False)
@pytest.mark.slow_test
def test_mixture_averaged_case6(self):
self.run_mix(phi=1.5, T=300, width=0.2, p=1.0, refine=True)
@pytest.mark.slow_test
def test_mixture_averaged_case7(self):
self.run_mix(phi=1.5, T=500, width=2.0, p=0.1, refine=False)
@pytest.mark.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_mixture_averaged_case9(self):
self.run_mix(phi=0.8, T=180, width=0.05, p=1.0, refine=False)
assert self.sim.flame.bounds('T')[0] < 190
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)
assert fwd == approx(dSdk_adj[m], rel=5e-3, abs=1e-7)
def test_adjoint_sensitivities2(self):
self.gas = ct.Solution('gri30.yaml')
self.gas.TP = 300, 101325
self.gas.set_equivalence_ratio(1.0, 'CH4', {"O2": 1.0, "N2": 3.76})
self.flame = ct.FreeFlame(self.gas, width=0.014)
self.flame.set_refine_criteria(ratio=3, slope=0.07, curve=0.14)
self.flame.solve(loglevel=0, refine_grid=False)
# Adjoint sensitivities
ix = -1
species = "NO"
dk = 1e-4
adjoint_sensitivities = self.flame.get_species_reaction_sensitivities(species, ix)
reaction_equations = self.gas.reaction_equations()
adjoint_sens = [(reaction, sensitivity) for reaction, sensitivity in
zip(reaction_equations, adjoint_sensitivities)]
adjoint_sens_sorted = sorted(adjoint_sens, key=lambda x: abs(x[1]), reverse=True)
Su0 = self.flame.X[self.gas.species_index(species), ix]
fwd_sensitivities = []
for m in range(self.flame.gas.n_reactions):
self.flame.gas.set_multiplier(1.0) # reset all multipliers
self.flame.gas.set_multiplier(1 + dk, m) # perturb reaction m
self.flame.solve(loglevel=0, refine_grid=False)
Suplus = self.flame.X[self.gas.species_index(species), ix]
self.flame.gas.set_multiplier(1 - dk, m) # perturb reaction m
self.flame.solve(loglevel=0, refine_grid=False)
Suminus = self.flame.X[self.gas.species_index(species), ix]
fwd_sensitivities.append((Suplus - Suminus) / (2 * Su0 * dk))
fwd_sens = [(reaction, sensitivity) for reaction, sensitivity in zip(reaction_equations, fwd_sensitivities)]
fwd_sens_sorted = sorted(fwd_sens, key=lambda x: abs(x[1]), reverse=True)
# Extract top 10 reactions from both lists
top_reactions_adjoint = [reaction for reaction, _ in adjoint_sens_sorted[:10]]
top_reactions_fwd = [reaction for reaction, _ in fwd_sens_sorted[:10]]
# Count common reactions
common_reactions = list(set(top_reactions_fwd) & set(top_reactions_adjoint))
# Assert that at least 8 reactions match
assert len(
common_reactions) >= 8, f"Only {len(common_reactions)} Fwd and adjoint based species sensitivities do not match"
def test_jacobian_options(self):
reactants = {'H2': 0.65, 'O2': 0.5, 'AR': 2}
self.create_sim(p=ct.one_atm, Tin=300, reactants=reactants, width=0.03)
assert isinstance(self.sim.linear_solver, ct.BandedJacobian)
self.sim.linear_solver = ct.EigenSparseDirectJacobian()
self.sim.set_jacobian_perturbation(1e-7, 1e-12, 1e-20)
self.solve_mix(refine=True)
# regression value matching test_mixture_averaged_case1
assert self.sim.velocity[0] == approx(1.693407, rel=1e-4)
# @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]
assert not self.sim.soret_enabled
assert Su_mix == approx(Su_multi, rel=5e-2)
assert 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)
assert self.sim.soret_enabled
Su_soret = self.sim.velocity[0]
assert Su_multi == approx(Su_soret, rel=2e-1)
assert 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 = np.ptp(self.sim.enthalpy_mass)
self.sim.transport_model = 'mixture-averaged'
self.sim.solve(loglevel=0)
dh_mix = np.ptp(self.sim.enthalpy_mass)
# deviation of enthalpy should be much lower for unity Le model (tends
# towards zero as grid is refined)
assert 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):
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)
self.sim.solve(loglevel=0, auto=True)
sl_without_Soret = self.sim.velocity[0]
self.sim.soret_enabled = True
self.sim.solve(loglevel=0, auto=True)
sl_with_Soret = self.sim.velocity[0]
assert sl_with_Soret < sl_without_Soret
assert sl_with_Soret > 0
self.sim.transport_model = 'multicomponent'
self.sim.solve(loglevel=0, auto=False)
sl_multi_Soret = self.sim.velocity[0]
# flame speeds should not be exactly equal
# but they should be close even on a coarse mesh
assert abs(sl_multi_Soret-sl_with_Soret) < 0.1
@pytest.mark.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)
assert 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 == approx(ct.one_atm)
assert len(self.sim.grid) == 9
def test_fixed_restore_yaml(self):
# save and restore using YAML format
filename, group = self.run_fixed_restore("yaml")
check_component_order(filename, group)
@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")
assert rtol == approx(T_rtol)
assert atol == approx(T_atol)
assert not self.sim.energy_enabled
P2a = self.sim.P
assert p == approx(P1)
assert P1 == approx(P2a)
Y2 = self.sim.Y
u2 = self.sim.velocity
V2 = self.sim.spread_rate
T2 = self.sim.T
assert Y1 == approx(Y2)
assert u1 == approx(u2)
assert V1 == approx(V2)
assert T1 == approx(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.
assert Y1 == approx(Y3, rel=3e-3)
assert u1 == approx(u3, rel=1e-3)
assert V1 == approx(V3, rel=1e-3)
assert not self.sim.radiation_enabled
assert not self.sim.soret_enabled
self.sim.restore(filename, "test2")
assert self.sim.radiation_enabled
assert self.sim.boundary_emissivities == (0.3, 0.8)
self.sim.solve(loglevel=0)
return filename, "test"
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
@pytest.mark.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"
filename.unlink(missing_ok=True)
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
assert self.sim.soret_enabled
assert self.sim.max_grid_points == 234
assert T1 == approx(T2)
for k1, species in enumerate(gas1.species_names):
k2 = gas2.species_index(species)
assert Y1[k1] == approx(Y2[k2])
@pytest.mark.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"
filename.unlink(missing_ok=True)
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
assert T1 == approx(T2)
for k2, species in enumerate(gas2.species_names):
k1 = gas1.species_index(species)
assert Y1[k1] == approx(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)
assert data.grid == approx(self.sim.grid)
assert data.T == approx(self.sim.T)
k = self.gas.species_index('H2')
assert data.X[:, k] == approx(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, test_data_path):
# Legacy input file was created using the Cantera 2.6 Python test suite:
# - restore_legacy.h5 -> test_onedim.py::TestFreeFlame::test_write_hdf
filename = 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'] == ct.__git_commit__
self.check_save_restore(f)
def check_save_restore(self, f, tol_T=None, tol_X=None):
# 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) == approx(list(self.sim.grid))
assert list(f.T) == approx(list(self.sim.T), rel=tol_T)
k = self.gas.species_index('H2')
assert list(f.X[k, :]) == approx(list(self.sim.X[k, :]), rel=tol_X)
assert list(f.inlet.X) == 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] == approx(vv)
else:
assert two[k][kk] == vv
elif isinstance(v, float):
assert two[k] == 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 pytest.raises(ct.CanteraError, match='Refiner::setCriteria'):
vals = list(good)
vals[i] = bad[i]
self.sim.set_refine_criteria(*vals)