Coverage for pySDC/projects/PinTSimE/hardcoded_solutions.py: 100%
140 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
1import numpy as np
3from pySDC.core.errors import ParameterError
6def testSolution(u_num, prob_cls_name, dt, use_adaptivity, use_detection, maxiter):
7 r"""
8 Test for numerical solution if values satisfy hardcoded values.
10 Note
11 ----
12 Only the items are tested which does make sense for any problem. For instance, `getDataDict` stores the global error
13 after each step **for each problem class**. Since an exact solution is only available for e.g. ``DiscontinuousTestODE``,
14 a test is only be done for this problem class.
16 Hardcoded solutions are computed for only one collocation node ``M_fix``, which is specified for each problem in the
17 related files, see ``pySDC.projects.PinTSimE.battery_model``, ``pySDC.projects.PinTSimE.estimation_check`` and
18 ``pySDC.projects.PinTSimE.discontinuous_test_ODE``.
20 Parameters
21 ----------
22 u_num : dict
23 Contains the numerical solution together with event time found by event detection, step sizes adjusted
24 via adaptivity and/or switch estimation.
25 prob_cls_name : str
26 Indicates which problem class is tested.
27 dt : float
28 (Initial) step sizes used for the simulation.
29 use_adaptivity : bool
30 Indicates whether adaptivity is used in the simulation or not.
31 use_detection : bool
32 Indicates whether discontinuity handling is used in the simulation or not.
33 maxiter : int
34 Number of iterations per step, which is the order adaptivity works with (``restol=-1`` with adaptivity).
35 """
37 unknowns = u_num['unknowns']
38 u_num_tmp = {unknown: u_num[unknown][-1] for unknown in unknowns}
40 got = {}
41 got = u_num_tmp
43 if prob_cls_name == 'battery':
44 if use_adaptivity and use_detection:
45 msg = f"Error when using switch estimator and adaptivity for {prob_cls_name} for dt={dt:.1e}:"
46 if dt == 1e-2:
47 expected = {
48 'iL': 0.5393867577881641,
49 'vC': 0.9999999999913842,
50 't_switches': [0.1823215567921536, 0.18232155679215356, 0.18232155679173784],
51 'dt': 0.09453745651144455,
52 'e_em': 1.7587042933087105e-12,
53 'sum_restarts': 15.0,
54 'sum_niters': 280.0,
55 }
56 elif dt == 1e-3:
57 expected = {
58 'iL': 0.5393867577223005,
59 'vC': 0.9999999999813279,
60 't_switches': [0.18232155676894835, 0.1823215567897308, 0.18232155678877865],
61 'dt': 0.06467602356229402,
62 'e_em': 1.1468603844377867e-13,
63 'sum_restarts': 17.0,
64 'sum_niters': 368.0,
65 }
66 got.update(
67 {
68 't_switches': u_num['t_switches'],
69 'dt': u_num['dt'][-1, 1],
70 'e_em': u_num['e_em'][-1, 1],
71 'sum_restarts': u_num['sum_restarts'],
72 'sum_niters': u_num['sum_niters'],
73 }
74 )
76 elif not use_adaptivity and use_detection:
77 msg = f"Error when using switch estimator for {prob_cls_name} for dt={dt:.1e}:"
78 if dt == 1e-2:
79 expected = {
80 'iL': 0.5456190026495924,
81 'vC': 0.9991666666670524,
82 't_switches': [0.18232155679395579, 0.18232155679395592, 0.18232155679356965],
83 'sum_restarts': 14.0,
84 'sum_niters': 416.0,
85 }
86 elif dt == 1e-3:
87 expected = {
88 'iL': 0.5403849766797957,
89 'vC': 0.9999166666675774,
90 't_switches': [0.18232155679395004, 0.18232155679303919],
91 'sum_restarts': 11.0,
92 'sum_niters': 2536.0,
93 }
94 got.update(
95 {
96 't_switches': u_num['t_switches'],
97 'sum_restarts': u_num['sum_restarts'],
98 'sum_niters': u_num['sum_niters'],
99 }
100 )
102 if use_adaptivity and not use_detection:
103 msg = f"Error when using adaptivity for {prob_cls_name} for dt={dt:.1e}:"
104 if dt == 1e-2:
105 expected = {
106 'iL': 0.4433805288639916,
107 'vC': 0.90262388393713,
108 'dt': 0.18137307612335937,
109 'e_em': 2.7177844974524135e-09,
110 'sum_restarts': 0.0,
111 'sum_niters': 24.0,
112 }
113 elif dt == 1e-3:
114 expected = {
115 'iL': 0.3994744179584864,
116 'vC': 0.9679037468770668,
117 'dt': 0.1701392217033212,
118 'e_em': 2.0992988458701234e-09,
119 'sum_restarts': 0.0,
120 'sum_niters': 32.0,
121 }
122 got.update(
123 {
124 'dt': u_num['dt'][-1, 1],
125 'e_em': u_num['e_em'][-1, 1],
126 'sum_restarts': u_num['sum_restarts'],
127 'sum_niters': u_num['sum_niters'],
128 }
129 )
131 elif not use_adaptivity and not use_detection:
132 msg = f'Error for {prob_cls_name} for dt={dt:.1e}:'
133 if dt == 1e-2:
134 expected = {
135 'iL': 0.5456625861551172,
136 'vC': 0.9973251377556902,
137 'sum_niters': 240.0,
138 }
139 elif dt == 1e-3:
140 expected = {
141 'iL': 0.538639340748308,
142 'vC': 0.9994050706403905,
143 'sum_niters': 2400.0,
144 }
145 got.update(
146 {
147 'sum_niters': u_num['sum_niters'],
148 }
149 )
151 elif prob_cls_name == 'battery_implicit':
152 if use_adaptivity and use_detection:
153 msg = f"Error when using switch estimator and adaptivity for {prob_cls_name} for dt={dt:.1e}:"
154 if dt == 1e-2:
155 expected = {
156 'iL': 0.5393867577468375,
157 'vC': 0.9999999999980123,
158 't_switches': [0.18232155680038617, 0.1823215568023739],
159 'dt': 0.08896232033732146,
160 'e_em': 2.220446049250313e-16,
161 'sum_restarts': 15.0,
162 'sum_niters': 280.0,
163 }
164 elif dt == 1e-3:
165 expected = {
166 'iL': 0.5393867576415584,
167 'vC': 0.9999999999802239,
168 't_switches': [0.18232155678530526, 0.1823215568066914, 0.1823215568057151],
169 'dt': 0.06333183541149384,
170 'e_em': 2.220446049250313e-16,
171 'sum_restarts': 17.0,
172 'sum_niters': 368.0,
173 }
174 got.update(
175 {
176 't_switches': u_num['t_switches'],
177 'dt': u_num['dt'][-1, 1],
178 'e_em': u_num['e_em'][-1, 1],
179 'sum_restarts': u_num['sum_restarts'],
180 'sum_niters': u_num['sum_niters'],
181 }
182 )
184 elif not use_adaptivity and use_detection:
185 msg = f"Error when using switch estimator for {prob_cls_name} for dt={dt:.1e}:"
186 if dt == 1e-2:
187 expected = {
188 'iL': 0.5490992952561473,
189 'vC': 0.9999999999982524,
190 't_switches': [0.1823215567992934, 0.18232155680104123],
191 'sum_restarts': 14.0,
192 'sum_niters': 416.0,
193 }
194 elif dt == 1e-3:
195 expected = {
196 'iL': 0.5403849766692059,
197 'vC': 0.9999166666681585,
198 't_switches': [0.18232155679522236, 0.1823215567943357],
199 'sum_restarts': 11.0,
200 'sum_niters': 2536.0,
201 }
202 got.update(
203 {
204 't_switches': u_num['t_switches'],
205 'sum_restarts': u_num['sum_restarts'],
206 'sum_niters': u_num['sum_niters'],
207 }
208 )
210 if use_adaptivity and not use_detection:
211 msg = f"Error when using adaptivity for {prob_cls_name} for dt={dt:.1e}:"
212 if dt == 1e-2:
213 expected = {
214 'iL': 0.4694087102919169,
215 'vC': 0.9026238839418407,
216 'dt': 0.18137307612335937,
217 'e_em': 2.3469713394952407e-09,
218 'sum_restarts': 0.0,
219 'sum_niters': 24.0,
220 }
221 elif dt == 1e-3:
222 expected = {
223 'iL': 0.39947441811958956,
224 'vC': 0.9679037468856341,
225 'dt': 0.1701392217033212,
226 'e_em': 1.147640815712947e-09,
227 'sum_restarts': 0.0,
228 'sum_niters': 32.0,
229 }
230 got.update(
231 {
232 'dt': u_num['dt'][-1, 1],
233 'e_em': u_num['e_em'][-1, 1],
234 'sum_restarts': u_num['sum_restarts'],
235 'sum_niters': u_num['sum_niters'],
236 }
237 )
239 elif not use_adaptivity and not use_detection:
240 msg = f'Error for {prob_cls_name} for dt={dt:.1e}:'
241 if dt == 1e-2:
242 expected = {
243 'iL': 0.5456915668459889,
244 'vC': 0.9973251377578705,
245 'sum_niters': 240.0,
246 }
247 elif dt == 1e-3:
248 expected = {
249 'iL': 0.5386399890100035,
250 'vC': 0.999405070641239,
251 'sum_niters': 2400.0,
252 }
253 got.update(
254 {
255 'sum_niters': u_num['sum_niters'],
256 }
257 )
259 elif prob_cls_name == 'battery_n_capacitors':
260 if use_adaptivity and use_detection:
261 msg = f"Error when using switch estimator and adaptivity for {prob_cls_name} for dt={dt:.1e}:"
262 if dt == 1e-2:
263 expected = {
264 'iL': 0.6125019898578352,
265 'vC1': 1.0000000000471956,
266 'vC2': 1.0000000000165106,
267 't_switches': [0.18232155678158268, 0.36464311353802376],
268 'dt': 0.0985931246953285,
269 'e_em': 2.295386103412511e-12,
270 'sum_restarts': 24.0,
271 'sum_niters': 552.0,
272 }
273 elif dt == 1e-3:
274 expected = {
275 'iL': 0.6125019901065321,
276 'vC1': 1.0000000000787372,
277 'vC2': 1.000000000028657,
278 't_switches': [0.1823215567907939, 0.3646431134803315],
279 'dt': 0.07154669986159717,
280 'e_em': 2.3381296898605797e-13,
281 'sum_restarts': 22.0,
282 'sum_niters': 472.0,
283 }
284 got.update(
285 {
286 't_switches': u_num['t_switches'],
287 'dt': u_num['dt'][-1, 1],
288 'e_em': u_num['e_em'][-1, 1],
289 'sum_restarts': u_num['sum_restarts'],
290 'sum_niters': u_num['sum_niters'],
291 }
292 )
294 elif not use_adaptivity and use_detection:
295 msg = f"Error when using switch estimator for {prob_cls_name} for dt={dt:.1e}:"
296 if dt == 1e-2:
297 expected = {
298 'iL': 0.6313858468030417,
299 'vC1': 1.0000000002414198,
300 'vC2': 1.0000000000095093,
301 't_switches': [0.18232155679395579, 0.3646431133464922],
302 'sum_restarts': 19.0,
303 'sum_niters': 664.0,
304 }
305 elif dt == 1e-3:
306 expected = {
307 'iL': 0.6151254295045797,
308 'vC1': 1.0000000000227713,
309 'vC2': 1.0000000000329365,
310 't_switches': [0.18232155680153855, 0.3646431135651182],
311 'sum_restarts': 16.0,
312 'sum_niters': 4224.0,
313 }
314 got.update(
315 {
316 't_switches': u_num['t_switches'],
317 'sum_restarts': u_num['sum_restarts'],
318 'sum_niters': u_num['sum_niters'],
319 }
320 )
322 if use_adaptivity and not use_detection:
323 msg = f"Error when using adaptivity for {prob_cls_name} for dt={dt:.1e}:"
324 if dt == 1e-2:
325 expected = {
326 'iL': 0.15890544838473294,
327 'vC1': 0.8806086293336285,
328 'vC2': 0.9915019206727803,
329 'dt': 0.38137307612335936,
330 'e_em': 4.145817911194172e-09,
331 'sum_restarts': 0.0,
332 'sum_niters': 24.0,
333 }
334 elif dt == 1e-3:
335 expected = {
336 'iL': 0.15422467570971707,
337 'vC1': 0.8756872272783145,
338 'vC2': 0.9971015415168025,
339 'dt': 0.3701392217033212,
340 'e_em': 3.6970297934146856e-09,
341 'sum_restarts': 0.0,
342 'sum_niters': 32.0,
343 }
344 got.update(
345 {
346 'dt': u_num['dt'][-1, 1],
347 'e_em': u_num['e_em'][-1, 1],
348 'sum_restarts': u_num['sum_restarts'],
349 'sum_niters': u_num['sum_niters'],
350 }
351 )
353 elif not use_adaptivity and not use_detection:
354 msg = f'Error for {prob_cls_name} for dt={dt:.1e}:'
355 if dt == 1e-2:
356 expected = {
357 'iL': 0.5939796175551723,
358 'vC1': 0.9973251377556902,
359 'vC2': 0.9973251377466236,
360 'sum_niters': 400.0,
361 }
362 elif dt == 1e-3:
363 expected = {
364 'iL': 0.6107051960885036,
365 'vC1': 0.9994050706403905,
366 'vC2': 0.9997382611893499,
367 'sum_niters': 4000.0,
368 }
369 got.update(
370 {
371 'sum_niters': u_num['sum_niters'],
372 }
373 )
375 elif prob_cls_name == 'DiscontinuousTestODE':
376 if not use_adaptivity and use_detection:
377 msg = f"Error when using switch estimator for {prob_cls_name} for dt={dt:.1e}:"
378 if dt == 1e-2:
379 expected = {
380 'u': 5.998326995729771,
381 'e_global': 0.0041911003550794135,
382 't_switches': [1.6094379124660123],
383 'e_event': [3.1912028575220575e-11],
384 'sum_restarts': 22.0,
385 'sum_niters': 675.0,
386 }
387 elif dt == 1e-3:
388 expected = {
389 'u': 5.9721869476933005,
390 'e_global': 0.0004191100622819022,
391 't_switches': [1.6094379124262566, 1.6094379124260099],
392 'e_event': [7.843725668976731e-12],
393 'sum_restarts': 20.0,
394 'sum_niters': 2352.0,
395 }
396 got.update(
397 {
398 't_switches': u_num['t_switches'],
399 'sum_restarts': u_num['sum_restarts'],
400 'sum_niters': u_num['sum_niters'],
401 'e_global': u_num['e_global'][-1, 1],
402 'e_event': u_num['e_event'],
403 }
404 )
406 elif not use_adaptivity and not use_detection:
407 msg = f"Error for {prob_cls_name} for dt={dt:.1e}:"
408 if dt == 1e-2:
409 expected = {
410 'u': 5.9805345175338225,
411 'e_global': 0.009855041056925806,
412 'sum_niters': 527.0,
413 }
414 elif dt == 1e-3:
415 expected = {
416 'u': 5.971255816863172,
417 'e_global': 0.0005763403865506334,
418 'sum_niters': 2225.0,
419 }
420 got.update(
421 {
422 'sum_niters': u_num['sum_niters'],
423 'e_global': u_num['e_global'][-1, 1],
424 }
425 )
427 elif prob_cls_name == 'piline':
428 if not use_adaptivity and not use_detection:
429 msg = f"Error for {prob_cls_name} for dt={dt:.1e}:"
430 if dt == 5e-2:
431 expected = {
432 'vC1': 83.97535096068474,
433 'vC2': 80.52142367760014,
434 'iLp': 16.096505806313573,
435 'sum_niters': 2045.0,
436 }
437 elif dt == 1e-2:
438 expected = {
439 'vC1': 83.97535088144014,
440 'vC2': 80.52142375242333,
441 'iLp': 16.09650585135131,
442 'sum_niters': 7200.0,
443 }
444 got.update(
445 {
446 'sum_niters': u_num['sum_niters'],
447 }
448 )
450 elif prob_cls_name == 'buck_converter':
451 if not use_adaptivity and not use_detection:
452 msg = f"Error for {prob_cls_name} for dt={dt:.1e}:"
453 if dt == 2e-5:
454 expected = {
455 'vC1': 9.781955920747619,
456 'vC2': 6.396971204930281,
457 'iLp': -1.1056614708409171,
458 'sum_niters': 2519.0,
459 }
460 elif dt == 1e-5:
461 expected = {
462 'vC1': 9.782142840662102,
463 'vC2': 6.388775533709242,
464 'iLp': -1.0994027552202539,
465 'sum_niters': 4241.0,
466 }
467 got.update(
468 {
469 'sum_niters': u_num['sum_niters'],
470 }
471 )
473 else:
474 raise ParameterError(f"For {prob_cls_name} there is no test implemented here!")
476 # The runs are deterministic: the iteration and restart counts are reproduced exactly, and all other values
477 # were reproduced to <= 3e-15 relative (<= 6e-14 absolute), except for one final step size (1.3e-6 relative).
478 # - counts are integers and compared exactly
479 # - e_em and e_event are errors that can be at round-off level (down to 2e-16) -> relative 1e-3, but at
480 # least 1e-14 absolute, which makes it an upper bound for the ones near round-off
481 # - dt is the last step size chosen by adaptivity, from the embedded error estimate as dt ~ e_em^(-1/k) with
482 # k = maxiter: it is known as well as e_em is, divided by k. With e_em at round-off, a relative 1e-4 was
483 # tighter than that: one ulp in QI moves the battery_n_capacitors step size by 3e-4.
484 # - everything else (solution values, event times, global errors) -> relative 1e-10, absolute 1e-12
485 e_em_tols = {'rtol': 1e-3, 'atol': 1e-14}
486 for key in expected.keys():
487 if key in ['sum_niters', 'sum_restarts']:
488 tols = {'rtol': 0, 'atol': 0}
489 elif key in ['e_em', 'e_event']:
490 tols = e_em_tols
491 elif key == 'dt':
492 tols = {'rtol': (e_em_tols['rtol'] + e_em_tols['atol'] / abs(expected['e_em'])) / maxiter, 'atol': 0}
493 else:
494 tols = {'rtol': 1e-10, 'atol': 1e-12}
496 if key == 't_switches' or key == 'e_event':
497 err_msg = f'{msg} Expected {key}={expected[key]}, got {key}={got[key]}'
498 assert len(expected[key]) == len(got[key]), err_msg
499 assert np.allclose(got[key], expected[key], **tols), err_msg
500 else:
501 err_msg = f'{msg} Expected {key}={expected[key]:.15e}, got {key}={got[key]:.15e}'
502 assert np.isclose(got[key], expected[key], **tols), err_msg