gpbeam 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gpbeam/DumpCoordinates.py +139 -0
- gpbeam/GuineaPig.py +320 -0
- gpbeam/Result.py +132 -0
- gpbeam/Settings.py +271 -0
- gpbeam/__init__.py +5 -0
- gpbeam/config.py +38 -0
- gpbeam-0.0.1.dist-info/METADATA +38 -0
- gpbeam-0.0.1.dist-info/RECORD +11 -0
- gpbeam-0.0.1.dist-info/WHEEL +5 -0
- gpbeam-0.0.1.dist-info/licenses/LICENSE +674 -0
- gpbeam-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
from scipy.stats import truncnorm
|
|
4
|
+
from .Settings import AcceleratorConfig
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import xtrack
|
|
8
|
+
xtrack_loaded = True
|
|
9
|
+
except ImportError:
|
|
10
|
+
xtrack_loaded = False
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import RF_Track
|
|
14
|
+
rf_track_loaded = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
rf_track_loaded = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def dump_coordinates(cur_workdir, coords: np.ndarray | xtrack.particles.particles.Particles, positron: bool):
|
|
20
|
+
if type(coords) == np.ndarray:
|
|
21
|
+
if not len(coords.shape) == 2 or coords.shape[1] != 6:
|
|
22
|
+
raise ValueError("coordinate array must have shape (N,6)")
|
|
23
|
+
elif xtrack_loaded and isinstance(coords, xtrack.particles.particles.Particles):
|
|
24
|
+
particles = coords
|
|
25
|
+
coords = np.empty((6,len(particles.x)))
|
|
26
|
+
|
|
27
|
+
coords[:,0] = particles.energy / 1e9 # [GeV]
|
|
28
|
+
coords[:,1] = particles.x * 1e6 # [um]
|
|
29
|
+
coords[:,2] = particles.y * 1e6 # [um]
|
|
30
|
+
coords[:,3] = particles.zeta * 1e6 # [um]
|
|
31
|
+
coords[:,4] = particles.px * 1e6
|
|
32
|
+
coords[:,5] = particles.py * 1e6
|
|
33
|
+
elif rf_track_loaded and isinstance(coords, RF_Track.Bunch6d):
|
|
34
|
+
particles = coords
|
|
35
|
+
coords = np.empty((6,len(particles.x)))
|
|
36
|
+
|
|
37
|
+
coords[:,0] = particles.get_phase_space("%K") / 1e3 # [GeV]
|
|
38
|
+
coords[:,1] = particles.get_phase_space("%x") * 1e3 # [um]
|
|
39
|
+
coords[:,2] = particles.get_phase_space("%y") * 1e3 # [um]
|
|
40
|
+
coords[:,3] = particles.get_phase_space("%t") * 1e3 * RF_Track.clight # [um]
|
|
41
|
+
coords[:,4] = particles.get_phase_space("%xp") * 1e3
|
|
42
|
+
coords[:,5] = particles.get_phase_space("%yp") * 1e3
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError("coords must be of either np.ndarray or xtrack.particles.particles.Particles type")
|
|
45
|
+
|
|
46
|
+
if positron:
|
|
47
|
+
fname = "positron.ini"
|
|
48
|
+
else:
|
|
49
|
+
fname = "electron.ini"
|
|
50
|
+
|
|
51
|
+
np.savetxt(os.path.join(cur_workdir,fname), coords)
|
|
52
|
+
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def sampleGaussianWaist(accelerator: AcceleratorConfig, cut: int = 3, N: int = 100000,):
|
|
57
|
+
"""
|
|
58
|
+
Sample Gaussian waist particle distributions from an AcceleratorConfig.
|
|
59
|
+
"""
|
|
60
|
+
betx_1 = accelerator.beta_x[0] * 1e3 # mm → µm
|
|
61
|
+
betx_2 = accelerator.beta_x[1] * 1e3
|
|
62
|
+
|
|
63
|
+
bety_1 = accelerator.beta_y[0] * 1e3
|
|
64
|
+
bety_2 = accelerator.beta_y[1] * 1e3
|
|
65
|
+
|
|
66
|
+
sigma_z_1 = accelerator.sigma_z[0]
|
|
67
|
+
sigma_z_2 = accelerator.sigma_z[1]
|
|
68
|
+
|
|
69
|
+
energy_1 = accelerator.energy[0]
|
|
70
|
+
energy_2 = accelerator.energy[1]
|
|
71
|
+
|
|
72
|
+
espread_1 = accelerator.espread[0]
|
|
73
|
+
espread_2 = accelerator.espread[1]
|
|
74
|
+
|
|
75
|
+
lorentz_1 = (energy_1 * 1e9 - 511e3) / 511e3
|
|
76
|
+
lorentz_2 = (energy_2 * 1e9 - 511e3) / 511e3
|
|
77
|
+
|
|
78
|
+
ex_1 = accelerator.emitt_x[0] / lorentz_1
|
|
79
|
+
ex_2 = accelerator.emitt_x[1] / lorentz_2
|
|
80
|
+
|
|
81
|
+
ey_1 = accelerator.emitt_y[0] / lorentz_1
|
|
82
|
+
ey_2 = accelerator.emitt_y[1] / lorentz_2
|
|
83
|
+
|
|
84
|
+
x0_1 = accelerator.offset_x[0]
|
|
85
|
+
x0_2 = accelerator.offset_x[1]
|
|
86
|
+
|
|
87
|
+
y0_1 = accelerator.offset_y[0]
|
|
88
|
+
y0_2 = accelerator.offset_y[1]
|
|
89
|
+
|
|
90
|
+
which_espread_1 = accelerator.which_espread[0]
|
|
91
|
+
which_espread_2 = accelerator.which_espread[1]
|
|
92
|
+
|
|
93
|
+
# sample initial conditions
|
|
94
|
+
beam_1 = [
|
|
95
|
+
truncnorm.rvs(-cut, cut, loc=0, scale=np.sqrt(ex_1 * betx_1), size=N),
|
|
96
|
+
truncnorm.rvs(-cut, cut, loc=0, scale=np.sqrt(ey_1 * bety_1), size=N),
|
|
97
|
+
truncnorm.rvs(-cut, cut, loc=0, scale=sigma_z_1, size=N),
|
|
98
|
+
truncnorm.rvs(-cut, cut, loc=0, scale=np.sqrt(ex_1 * 1/betx_1), size=N),
|
|
99
|
+
truncnorm.rvs(-cut, cut, loc=0, scale=np.sqrt(ey_1 * 1/bety_1), size=N),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
beam_2 = [
|
|
103
|
+
truncnorm.rvs(-cut, cut, loc=0, scale=np.sqrt(ex_2 * betx_2), size=N),
|
|
104
|
+
truncnorm.rvs(-cut, cut, loc=0, scale=np.sqrt(ey_2 * bety_2), size=N),
|
|
105
|
+
truncnorm.rvs(-cut, cut, loc=0, scale=sigma_z_2, size=N),
|
|
106
|
+
truncnorm.rvs(-cut, cut, loc=0, scale=np.sqrt(ex_2 * 1/betx_2), size=N),
|
|
107
|
+
truncnorm.rvs(-cut, cut, loc=0, scale=np.sqrt(ey_2 * 1/bety_2), size=N),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
if which_espread_1 == 0:
|
|
111
|
+
beam_1.insert(0, np.ones(N) * energy_1)
|
|
112
|
+
elif which_espread_1 == 1:
|
|
113
|
+
beam_1.insert(0, ((np.random.random(N) - 1/2) * 2 * espread_1 + 1) * energy_1 )
|
|
114
|
+
elif which_espread_1 == 3:
|
|
115
|
+
beam_1.insert(0, truncnorm.rvs(-cut, cut, loc=energy_1, scale=espread_1, size=N))
|
|
116
|
+
else:
|
|
117
|
+
raise NotImplementedError("Beam_1: unknown setting for which_espread")
|
|
118
|
+
|
|
119
|
+
if which_espread_2 == 0:
|
|
120
|
+
beam_2.insert(0, np.ones(N) * energy_2)
|
|
121
|
+
elif which_espread_2 == 1:
|
|
122
|
+
beam_2.insert(0, ((np.random.random(N) - 1/2) * 2 * espread_2 + 1) * energy_2 )
|
|
123
|
+
elif which_espread_2 == 3:
|
|
124
|
+
beam_2.insert(0, truncnorm.rvs(-cut, cut, loc=energy_2, scale=espread_2, size=N))
|
|
125
|
+
else:
|
|
126
|
+
raise NotImplementedError("Beam_2: unknown setting for which_espread")
|
|
127
|
+
|
|
128
|
+
# add offset
|
|
129
|
+
beam_1, beam_2 = np.array(beam_1).T, np.array(beam_2).T
|
|
130
|
+
|
|
131
|
+
beam_1[:,1:] -= beam_1[:,1:].mean(axis=0)
|
|
132
|
+
beam_2[:,1:] -= beam_2[:,1:].mean(axis=0)
|
|
133
|
+
|
|
134
|
+
beam_1[:,1] += x0_1
|
|
135
|
+
beam_2[:,1] += x0_2
|
|
136
|
+
beam_1[:,2] += y0_1
|
|
137
|
+
beam_2[:,2] += y0_2
|
|
138
|
+
|
|
139
|
+
return beam_1, beam_2
|
gpbeam/GuineaPig.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import re
|
|
4
|
+
import subprocess
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field, asdict
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .Settings import AcceleratorConfig, SimulationConfig
|
|
12
|
+
from .DumpCoordinates import dump_coordinates, sampleGaussianWaist
|
|
13
|
+
from .Result import parse_guinea_output
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
##########################
|
|
17
|
+
### general config
|
|
18
|
+
##########################
|
|
19
|
+
from .config import WORKDIR, EXECUTABLE_GUINEA_PLUSPLUS
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _promote_scalars(cls, raw_dict):
|
|
23
|
+
"""
|
|
24
|
+
Promote scalar values to (value, value) if dataclass field expects tuple.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from typing import get_origin
|
|
28
|
+
|
|
29
|
+
promoted = {}
|
|
30
|
+
|
|
31
|
+
for field in cls.__dataclass_fields__.values():
|
|
32
|
+
name = field.name
|
|
33
|
+
expected_type = field.type
|
|
34
|
+
|
|
35
|
+
if name not in raw_dict:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
value = raw_dict[name]
|
|
39
|
+
|
|
40
|
+
# If expected type is Tuple[...] but value is scalar → promote
|
|
41
|
+
if (
|
|
42
|
+
get_origin(expected_type) is tuple
|
|
43
|
+
and not isinstance(value, tuple)
|
|
44
|
+
):
|
|
45
|
+
promoted[name] = (value, value)
|
|
46
|
+
else:
|
|
47
|
+
promoted[name] = value
|
|
48
|
+
|
|
49
|
+
return promoted
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class GuineaPig:
|
|
54
|
+
"""
|
|
55
|
+
Container for a full GuineaPig configuration project.
|
|
56
|
+
|
|
57
|
+
Holds multiple accelerator definitions and multiple simulation
|
|
58
|
+
parameter sets.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
accelerators: Dict[str, "AcceleratorConfig"] = field(default_factory=dict)
|
|
62
|
+
simulations: Dict[str, "SimulationConfig"] = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
active_accelerator: Optional[str] = None
|
|
65
|
+
active_simulation: Optional[str] = None
|
|
66
|
+
|
|
67
|
+
# ============================================================
|
|
68
|
+
# Basic management
|
|
69
|
+
# ============================================================
|
|
70
|
+
|
|
71
|
+
def add_accelerator(self, name: str, config: "AcceleratorConfig"):
|
|
72
|
+
self.accelerators[name] = config
|
|
73
|
+
|
|
74
|
+
def add_simulation(self, name: str, config: "SimulationConfig"):
|
|
75
|
+
self.simulations[name] = config
|
|
76
|
+
|
|
77
|
+
def set_active(self, accelerator: str, simulation: str):
|
|
78
|
+
if accelerator not in self.accelerators:
|
|
79
|
+
raise KeyError(f"Unknown accelerator '{accelerator}'")
|
|
80
|
+
if simulation not in self.simulations:
|
|
81
|
+
raise KeyError(f"Unknown simulation '{simulation}'")
|
|
82
|
+
|
|
83
|
+
self.active_accelerator = accelerator
|
|
84
|
+
self.active_simulation = simulation
|
|
85
|
+
|
|
86
|
+
# ============================================================
|
|
87
|
+
# Accessors
|
|
88
|
+
# ============================================================
|
|
89
|
+
@property
|
|
90
|
+
def accelerator(self) -> "AcceleratorConfig":
|
|
91
|
+
if self.active_accelerator is None:
|
|
92
|
+
# implicitly activate only choice
|
|
93
|
+
if len(self.accelerators) == 1:
|
|
94
|
+
self.active_accelerator = tuple(self.accelerators.keys())[0]
|
|
95
|
+
return self.accelerators[self.active_accelerator]
|
|
96
|
+
else:
|
|
97
|
+
raise RuntimeError("No active accelerator selected")
|
|
98
|
+
return self.accelerators[self.active_accelerator]
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def simulation(self) -> "SimulationConfig":
|
|
102
|
+
if self.active_simulation is None:
|
|
103
|
+
# implicitly activate only choice
|
|
104
|
+
if len(self.simulations) == 1:
|
|
105
|
+
self.active_simulation = tuple(self.simulations.keys())[0]
|
|
106
|
+
return self.simulations[self.active_simulation]
|
|
107
|
+
else:
|
|
108
|
+
raise RuntimeError("No active simulation selected")
|
|
109
|
+
return self.simulations[self.active_simulation]
|
|
110
|
+
|
|
111
|
+
# ============================================================
|
|
112
|
+
# File I/O
|
|
113
|
+
# ============================================================
|
|
114
|
+
@classmethod
|
|
115
|
+
def load(cls, filepath: str | Path) -> "GuineaPig":
|
|
116
|
+
with open(filepath, "r") as f:
|
|
117
|
+
text = f.read()
|
|
118
|
+
|
|
119
|
+
section_pattern = re.compile(
|
|
120
|
+
r"\$(\w+)::\s*(\w+)\s*\{([^}]*)\}",
|
|
121
|
+
re.DOTALL
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
project = cls()
|
|
125
|
+
|
|
126
|
+
for section, name, body in section_pattern.findall(text):
|
|
127
|
+
|
|
128
|
+
raw = {}
|
|
129
|
+
tuple_accumulator = {}
|
|
130
|
+
|
|
131
|
+
for entry in body.split(";"):
|
|
132
|
+
entry = entry.strip()
|
|
133
|
+
if not entry:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if "=" not in entry:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
key, value = entry.split("=", 1)
|
|
140
|
+
key = key.strip()
|
|
141
|
+
value = value.strip()
|
|
142
|
+
|
|
143
|
+
# DO NOT evaluate expressions like 3.0*sigma_z.1
|
|
144
|
+
try:
|
|
145
|
+
value_eval = eval(value, {"__builtins__": None}, {})
|
|
146
|
+
except Exception:
|
|
147
|
+
value_eval = value # keep expression string
|
|
148
|
+
|
|
149
|
+
# convert GP naming → python naming
|
|
150
|
+
key = key.replace(" ", "_")
|
|
151
|
+
|
|
152
|
+
# handle .1 / .2
|
|
153
|
+
if "." in key:
|
|
154
|
+
base, idx = key.split(".")
|
|
155
|
+
idx = int(idx) - 1
|
|
156
|
+
|
|
157
|
+
tuple_accumulator.setdefault(base, [None, None])
|
|
158
|
+
tuple_accumulator[base][idx] = value_eval
|
|
159
|
+
else:
|
|
160
|
+
raw[key] = value_eval
|
|
161
|
+
|
|
162
|
+
# finalize tuples
|
|
163
|
+
for k, v in tuple_accumulator.items():
|
|
164
|
+
if v[0] is None or v[1] is None:
|
|
165
|
+
raise ValueError(f"Incomplete tuple for {k}")
|
|
166
|
+
raw[k] = tuple(v)
|
|
167
|
+
|
|
168
|
+
# Promote scalar to tuple if dataclass expects tuple
|
|
169
|
+
if section.upper() == "ACCELERATOR":
|
|
170
|
+
obj = AcceleratorConfig(**_promote_scalars(AcceleratorConfig, raw))
|
|
171
|
+
project.accelerators[name] = obj
|
|
172
|
+
|
|
173
|
+
elif section.upper() == "PARAMETERS":
|
|
174
|
+
obj = SimulationConfig(**_promote_scalars(SimulationConfig, raw))
|
|
175
|
+
project.simulations[name] = obj
|
|
176
|
+
|
|
177
|
+
return project
|
|
178
|
+
|
|
179
|
+
def save(self, filepath: str | Path):
|
|
180
|
+
lines = []
|
|
181
|
+
|
|
182
|
+
def write_section(section_name, objects):
|
|
183
|
+
for name, obj in objects.items():
|
|
184
|
+
|
|
185
|
+
lines.append(f"${section_name}:: {name}")
|
|
186
|
+
lines.append("{")
|
|
187
|
+
|
|
188
|
+
data = asdict(obj)
|
|
189
|
+
|
|
190
|
+
for key, value in data.items():
|
|
191
|
+
# gp_key = key.replace("_", " ")
|
|
192
|
+
gp_key = key
|
|
193
|
+
|
|
194
|
+
if isinstance(value, tuple) and len(value) == 2:
|
|
195
|
+
lines.append(f" {gp_key}.1={value[0]};")
|
|
196
|
+
lines.append(f" {gp_key}.2={value[1]};")
|
|
197
|
+
else:
|
|
198
|
+
lines.append(f" {gp_key}={value};")
|
|
199
|
+
|
|
200
|
+
lines.append("}")
|
|
201
|
+
|
|
202
|
+
write_section("ACCELERATOR", self.accelerators)
|
|
203
|
+
write_section("PARAMETERS", self.simulations)
|
|
204
|
+
|
|
205
|
+
with open(filepath, "w") as f:
|
|
206
|
+
f.write("\n".join(lines) + "\n")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ============================================================
|
|
210
|
+
# Convenience utilities
|
|
211
|
+
# ============================================================
|
|
212
|
+
def remove_accelerator(self, name: str):
|
|
213
|
+
if name in self.accelerators:
|
|
214
|
+
del self.accelerators[name]
|
|
215
|
+
if self.active_accelerator == name:
|
|
216
|
+
self.active_accelerator = None
|
|
217
|
+
|
|
218
|
+
def remove_simulation(self, name: str):
|
|
219
|
+
if name in self.simulations:
|
|
220
|
+
del self.simulations[name]
|
|
221
|
+
if self.active_simulation == name:
|
|
222
|
+
self.active_simulation = None
|
|
223
|
+
|
|
224
|
+
def list_accelerators(self):
|
|
225
|
+
return list(self.accelerators.keys())
|
|
226
|
+
|
|
227
|
+
def list_simulations(self):
|
|
228
|
+
return list(self.simulations.keys())
|
|
229
|
+
|
|
230
|
+
def to_dict(self):
|
|
231
|
+
"""
|
|
232
|
+
Export project as nested dictionary (dataclasses preserved).
|
|
233
|
+
"""
|
|
234
|
+
return {
|
|
235
|
+
"ACCELERATOR": self.accelerators,
|
|
236
|
+
"PARAMETERS": self.simulations,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# ============================================================
|
|
240
|
+
# Validation
|
|
241
|
+
# ============================================================
|
|
242
|
+
def validate(self):
|
|
243
|
+
"""
|
|
244
|
+
Re-run dataclass validation explicitly.
|
|
245
|
+
Useful if configs were modified after creation.
|
|
246
|
+
"""
|
|
247
|
+
for acc in self.accelerators.values():
|
|
248
|
+
acc.__post_init__()
|
|
249
|
+
|
|
250
|
+
for sim in self.simulations.values():
|
|
251
|
+
sim.__post_init__()
|
|
252
|
+
|
|
253
|
+
# ============================================================
|
|
254
|
+
# Representation
|
|
255
|
+
# ============================================================
|
|
256
|
+
def __repr__(self):
|
|
257
|
+
return (
|
|
258
|
+
f"GuineaPigProject("
|
|
259
|
+
f"{len(self.accelerators)} accelerators, "
|
|
260
|
+
f"{len(self.simulations)} simulations, "
|
|
261
|
+
f"active=({self.active_accelerator}, {self.active_simulation})"
|
|
262
|
+
f")"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# ============================================================
|
|
266
|
+
# Run Guinea-Pig
|
|
267
|
+
# ============================================================
|
|
268
|
+
def simulate(self, particles_1 = None, particles_2 = None, retain_files: bool = False):
|
|
269
|
+
unique_id = abs(hash((os.getpid(), time.time())))
|
|
270
|
+
cur_workdir = os.path.normpath(WORKDIR +"/workdir_guinea_" + str(unique_id))
|
|
271
|
+
os.makedirs(cur_workdir, exist_ok=False)
|
|
272
|
+
|
|
273
|
+
# ensure proper configuration
|
|
274
|
+
_ = self.accelerator
|
|
275
|
+
_ = self.simulation
|
|
276
|
+
|
|
277
|
+
self.save(os.path.join(cur_workdir, "acc.dat"))
|
|
278
|
+
|
|
279
|
+
# initial conditions
|
|
280
|
+
if particles_1 is not None and particles_2 is not None:
|
|
281
|
+
dump_coordinates(cur_workdir, particles_1,positron=False)
|
|
282
|
+
dump_coordinates(cur_workdir, particles_2,positron=True)
|
|
283
|
+
elif particles_1 is not None and particles_2 is None:
|
|
284
|
+
dump_coordinates(cur_workdir, particles_1,positron=False)
|
|
285
|
+
dump_coordinates(cur_workdir, particles_1,positron=True)
|
|
286
|
+
elif particles_1 is None and particles_2 is None:
|
|
287
|
+
pass
|
|
288
|
+
particles_1, particles_2 = sampleGaussianWaist(self.accelerator)
|
|
289
|
+
|
|
290
|
+
dump_coordinates(cur_workdir, particles_1,positron=False)
|
|
291
|
+
dump_coordinates(cur_workdir, particles_2,positron=True)
|
|
292
|
+
else:
|
|
293
|
+
raise ValueError("particles_2 must be None if particles_1 is not provided")
|
|
294
|
+
|
|
295
|
+
# call guinea <- guinea seems to read from stdin always as interactive program <- feed it with fake stdin
|
|
296
|
+
out_file = os.path.join(cur_workdir, "g++_result.out")
|
|
297
|
+
with open(os.devnull, "r") as nullin:
|
|
298
|
+
result = subprocess.run(
|
|
299
|
+
[EXECUTABLE_GUINEA_PLUSPLUS(), self.active_accelerator, self.active_simulation, out_file],
|
|
300
|
+
stdin=nullin,
|
|
301
|
+
stdout=subprocess.PIPE, # Capture stdout
|
|
302
|
+
stderr=subprocess.STDOUT, # Capture stderr too
|
|
303
|
+
text=True, # Decode bytes -> str
|
|
304
|
+
cwd=cur_workdir,
|
|
305
|
+
check=True
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
simulation_result = parse_guinea_output(out_file)
|
|
309
|
+
|
|
310
|
+
if simulation_result.extra["miss"] > 0.3:
|
|
311
|
+
print("Warning: particles outside of grid, miss rate = ", simulation_result.extra["miss"])
|
|
312
|
+
|
|
313
|
+
if retain_files:
|
|
314
|
+
with open(os.path.join(cur_workdir, "guinea.log"), "w") as logfile:
|
|
315
|
+
logfile.write(result.stdout)
|
|
316
|
+
else:
|
|
317
|
+
shutil.rmtree(cur_workdir, ignore_errors=True)
|
|
318
|
+
|
|
319
|
+
return simulation_result
|
|
320
|
+
|
gpbeam/Result.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class BeamResult:
|
|
8
|
+
tracked_macroparticles: int
|
|
9
|
+
energy_loss: float
|
|
10
|
+
avg_photon_energy: float
|
|
11
|
+
photons_per_particle: float
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class GeneralResults:
|
|
16
|
+
lumi_fine: float
|
|
17
|
+
lumi_ee: float
|
|
18
|
+
lumi_ee_high: float
|
|
19
|
+
lumi_pp: float
|
|
20
|
+
lumi_eg: float
|
|
21
|
+
lumi_ge: float
|
|
22
|
+
lumi_gg: float
|
|
23
|
+
lumi_gg_high: float
|
|
24
|
+
upsmax: float
|
|
25
|
+
E_cm: float
|
|
26
|
+
E_cm_var: float
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class GuineaPigOutput:
|
|
31
|
+
beam1: BeamResult
|
|
32
|
+
beam2: BeamResult
|
|
33
|
+
general: GeneralResults
|
|
34
|
+
|
|
35
|
+
# additional scalar key=value style results
|
|
36
|
+
extra: Dict[str, float] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# -------------------------------------------------
|
|
40
|
+
# Parse GuineaPig output file
|
|
41
|
+
# -------------------------------------------------
|
|
42
|
+
def parse_guinea_output(file_path: str) -> GuineaPigOutput:
|
|
43
|
+
with open(file_path, "r") as f:
|
|
44
|
+
content = f.read()
|
|
45
|
+
|
|
46
|
+
# Remove everything after step table
|
|
47
|
+
content = content.split("step lumi total lumi peak")[0]
|
|
48
|
+
|
|
49
|
+
# -------------------------------------------------
|
|
50
|
+
# Extract beam results (compact key=value section)
|
|
51
|
+
# -------------------------------------------------
|
|
52
|
+
kv_pairs = dict(
|
|
53
|
+
# re.findall(r"([A-Za-z0-9_.]+)\s*=\s*([-+0-9.eE]+)", content)
|
|
54
|
+
re.findall(r"([A-Za-z0-9_.-]+)\s*=\s*([-+0-9.eE]+)", content)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def get_float(key, default=0.0):
|
|
58
|
+
return float(kv_pairs.get(key, default))
|
|
59
|
+
|
|
60
|
+
def get_int_from_line(pattern):
|
|
61
|
+
m = re.search(pattern, content)
|
|
62
|
+
return int(m.group(1)) if m else 0
|
|
63
|
+
|
|
64
|
+
def get_int_multiple_line(pattern):
|
|
65
|
+
m = re.search(pattern, content, re.DOTALL)
|
|
66
|
+
return int(m.group(1)) if m else 0
|
|
67
|
+
|
|
68
|
+
# -------------------------------------------------
|
|
69
|
+
# Beam 1
|
|
70
|
+
# -------------------------------------------------
|
|
71
|
+
beam1 = BeamResult(
|
|
72
|
+
tracked_macroparticles=get_int_multiple_line(
|
|
73
|
+
r"beam1.*?number of tracked macroparticles\s*:\s*(\d+)"
|
|
74
|
+
),
|
|
75
|
+
energy_loss=get_float("de1"),
|
|
76
|
+
avg_photon_energy=get_float("phot-e1"),
|
|
77
|
+
photons_per_particle=get_float("n_phot1"),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# -------------------------------------------------
|
|
81
|
+
# Beam 2
|
|
82
|
+
# -------------------------------------------------
|
|
83
|
+
beam2 = BeamResult(
|
|
84
|
+
tracked_macroparticles=get_int_multiple_line(
|
|
85
|
+
r"beam2.*?number of tracked macroparticles\s*:\s*(\d+)"
|
|
86
|
+
),
|
|
87
|
+
energy_loss=get_float("de2"),
|
|
88
|
+
avg_photon_energy=get_float("phot-e2"),
|
|
89
|
+
photons_per_particle=get_float("n_phot2"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# -------------------------------------------------
|
|
93
|
+
# General results
|
|
94
|
+
# -------------------------------------------------
|
|
95
|
+
def extract_float(pattern):
|
|
96
|
+
m = re.search(pattern, content)
|
|
97
|
+
return float(m.group(1)) if m else 0.0
|
|
98
|
+
|
|
99
|
+
general = GeneralResults(
|
|
100
|
+
lumi_fine=extract_float(r"lumi_fine\s*=\s*([-+0-9.eE]+)"),
|
|
101
|
+
lumi_ee=extract_float(r"lumi_ee\s*=\s*([-+0-9.eE]+)"),
|
|
102
|
+
lumi_ee_high=extract_float(r"lumi_ee_high\s*=\s*([-+0-9.eE]+)"),
|
|
103
|
+
lumi_pp=extract_float(r"lumi_pp\s*=\s*([-+0-9.eE]+)"),
|
|
104
|
+
lumi_eg=extract_float(r"lumi_eg\s*=\s*([-+0-9.eE]+)"),
|
|
105
|
+
lumi_ge=extract_float(r"lumi_ge\s*=\s*([-+0-9.eE]+)"),
|
|
106
|
+
lumi_gg=extract_float(r"lumi_gg\s*=\s*([-+0-9.eE]+)"),
|
|
107
|
+
lumi_gg_high=extract_float(r"lumi_gg_high\s*=\s*([-+0-9.eE]+)"),
|
|
108
|
+
upsmax=extract_float(r"upsmax=\s*([-+0-9.eE]+)"),
|
|
109
|
+
E_cm=extract_float(r"E_cm\s*=\s*([-+0-9.eE]+)"),
|
|
110
|
+
E_cm_var=extract_float(r"E_cm_var\s*=\s*([-+0-9.eE]+)"),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# -------------------------------------------------
|
|
114
|
+
# Extra numeric key=value pairs (angles, hadrons, etc.)
|
|
115
|
+
# -------------------------------------------------
|
|
116
|
+
extra = {
|
|
117
|
+
k: float(v)
|
|
118
|
+
for k, v in kv_pairs.items()
|
|
119
|
+
if k not in {
|
|
120
|
+
"phot-e1", "phot-e2",
|
|
121
|
+
"n_phot1", "n_phot2",
|
|
122
|
+
"de1", "de2",
|
|
123
|
+
"lumi_ee", "lumi_ee_high"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return GuineaPigOutput(
|
|
128
|
+
beam1=beam1,
|
|
129
|
+
beam2=beam2,
|
|
130
|
+
general=general,
|
|
131
|
+
extra=extra,
|
|
132
|
+
)
|