Skip to content

Commit bd0eaa3

Browse files
authored
Merge pull request #279 from brownbaerchen/work_models
Counters for measuring work
2 parents be8c826 + cfae4be commit bd0eaa3

File tree

8 files changed

+131
-36
lines changed

8 files changed

+131
-36
lines changed

pySDC/core/Problem.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ def __init__(self, pars):
1212
self._freeze()
1313

1414

15+
class WorkCounter(object):
16+
"""
17+
Class for counting iterations
18+
"""
19+
20+
def __init__(self):
21+
self.niter = 0
22+
23+
def __call__(self, *args, **kwargs):
24+
self.niter += 1
25+
26+
1527
class ptype(object):
1628
"""
1729
Prototype class for problems, just defines the attributes essential to get started
@@ -36,6 +48,7 @@ def __init__(self, init, dtype_u, dtype_f, params):
3648
"""
3749

3850
self.params = _Pars(params)
51+
self.work_counters = {}
3952

4053
# set up logger
4154
self.logger = logging.getLogger('problem')
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from pySDC.core.Hooks import hooks
2+
3+
4+
class LogWork(hooks):
5+
"""
6+
Log the increment of all work counters in the problem between steps
7+
"""
8+
9+
def pre_step(self, step, level_number):
10+
"""
11+
Store the current values of the work counters
12+
13+
Args:
14+
step (pySDC.Step.step): the current step
15+
level_number (int): the current level number
16+
17+
Returns:
18+
None
19+
"""
20+
if level_number == 0:
21+
self.__work_last_step = [
22+
{key: step.levels[i].prob.work_counters[key].niter for key in step.levels[i].prob.work_counters.keys()}
23+
for i in range(len(step.levels))
24+
]
25+
26+
def post_step(self, step, level_number):
27+
"""
28+
Add the difference between current values of counters and their values before the iteration to the stats.
29+
30+
Args:
31+
step (pySDC.Step.step): the current step
32+
level_number (int): the current level number
33+
34+
Returns:
35+
None
36+
"""
37+
L = step.levels[level_number]
38+
for key in self.__work_last_step[level_number].keys():
39+
self.add_to_stats(
40+
process=step.status.slot,
41+
time=L.time + L.dt,
42+
level=L.level_index,
43+
iter=step.status.iter,
44+
sweep=L.status.sweep,
45+
type=f'work_{key}',
46+
value=L.prob.work_counters[key].niter - self.__work_last_step[level_number][key],
47+
)

pySDC/implementations/problem_classes/LeakySuperconductor.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import numpy as np
22
import scipy.sparse as sp
3-
from scipy.sparse.linalg import spsolve
3+
from scipy.sparse.linalg import spsolve, gmres, inv
44

55
from pySDC.core.Errors import ParameterError, ProblemError
6-
from pySDC.core.Problem import ptype
6+
from pySDC.core.Problem import ptype, WorkCounter
77
from pySDC.helpers import problem_helper
88
from pySDC.implementations.datatype_classes.mesh import mesh, imex_mesh
99

@@ -46,6 +46,9 @@ def __init__(self, problem_params, dtype_u=mesh, dtype_f=mesh):
4646
'nvars': 2**7,
4747
'newton_tol': 1e-8,
4848
'newton_iter': 99,
49+
'lintol': 1e-8,
50+
'liniter': 99,
51+
'direct_solver': True,
4952
}
5053

5154
for key in problem_params.keys():
@@ -92,7 +95,10 @@ def __init__(self, problem_params, dtype_u=mesh, dtype_f=mesh):
9295

9396
self.leak = np.logical_and(self.xv > self.params.leak_range[0], self.xv < self.params.leak_range[1])
9497

95-
self.total_newton_iter = 0 # store here how many iterations you needed for the Newton solver over the entire run to extract the desired information in the hooks
98+
self.work_counters['newton'] = WorkCounter()
99+
self.work_counters['rhs'] = WorkCounter()
100+
if not self.params.direct_solver:
101+
self.work_counters['linear'] = WorkCounter()
96102

97103
def eval_f_non_linear(self, u, t):
98104
"""
@@ -136,6 +142,7 @@ def eval_f(self, u, t):
136142
"""
137143
f = self.dtype_f(self.init)
138144
f[:] = self.A.dot(u.flatten()).reshape(self.params.nvars) + self.eval_f_non_linear(u, t)
145+
self.work_counters['rhs']()
139146
return f
140147

141148
def solve_system(self, rhs, factor, u0, t):
@@ -182,9 +189,20 @@ def get_non_linear_Jacobian(u):
182189

183190
u = self.dtype_u(u0)
184191
res = np.inf
192+
delta = np.zeros_like(u)
193+
194+
# construct a preconditioner for the space solver
195+
if not self.params.direct_solver:
196+
M = inv(self.Id - factor * self.A)
197+
185198
for n in range(0, self.params.newton_iter):
186199
# assemble G such that G(u) = 0 at the solution of the step
187200
G = u - factor * self.eval_f(u, t) - rhs
201+
self.work_counters[
202+
'rhs'
203+
].niter -= (
204+
1 # Work regarding construction of the Jacobian etc. should count into the Newton iterations only
205+
)
188206

189207
res = np.linalg.norm(G, np.inf)
190208
if res <= self.params.newton_tol and n > 0: # we want to make at least one Newton iteration
@@ -194,15 +212,27 @@ def get_non_linear_Jacobian(u):
194212
J = self.Id - factor * (self.A + get_non_linear_Jacobian(u))
195213

196214
# solve the linear system
197-
delta = spsolve(J, G)
215+
if self.params.direct_solver:
216+
delta = spsolve(J, G)
217+
else:
218+
delta, info = gmres(
219+
J,
220+
G,
221+
x0=delta,
222+
M=M,
223+
tol=self.params.lintol,
224+
maxiter=self.params.liniter,
225+
atol=0,
226+
callback=self.work_counters['linear'],
227+
)
198228

199229
if not np.isfinite(delta).all():
200230
break
201231

202232
# update solution
203233
u = u - delta
204234

205-
self.total_newton_iter += n
235+
self.work_counters['newton']()
206236

207237
return u
208238

@@ -258,6 +288,7 @@ def eval_f(self, u, t):
258288
f.impl[:] = self.A.dot(u.flatten()).reshape(self.params.nvars)
259289
f.expl[:] = self.eval_f_non_linear(u, t)
260290

291+
self.work_counters['rhs']()
261292
return f
262293

263294
def solve_system(self, rhs, factor, u0, t):

pySDC/implementations/problem_classes/Lorenz.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import numpy as np
2-
from pySDC.core.Problem import ptype
2+
from pySDC.core.Problem import ptype, WorkCounter
33
from pySDC.implementations.datatype_classes.mesh import mesh
44

55

@@ -44,6 +44,7 @@ def __init__(self, problem_params):
4444
dtype_f=mesh,
4545
params=problem_params,
4646
)
47+
self.work_counters['newton'] = WorkCounter()
4748

4849
def eval_f(self, u, t):
4950
"""
@@ -122,6 +123,7 @@ def solve_system(self, rhs, dt, u0, t):
122123

123124
# update solution
124125
u = u - delta
126+
self.work_counters['newton']()
125127

126128
return u
127129

pySDC/implementations/problem_classes/Van_der_Pol_implicit.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from scipy.integrate import solve_ivp
33

44
from pySDC.core.Errors import ParameterError, ProblemError
5-
from pySDC.core.Problem import ptype
5+
from pySDC.core.Problem import ptype, WorkCounter
66
from pySDC.implementations.datatype_classes.mesh import mesh
77

88

@@ -39,6 +39,7 @@ def __init__(self, problem_params, dtype_u=mesh, dtype_f=mesh):
3939
super(vanderpol, self).__init__(
4040
(problem_params['nvars'], None, np.dtype('float64')), dtype_u, dtype_f, problem_params
4141
)
42+
self.work_counters['newton'] = WorkCounter()
4243

4344
def u_exact(self, t, u_init=None, t_init=None):
4445
"""
@@ -128,6 +129,7 @@ def solve_system(self, rhs, dt, u0, t):
128129
x1 = u[0]
129130
x2 = u[1]
130131
n += 1
132+
self.work_counters['newton']()
131133

132134
if np.isnan(res) and self.params.stop_at_nan:
133135
raise ProblemError('Newton got nan after %i iterations, aborting...' % n)

pySDC/projects/Resilience/fault_stats.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
import pySDC.helpers.plot_helper as plot_helper
1010
from pySDC.helpers.stats_helper import get_sorted
1111

12-
from pySDC.projects.Resilience.hook import hook_collection, LogUAllIter, LogData, LogNewtonIter
12+
from pySDC.projects.Resilience.hook import hook_collection, LogUAllIter, LogData
1313
from pySDC.projects.Resilience.fault_injection import get_fault_injector_hook
1414
from pySDC.implementations.convergence_controller_classes.hotrod import HotRod
1515
from pySDC.implementations.convergence_controller_classes.adaptivity import Adaptivity
1616
from pySDC.implementations.convergence_controller_classes.basic_restarting import BasicRestartingNonMPI
1717
from pySDC.implementations.hooks.log_errors import LogLocalErrorPostStep
18+
from pySDC.implementations.hooks.log_work import LogWork
1819

1920
# these problems are available for testing
2021
from pySDC.projects.Resilience.advection import run_advection
@@ -620,7 +621,7 @@ def generate_stats(self, strategy=None, runs=1000, reload=True, faults=True, com
620621
else:
621622
error = self.get_error(u, t, controller, strategy)
622623
total_iteration = sum([k[1] for k in get_sorted(stats, type='k')])
623-
total_newton_iteration = sum([k[1] for k in get_sorted(stats, type='newton_iter')])
624+
total_newton_iteration = sum([k[1] for k in get_sorted(stats, type='work_newton')])
624625

625626
# record the new data point
626627
if faults:
@@ -694,7 +695,7 @@ def single_run(self, strategy, run=0, faults=False, force_params=None, hook_clas
694695
pySDC.Controller: The controller of the run
695696
float: The time the problem should have run to
696697
'''
697-
hook_class = hook_collection + [LogNewtonIter] + ([LogData] if hook_class is None else hook_class)
698+
hook_class = hook_collection + [LogWork] + ([LogData] if hook_class is None else hook_class)
698699
force_params = {} if force_params is None else force_params
699700

700701
# build the custom description
@@ -870,7 +871,7 @@ def scrutinize(self, strategy, run, faults=True):
870871
)
871872
print(f'k: sum: {np.sum(k)}, min: {np.min(k)}, max: {np.max(k)}, mean: {np.mean(k):.2f},')
872873

873-
_newton_iter = get_sorted(stats, type='newton_iter')
874+
_newton_iter = get_sorted(stats, type='work_newton')
874875
if len(_newton_iter) > 0:
875876
newton_iter = [me[1] for me in _newton_iter]
876877
print(

pySDC/projects/Resilience/hook.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,6 @@
88
hook_collection = [LogSolution, LogEmbeddedErrorEstimate, LogExtrapolationErrorEstimate, LogStepSize]
99

1010

11-
class LogNewtonIter(hooks):
12-
"""
13-
Log the number of Newton iterations required for each step
14-
"""
15-
16-
def pre_step(self, step, level_number):
17-
if level_number == 0:
18-
self.__newton_iter_last_step = [
19-
step.levels[i].prob.__dict__.get('total_newton_iter', 0) for i in range(len(step.levels))
20-
]
21-
22-
def post_step(self, step, level_number):
23-
L = step.levels[level_number]
24-
self.add_to_stats(
25-
process=step.status.slot,
26-
time=L.time + L.dt,
27-
level=L.level_index,
28-
iter=step.status.iter,
29-
sweep=L.status.sweep,
30-
type='newton_iter',
31-
value=L.prob.__dict__.get('total_newton_iter', 0) - self.__newton_iter_last_step[level_number],
32-
)
33-
34-
3511
class LogData(hooks):
3612
"""
3713
Record data required for analysis of problems in the resilience project

pySDC/projects/Resilience/leaky_superconductor.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,15 +202,24 @@ def compare_imex_full(plotting=False):
202202
plotting (bool): Plot the solution or not
203203
"""
204204
from pySDC.implementations.convergence_controller_classes.adaptivity import Adaptivity
205+
from pySDC.implementations.hooks.log_work import LogWork
206+
207+
maxiter = 5
208+
num_nodes = 3
209+
newton_iter_max = 20
205210

206211
res = {}
212+
rhs = {}
207213

208214
custom_description = {}
209215
custom_description['problem_params'] = {
210216
'newton_tol': 1e-10,
211-
'newton_iter': 20,
217+
'newton_iter': newton_iter_max,
212218
'nvars': 2**9,
213219
}
220+
custom_description['step_params'] = {'maxiter': maxiter}
221+
custom_description['sweeper_params'] = {'num_nodes': num_nodes}
222+
214223
custom_controller_params = {'logger_level': 30}
215224
for imex in [False, True]:
216225
custom_description['convergence_controllers'] = {Adaptivity: {'e_tol': 1e-6, 'dt_max': 1e2}}
@@ -219,9 +228,19 @@ def compare_imex_full(plotting=False):
219228
custom_controller_params=custom_controller_params,
220229
imex=imex,
221230
Tend=5e2,
231+
hook_class=LogWork,
222232
)
223233

224234
res[imex] = get_sorted(stats, type='u')[-1][1]
235+
newton_iter = [me[1] for me in get_sorted(stats, type='work_newton')]
236+
rhs[imex] = np.mean([me[1] for me in get_sorted(stats, type='work_rhs')]) // 1
237+
238+
if imex:
239+
assert all([me == 0 for me in newton_iter]), "IMEX is not supposed to do Newton iterations!"
240+
else:
241+
assert (
242+
max(newton_iter) / num_nodes / maxiter <= newton_iter_max
243+
), "Took more Newton iterations than allowed!"
225244
if plotting: # pragma no cover
226245
plot_solution(stats, controller)
227246

@@ -235,6 +254,10 @@ def compare_imex_full(plotting=False):
235254
max(res[True]) > prob.params.u_max
236255
), f"Expected runaway to happen, but maximum temperature is {max(res[True]):.2e} < u_max={prob.params.u_max:.2e}!"
237256

257+
assert (
258+
rhs[True] == rhs[False]
259+
), f"Expected IMEX and fully implicit schemes to take the same number of right hand side evaluations per step, but got {rhs[True]} and {rhs[False]}!"
260+
238261

239262
if __name__ == '__main__':
240263
compare_imex_full(plotting=True)

0 commit comments

Comments
 (0)