Skip to content

Commit 76d3623

Browse files
committed
Envelope: Space Charge Python
Full Python bindings and optimizer
1 parent 8bf1552 commit 76d3623

File tree

7 files changed

+330
-94
lines changed

7 files changed

+330
-94
lines changed

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ ln -s /usr/lib/llvm-16/bin/lld-link lld
2929
ln -s $(which ld.lld-16) ld.lld # note: not sufficient yet... somehow hard-coded in compiler detection... use docker
3030
# manually linking /usr/bin/ld.lld-16 as /usr/bin/ld.lld works...
3131
export PATH=$PWD:$PATH
32+
33+
# optional: create a venv for Python
34+
rm -rf ~/src/venv-impactx-enzyme
35+
python3 -m venv ~/src/venv-impactx-enzyme
36+
source ~/src/venv-impactx-enzyme/bin/activate
37+
python3 -m pip install --upgrade pip
38+
python3 -m pip install --upgrade scipy numpy packaging setuptools[core] wheel pytest pytest-benchmark matplotlib PyQt6
3239
```
3340

3441

@@ -46,13 +53,15 @@ From the [homepage](https://enzyme.mit.edu/getting_started/UsingEnzyme/):
4653
### Always
4754

4855
```bash
49-
export PATH=$HOME/.local/pipx/venvs/cmake/bin:$PATH
56+
source ~/src/venv-impactx-enzyme/bin/activate
57+
alias cmake=$HOME/.local/pipx/venvs/cmake/bin/cmake
5058
export CC="clang-16"
5159
export CXX="clang++-16"
5260

5361
# one TU: Clang Plugin
5462
# Extra Enzyme options, e.g., print https://enzyme.mit.edu/getting_started/UsingEnzyme/#semantic-options
55-
export CXXFLAGS="-fplugin=$HOME/src/Enzyme/build/Enzyme/ClangEnzyme-16.so -mllvm -enzyme-print -mllvm -enzyme-print"
63+
# optional add for verbose output: -mllvm -enzyme-print
64+
export CXXFLAGS="-fplugin=$HOME/src/Enzyme/build/Enzyme/ClangEnzyme-16.so"
5665
```
5766

5867
With the active developer env above, inside the ImpactX source dir:
@@ -66,8 +75,13 @@ cmake --fresh \
6675
-DImpactX_OPENPMD=OFF \
6776
-DCMAKE_LINKER_TYPE=LLD \
6877
-DCMAKE_LINKER=/usr/lib/llvm-16/bin/lld-link
78+
# optional:
79+
# -DImpactX_PYTHON=ON -DpyAMReX_IPO=OFF -DImpactX_PYTHON_IPO=OFF
6980

7081
cmake --build build -j 6
82+
83+
# optional:
84+
cmake --build build -j 6 --target pip_install
7185
```
7286

7387

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
impactx.verbose = 0
2+
13
###############################################################################
24
# Particle Beam(s)
35
###############################################################################
@@ -15,37 +17,9 @@ beam.emittX = 1.0e-6
1517
beam.emittY = beam.emittX
1618
beam.emittT = 1.0e-12
1719

18-
###############################################################################
19-
# Beamline: lattice elements and segments
20-
###############################################################################
21-
lattice.elements = monitor drift1 quad1 drift2 quad2 drift1 monitor
22-
lattice.nslice = 50
23-
24-
monitor.type = beam_monitor
25-
monitor.backend = h5
26-
27-
drift1.type = drift
28-
drift1.ds = 7.44e-2
29-
30-
quad1.type = quad
31-
quad1.ds = 6.10e-2
32-
quad1.k = -103.12574100336
33-
34-
drift2.type = drift
35-
drift2.ds = 14.88e-2
36-
37-
quad2.type = quad
38-
quad2.ds = 6.10e-2
39-
quad2.k = -quad1.k
40-
4120
###############################################################################
4221
# Algorithms
4322
###############################################################################
4423
algo.particle_shape = 2
4524
algo.track = "envelope"
4625
algo.space_charge = 2D
47-
48-
###############################################################################
49-
# Diagnostics
50-
###############################################################################
51-
diag.slice_step_diagnostics = true

match_beam.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright 2025 ImpactX contributors
4+
# Authors: Axel Huebl, Chad Mitchell
5+
# License: BSD-3-Clause-LBNL
6+
#
7+
# -*- coding: utf-8 -*-
8+
9+
import time
10+
11+
import matplotlib.pyplot as plt
12+
import numpy as np
13+
from scipy.optimize import minimize
14+
15+
from impactx import my_run
16+
17+
verbose = False
18+
# mode = "forward"
19+
mode = "backward"
20+
inputs_file_beam = "examples/fodo_space_charge/input_fodo_envelope_sc.in"
21+
22+
history = {}
23+
runtime = {}
24+
optimizer = None
25+
grads = False
26+
27+
28+
class ProcessTimer:
29+
def __init__(self):
30+
self.elapsed_time = 0
31+
32+
def __enter__(self):
33+
self.start_time = time.process_time_ns()
34+
return self
35+
36+
def __exit__(self, exc_type, exc_val, exc_tb):
37+
end_time = time.process_time_ns()
38+
self.elapsed_time = end_time - self.start_time
39+
40+
41+
def objective(parameters: tuple) -> float:
42+
"""
43+
A function that is evaluated by the optimizer.
44+
45+
Parameters
46+
----------
47+
parameters: tuple
48+
quadrupole strengths k of quad 1 and quad 2.
49+
50+
Returns
51+
-------
52+
The L2 norm of alpha and beta of the beam at the end of the
53+
simulation.
54+
"""
55+
global grads
56+
57+
if verbose:
58+
print(f"Run objective with parameters={parameters}...")
59+
60+
if not grads:
61+
use_mode = "gradient-free"
62+
else:
63+
use_mode = mode
64+
65+
q1_k, q2_k = parameters
66+
67+
with ProcessTimer() as time_ns:
68+
values = my_run(q1_k, q2_k, use_mode, inputs_file_beam, verbose)
69+
# print(f"optimizer (grads={grads}): {time_ns.elapsed_time}ns")
70+
error = values["error"]
71+
# print(q1_k, q2_k, error, values)
72+
73+
history[optimizer].append([q1_k, q2_k])
74+
runtime[optimizer] = min(time_ns.elapsed_time, runtime[optimizer])
75+
76+
if verbose:
77+
print(f"error={error}, q1_k={q1_k}, q2_k={q2_k}")
78+
79+
if np.isnan(error):
80+
error = 1.0e99
81+
82+
if grads:
83+
return (error, [values["derror_dq1_k"], values["derror_dq2_k"]])
84+
else:
85+
return error
86+
87+
88+
def optimize_and_plot(opti, jac=None, plot=True):
89+
global grads, history, optimizer, runtime
90+
grads = jac == True # noqa
91+
optimizer = opti
92+
history[optimizer] = []
93+
runtime[optimizer] = 9e99
94+
res = minimize(
95+
objective,
96+
initial_quad_strengths,
97+
method=optimizer,
98+
jac=jac,
99+
tol=1.0e-8,
100+
options=options,
101+
bounds=bounds,
102+
)
103+
ln = len(history[optimizer])
104+
print(optimizer, ln)
105+
q1_k, q2_k = zip(*history[optimizer])
106+
if plot:
107+
opt_str = optimizer
108+
if optimizer == "CG":
109+
opt_str = "Conjugate Gradient"
110+
plt.plot(q1_k, q2_k, label=f"{opt_str}: {ln}x")
111+
print(" Optimal parameters for k:", res.x)
112+
print(" L2 norm of alpha & beta at the optimum:", res.fun)
113+
print(f" Min. runtime: {runtime[optimizer] / ln}ns")
114+
115+
116+
if __name__ == "__main__":
117+
# Initial guess for the quadrople strengths
118+
initial_quad_strengths = np.array([-85, 80])
119+
120+
# Bounds for values to test: (min, max)
121+
positive = (0, None)
122+
negative = (None, 0)
123+
bounds = [negative, positive]
124+
125+
# optimizer specific values
126+
# https://docs.scipy.org/doc/scipy/reference/optimize.minimize-neldermead.html
127+
# https://docs.scipy.org/doc/scipy/reference/optimize.minimize-lbfgsb.html
128+
options = {
129+
"maxiter": 1000,
130+
}
131+
132+
fig = plt.figure(figsize=(4.5, 2.2))
133+
134+
# Call the optimizer
135+
optimize_and_plot("Nelder-Mead", False)
136+
optimize_and_plot("CG", "2-point", False)
137+
138+
# gradient-based: CG, BFGS, Newton-CG, L-BFGS-B, TNC, SLSQP, dogleg, trust-ncg, trust-krylov, trust-exact and trust-constr
139+
optimize_and_plot("CG", True)
140+
141+
# analytical result:
142+
# q1_k = -103.12574100336
143+
# q2_k = -q1_k
144+
145+
fig.gca().annotate(
146+
"initial quadrupole strengths", # Annotation text
147+
xy=(-85, 80), # Point to annotate (arrow points here)
148+
xytext=(-95.8, 62), # Text position (arrow starts here)
149+
arrowprops=dict(
150+
facecolor="black", shrink=0.1, width=1.5, headwidth=4, headlength=6
151+
),
152+
)
153+
fig.gca().annotate(
154+
"solution", # Annotation text
155+
xy=(-103.12574100336, 103.12574100336), # Point to annotate (arrow points here)
156+
xytext=(-106, 113), # Text position (arrow starts here)
157+
arrowprops=dict(
158+
facecolor="black", shrink=0.1, width=1.5, headwidth=4, headlength=6
159+
),
160+
)
161+
162+
plt.gca().set_xlim(-108, -80)
163+
plt.gca().set_ylim(60, 120)
164+
plt.xlabel(r"$k_1$ [1/m$^2$]")
165+
plt.ylabel(r"$k_2$ [1/m$^2$]")
166+
plt.legend(loc="upper right")
167+
plt.tight_layout()
168+
plt.show()

match_runtimes.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
BW
2+
3+
Nelder-Mead 167
4+
Optimal parameters for k: [-103.0293668 102.82694643]
5+
L2 norm of alpha & beta at the optimum: 2.1624999901619736e-05
6+
Min. runtime: 6999.347305389221ns
7+
/home/axel/src/impactx/./match_beam.py:94: RuntimeWarning: Method CG cannot handle bounds.
8+
res = minimize(
9+
CG 150
10+
Optimal parameters for k: [-103.02936496 102.82694212]
11+
L2 norm of alpha & beta at the optimum: 2.1624999956742072e-05
12+
Min. runtime: 7764.473333333333ns
13+
/home/axel/src/impactx/./match_beam.py:94: RuntimeWarning: Method CG cannot handle bounds.
14+
res = minimize(
15+
CG 37
16+
Optimal parameters for k: [-103.02936686 102.82694637]
17+
L2 norm of alpha & beta at the optimum: 2.1624999902094705e-05
18+
Min. runtime: 34293.21621621621ns
19+
20+
21+
22+
23+
24+
FW
25+
26+
Nelder-Mead 167
27+
Optimal parameters for k: [-103.0293668 102.82694643]
28+
L2 norm of alpha & beta at the optimum: 2.1624999901619736e-05
29+
Min. runtime: 6997.185628742515ns
30+
/home/axel/src/impactx/./match_beam.py:94: RuntimeWarning: Method CG cannot handle bounds.
31+
res = minimize(
32+
CG 150
33+
Optimal parameters for k: [-103.02936496 102.82694212]
34+
L2 norm of alpha & beta at the optimum: 2.1624999956742072e-05
35+
Min. runtime: 7740.566666666667ns
36+
/home/axel/src/impactx/./match_beam.py:94: RuntimeWarning: Method CG cannot handle bounds.
37+
res = minimize(
38+
CG 37
39+
Optimal parameters for k: [-103.02936686 102.82694637]
40+
L2 norm of alpha & beta at the optimum: 2.1624999902094705e-05
41+
Min. runtime: 33754.16216216216ns

src/ImpactX.H

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <AMReX_REAL.H>
1919

2020
#include <list>
21+
#include <map>
2122
#include <memory>
2223

2324

@@ -181,7 +182,12 @@ namespace impactx
181182
void
182183
evaluate_lattice (ImpactX * sim, amrex::ParticleReal * error, amrex::ParticleReal const * q1_k, amrex::ParticleReal const * q2_k);
183184

184-
void my_run ();
185+
// optimal solution is:
186+
// q1_k = -103.12574100336;
187+
// q2_k = -q1_k;
188+
// mode: "gradient-free", "forward", "backward"
189+
std::map<std::string, amrex::ParticleReal>
190+
my_run (amrex::ParticleReal q1_k = -85.0, amrex::ParticleReal q2_k=80.0, std::string mode="backward", std::string inputs_file="../../examples/fodo_space_charge/input_fodo_envelope_sc.in", bool verbose=true);
185191

186192
} // namespace impactx
187193

0 commit comments

Comments
 (0)