Coverage for pySDC/tutorial/step_3/B_adding_statistics.py: 100%
31 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-29 12:50 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-29 12:50 +0000
1# ---
2# jupyter:
3# jupytext:
4# formats: py:percent
5# kernelspec:
6# display_name: Python 3
7# name: python3
8# ---
10# %% [markdown]
11# # Part B: Adding statistics
12#
13# Now we extend the statistics with entries of our own. To make things more interesting (and complicated), this
14# part also brings a new problem, a new sweeper and new data types:
15#
16# - `PenningTrap_3D`: particles in a Penning trap, held in place by an electric and a magnetic field,
17# - `boris_2nd_order`: SDC for second-order problems with the Boris method, see
18# [this paper](http://dx.doi.org/10.1016/j.jcp.2015.04.022),
19# - `particles` for positions, velocities, charges and masses, and `fields` for the electric and magnetic field.
20#
21# An important measure for this kind of problem is the total energy of the system, which we would like to compute
22# after each time step. That is a job for a new **hook**.
23#
24# ## A hook of our own
25#
26# Hooks are called by the controller at fixed points of a run: before it, before and after each step, iteration
27# or sweep, and after it. `particle_hook`, in `HookClass_Particles.py` next to this tutorial, computes the total
28# energy before the run and after each step and adds it to the statistics with `add_to_stats`, as type `'etot'`.
29# This is what it does after a step:
30#
31# :::{literalinclude} HookClass_Particles.py
32# :pyobject: particle_hook.post_step
33# :::
34#
35# Our hook does not replace pySDC's default statistics: those come from `DefaultHooks`, which the controller always
36# adds next to ours. The call to `super()` first still matters: the base class keeps track of restarted steps, and
37# `add_to_stats` writes that into the key of each entry.
39# %%
40import numpy as np
41from pathlib import Path
43from pySDC.helpers.stats_helper import get_list_of_types, get_sorted
44from pySDC.implementations.controller_classes.controller_nonMPI import controller_nonMPI
45from pySDC.implementations.problem_classes.PenningTrap_3D import penningtrap
46from pySDC.implementations.sweeper_classes.boris_2nd_order import boris_2nd_order
47from pySDC.tutorial.step_3.HookClass_Particles import particle_hook
49# initialize level parameters
50level_params = {'restol': 1e-08, 'dt': 1.0 / 16}
52# initialize sweeper parameters
53sweeper_params = {'quad_type': 'RADAU-RIGHT', 'num_nodes': 3}
55# initialize problem parameters for the Penning trap
56problem_params = {
57 'omega_E': 4.9, # E-field frequency
58 'omega_B': 25.0, # B-field frequency
59 'u0': np.array([[10, 0, 0], [100, 0, 100], [1], [1]], dtype=object), # initial position, velocity, charge, mass
60 'nparts': 1, # number of particles in the trap
61 'sig': 0.1, # smoothing parameter for the forces
62}
64# initialize step parameters
65step_params = {'maxiter': 20}
67# initialize controller parameters
68controller_params = {
69 'hook_class': particle_hook, # specialized hook class for more statistics and output
70 'log_to_file': True,
71 'fname': 'data/step_3_B_out.txt',
72}
74# Fill description dictionary for easy hierarchy creation
75description = {
76 'problem_class': penningtrap,
77 'problem_params': problem_params,
78 'sweeper_class': boris_2nd_order,
79 'sweeper_params': sweeper_params,
80 'level_params': level_params,
81 'step_params': step_params,
82}
84Path("data").mkdir(parents=True, exist_ok=True)
86# %% [markdown]
87# The hook goes into the controller parameters. As in [Step 2](../step_2/C_using_pySDCs_frontend), the controller
88# prints its setup when we create it, and the log of the run:
90# %% tags=["scroll-output"]
91# instantiate the controller
92controller = controller_nonMPI(num_procs=1, controller_params=controller_params, description=description)
94# set time parameters: a single step
95t0 = 0.0
96Tend = level_params['dt']
98# get initial values on finest level
99P = controller.MS[0].levels[0].prob
100uinit = P.u_init()
102# call main function to get things done...
103uend, stats = controller.run(u0=uinit, t0=t0, Tend=Tend)
105# %% [markdown]
106# ## Particles
107#
108# The solution is a `particles` data type. Its parts are separate arrays, one column per particle:
110# %%
111print('position:', uend.pos.T, '\nvelocity:', uend.vel.T, '\ncharge:', uend.q, ' mass:', uend.m)
113# %% [markdown]
114# ## Our statistics
115#
116# Our type `'etot'` now shows up among the others, and `get_sorted` treats it like any other:
118# %%
119print('etot is registered:', 'etot' in get_list_of_types(stats))
121# filter statistics type (etot)
122energy = get_sorted(stats, type='etot', sortby='iter')
124# get base energy and show difference
125base_energy = energy[0][1]
126for item in energy:
127 print(
128 'Total energy and deviation in iteration %2i: %12.10f -- %12.8e'
129 % (item[0], item[1], abs(base_energy - item[1]))
130 )
132# %% [markdown]
133# Iteration 0 is the energy the hook computed before the run, the other one after the step, at the iteration it
134# converged in. For this single particle the exact solution is known, so we can also check the position:
136# %%
137# compute error compared to know exact solution for one particle
138uex = P.u_exact(Tend)
139err = np.linalg.norm(uex.pos - uend.pos, np.inf) / np.linalg.norm(uex.pos, np.inf)
140print(f'relative error of the position: {err:.3e}')
142# %% [markdown]
143# The position is accurate, but the energy has changed by about 14.5 out of 8800 in a single step. We look into
144# that in [Part C](C_study_collocations).
145#
146# :::{admonition} Important things to note
147# - A custom hook calls `super()` in every method it overrides, so that its entries are labelled correctly when a
148# step is restarted, e.g. by adaptivity.
149# - User-defined statistics can also come from the problem class: give it an attribute (e.g. the number of GMRES
150# iterations of its spatial solver) and read it in the hook through the level, as `L.prob`.
151# :::
152#
153# The checks the tests run:
155# %%
156assert abs(base_energy - energy[-1][1]) < 15, f'ERROR: energy deviated too much, got {base_energy - energy[-1][1]}'
157assert err < 5e-04, f"ERROR: solution is not as exact as expected, got {err}"