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

1import numpy as np 

2 

3from pySDC.core.errors import ParameterError 

4 

5 

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. 

9 

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. 

15 

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``. 

19 

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 """ 

36 

37 unknowns = u_num['unknowns'] 

38 u_num_tmp = {unknown: u_num[unknown][-1] for unknown in unknowns} 

39 

40 got = {} 

41 got = u_num_tmp 

42 

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 ) 

75 

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 ) 

101 

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 ) 

130 

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 ) 

150 

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 ) 

183 

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 ) 

209 

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 ) 

238 

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 ) 

258 

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 ) 

293 

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 ) 

321 

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 ) 

352 

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 ) 

374 

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 ) 

405 

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 ) 

426 

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 ) 

449 

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 ) 

472 

473 else: 

474 raise ParameterError(f"For {prob_cls_name} there is no test implemented here!") 

475 

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} 

495 

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