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.
@@ -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
+ )