File indexing completed on 2026-07-16 08:08:22
0001
0002
0003
0004
0005
0006
0007 import argparse
0008 import time
0009 from pathlib import Path
0010
0011 import acts
0012 import acts.acts_toroidal_field as toroidal_field
0013 import matplotlib.pyplot as plt
0014 import numpy as np
0015 from acts import MagneticFieldContext
0016 from matplotlib.colors import LogNorm
0017 from mpl_toolkits.axes_grid1 import make_axes_locatable
0018
0019
0020 def create_toroidal_field():
0021 """Create a toroidal field with default configuration"""
0022 config = toroidal_field.Config()
0023 return toroidal_field.ToroidalField(config)
0024
0025
0026 def benchmark_single_evaluations(field, num_evaluations=10000):
0027 """
0028 Benchmark single magnetic field evaluations at random points.
0029
0030 This function tests the performance of individual field evaluations across
0031 a representative sample of detector geometry positions. It measures the
0032 time required for single getField() calls and provides statistics on
0033 evaluation speed.
0034 """
0035 print("\n=== Single Field Evaluation Benchmark ===")
0036 print(f"Number of evaluations: {num_evaluations}")
0037
0038 np.random.seed(42)
0039
0040
0041 r = np.random.uniform(0.1, 5.0, num_evaluations)
0042 phi = np.random.uniform(0, 2 * np.pi, num_evaluations)
0043 z = np.random.uniform(-10.0, 10.0, num_evaluations)
0044
0045 x = r * np.cos(phi)
0046 y = r * np.sin(phi)
0047
0048 positions = np.column_stack((x, y, z))
0049
0050
0051 ctx = MagneticFieldContext()
0052 cache = field.makeCache(ctx)
0053 for i in range(10):
0054 pos = acts.Vector3(positions[i])
0055 field.getField(pos, cache)
0056
0057
0058 start_time = time.perf_counter()
0059
0060 for i in range(num_evaluations):
0061 pos = acts.Vector3(positions[i])
0062 field.getField(pos, cache)
0063
0064 end_time = time.perf_counter()
0065 total_time = end_time - start_time
0066
0067 avg_time_per_eval = total_time / num_evaluations
0068 evaluations_per_second = num_evaluations / total_time
0069
0070 print(f"Total time: {total_time:.4f} seconds")
0071 print(f"Average time per evaluation: " f"{avg_time_per_eval*1e6:.2f} microseconds")
0072 print(f"Evaluations per second: {evaluations_per_second:.0f}")
0073
0074 return avg_time_per_eval, evaluations_per_second
0075
0076
0077 def benchmark_vectorized_evaluations(field, batch_sizes=None):
0078 """Benchmark magnetic field evaluations with different batch sizes"""
0079 if batch_sizes is None:
0080 batch_sizes = [1, 10, 100, 1000, 10000]
0081 print("\n=== Vectorized Field Evaluation Benchmark ===")
0082
0083 results = []
0084
0085 for batch_size in batch_sizes:
0086 print(f"\nBatch size: {batch_size}")
0087
0088
0089 np.random.seed(42)
0090 r = np.random.uniform(0.1, 5.0, batch_size)
0091 phi = np.random.uniform(0, 2 * np.pi, batch_size)
0092 z = np.random.uniform(-10.0, 10.0, batch_size)
0093
0094 x = r * np.cos(phi)
0095 y = r * np.sin(phi)
0096 positions = np.column_stack((x, y, z))
0097
0098
0099 ctx = MagneticFieldContext()
0100 cache = field.makeCache(ctx)
0101
0102
0103 for i in range(min(10, batch_size)):
0104 pos = acts.Vector3(positions[i])
0105 field.getField(pos, cache)
0106
0107
0108
0109 num_iterations = max(1, 1000 // batch_size)
0110
0111 start_time = time.perf_counter()
0112
0113 for _iteration in range(num_iterations):
0114 for i in range(batch_size):
0115 pos = acts.Vector3(positions[i])
0116 field.getField(pos, cache)
0117
0118 end_time = time.perf_counter()
0119
0120 total_evaluations = num_iterations * batch_size
0121 total_time = end_time - start_time
0122 avg_time_per_eval = total_time / total_evaluations
0123 evaluations_per_second = total_evaluations / total_time
0124
0125 print(f" Total evaluations: {total_evaluations}")
0126 print(f" Total time: " f"{total_time:.4f} seconds")
0127 print(
0128 f" Average time per evaluation: "
0129 f"{avg_time_per_eval*1e6:.2f} microseconds"
0130 )
0131 print(f" Evaluations per second: {evaluations_per_second:.0f}")
0132
0133 results.append(
0134 {
0135 "batch_size": batch_size,
0136 "avg_time_us": avg_time_per_eval * 1e6,
0137 "eval_per_sec": evaluations_per_second,
0138 "total_evaluations": total_evaluations,
0139 }
0140 )
0141
0142 return results
0143
0144
0145 def benchmark_spatial_distribution(field, grid_resolution=50):
0146 """Benchmark field evaluation across different spatial regions"""
0147 print("\n=== Spatial Distribution Benchmark ===")
0148 print(f"Grid resolution: {grid_resolution}x{grid_resolution} points")
0149
0150
0151 regions = [
0152 {"name": "Barrel Toroid", "r_range": (1.0, 3.0), "z_range": (-2.0, 2.0)},
0153 {"name": "Forward Endcap", "r_range": (0.5, 4.0), "z_range": (2.0, 8.0)},
0154 {"name": "Backward Endcap", "r_range": (0.5, 4.0), "z_range": (-8.0, -2.0)},
0155 {"name": "Central", "r_range": (0.1, 1.0), "z_range": (-1.0, 1.0)},
0156 ]
0157
0158 region_results = []
0159
0160 for region in regions:
0161 print(f"\n--- {region['name']} Region ---")
0162
0163
0164 r_min, r_max = region["r_range"]
0165 z_min, z_max = region["z_range"]
0166
0167 r_vals = np.linspace(r_min, r_max, grid_resolution)
0168 z_vals = np.linspace(z_min, z_max, grid_resolution)
0169
0170
0171 ctx = MagneticFieldContext()
0172 cache = field.makeCache(ctx)
0173
0174 times = []
0175 field_magnitudes = []
0176
0177 start_time = time.perf_counter()
0178
0179 for r in r_vals:
0180 for z in z_vals:
0181
0182 x = r
0183 y = 0.0
0184
0185 pos = acts.Vector3(x, y, z)
0186 eval_start = time.perf_counter()
0187 b_field = field.getField(pos, cache)
0188 eval_end = time.perf_counter()
0189
0190 times.append(eval_end - eval_start)
0191 field_magnitudes.append(
0192 np.sqrt(b_field[0] ** 2 + b_field[1] ** 2 + b_field[2] ** 2)
0193 )
0194
0195 end_time = time.perf_counter()
0196
0197 total_evaluations = len(times)
0198 total_time = end_time - start_time
0199 avg_time = np.mean(times)
0200 std_time = np.std(times)
0201 avg_field_magnitude = np.mean(field_magnitudes)
0202
0203 print(f" Total evaluations: {total_evaluations}")
0204 print(f" Total time: " f"{total_time:.4f} seconds")
0205 print(
0206 f" Average time per evaluation: "
0207 f"{avg_time*1e6:.2f} ± {std_time*1e6:.2f} microseconds"
0208 )
0209 print(f" Average field magnitude: " f"{avg_field_magnitude:.4f} Tesla")
0210 print(f" Evaluations per second: " f"{total_evaluations/total_time:.0f}")
0211
0212 region_results.append(
0213 {
0214 "region": region["name"],
0215 "total_evaluations": total_evaluations,
0216 "avg_time_us": avg_time * 1e6,
0217 "std_time_us": std_time * 1e6,
0218 "avg_field_magnitude": avg_field_magnitude,
0219 "eval_per_sec": total_evaluations / total_time,
0220 }
0221 )
0222
0223 return region_results
0224
0225
0226 def plot_benchmark_results(batch_results, region_results, output_dir):
0227 """Create plots showing benchmark results"""
0228 output_dir = Path(output_dir)
0229 output_dir.mkdir(exist_ok=True)
0230
0231
0232 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
0233
0234 batch_sizes = [r["batch_size"] for r in batch_results]
0235 avg_times = [r["avg_time_us"] for r in batch_results]
0236 eval_rates = [r["eval_per_sec"] for r in batch_results]
0237
0238 ax1.semilogx(batch_sizes, avg_times, "o-", linewidth=2, markersize=8)
0239 ax1.set_xlabel("Batch Size")
0240 ax1.set_ylabel("Average Time per Evaluation (μs)")
0241 ax1.set_title("Evaluation Time vs Batch Size")
0242 ax1.grid(True, alpha=0.3)
0243
0244 ax2.semilogx(
0245 batch_sizes, eval_rates, "s-", linewidth=2, markersize=8, color="orange"
0246 )
0247 ax2.set_xlabel("Batch Size")
0248 ax2.set_ylabel("Evaluations per Second")
0249 ax2.set_title("Evaluation Rate vs Batch Size")
0250 ax2.grid(True, alpha=0.3)
0251
0252 plt.tight_layout()
0253 plt.savefig(output_dir / "batch_performance.png", dpi=150, bbox_inches="tight")
0254 plt.close()
0255
0256
0257 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
0258
0259 regions = [r["region"] for r in region_results]
0260 region_times = [r["avg_time_us"] for r in region_results]
0261 region_rates = [r["eval_per_sec"] for r in region_results]
0262
0263 bars1 = ax1.bar(
0264 regions, region_times, color=["skyblue", "lightgreen", "lightcoral", "gold"]
0265 )
0266 ax1.set_ylabel("Average Time per Evaluation (μs)")
0267 ax1.set_title("Evaluation Time by Region")
0268 ax1.tick_params(axis="x", rotation=45)
0269
0270
0271 for bar, time_val in zip(bars1, region_times):
0272 height = bar.get_height()
0273 ax1.text(
0274 bar.get_x() + bar.get_width() / 2.0,
0275 height + height * 0.01,
0276 f"{time_val:.1f}μs",
0277 ha="center",
0278 va="bottom",
0279 )
0280
0281 bars2 = ax2.bar(
0282 regions, region_rates, color=["skyblue", "lightgreen", "lightcoral", "gold"]
0283 )
0284 ax2.set_ylabel("Evaluations per Second")
0285 ax2.set_title("Evaluation Rate by Region")
0286 ax2.tick_params(axis="x", rotation=45)
0287
0288
0289 for bar, rate_val in zip(bars2, region_rates):
0290 height = bar.get_height()
0291 ax2.text(
0292 bar.get_x() + bar.get_width() / 2.0,
0293 height + height * 0.01,
0294 f"{rate_val:.0f}/s",
0295 ha="center",
0296 va="bottom",
0297 )
0298
0299 plt.tight_layout()
0300 plt.savefig(output_dir / "regional_performance.png", dpi=150, bbox_inches="tight")
0301 plt.close()
0302
0303 print(f"\nPlots saved to {output_dir}/")
0304
0305
0306 def _eval_field_batch(field, points):
0307 """Evaluate B-field for an array of points (N,3) -> (N,3)."""
0308 ctx = MagneticFieldContext()
0309 cache = field.makeCache(ctx)
0310 out = np.empty_like(points, dtype=np.float64)
0311 for i in range(points.shape[0]):
0312 b_field = field.getField(acts.Vector3(points[i]), cache)
0313 out[i, 0] = b_field[0]
0314 out[i, 1] = b_field[1]
0315 out[i, 2] = b_field[2]
0316 return out
0317
0318
0319 def plot_field_maps(
0320 field,
0321 output_dir,
0322
0323 xy_z_plane=0.20,
0324 xy_xlim=(-10.0, 10.0),
0325 xy_ylim=(-10.0, 10.0),
0326 xy_nx=520,
0327 xy_ny=520,
0328
0329 zx_y_plane=0.10,
0330 zx_zlim=(-22.0, 22.0),
0331 zx_xlim=(-11.0, 11.0),
0332 zx_nz=560,
0333 zx_nx=560,
0334
0335 log_vmin=1e-4,
0336 log_vmax=4.1,
0337 quiver_stride_xy=28,
0338 quiver_stride_zx=28,
0339 ):
0340 """
0341 Produce two figures:
0342 1) XY slice at fixed z=xy_z_plane
0343 2) ZX slice at fixed y=zx_y_plane
0344 Saved to output_dir as field_xy.png and field_zx.png
0345 """
0346 output_dir = Path(output_dir)
0347 output_dir.mkdir(exist_ok=True)
0348
0349
0350 x = np.linspace(xy_xlim[0], xy_xlim[1], int(max(60, xy_nx)), dtype=np.float64)
0351 y = np.linspace(xy_ylim[0], xy_ylim[1], int(max(60, xy_ny)), dtype=np.float64)
0352 X, Y = np.meshgrid(x, y, indexing="xy")
0353 Z = np.full_like(X, float(xy_z_plane), dtype=np.float64)
0354
0355 pts_xy = np.column_stack([X.ravel(), Y.ravel(), Z.ravel()])
0356 B_xy = _eval_field_batch(field, pts_xy).reshape(*X.shape, 3)
0357 Bx, By, Bz = B_xy[..., 0], B_xy[..., 1], B_xy[..., 2]
0358 Bmag = np.sqrt(Bx**2 + By**2 + Bz**2, dtype=np.float64)
0359
0360 Bpos = np.clip(Bmag, log_vmin * 1e-2, None)
0361 norm = LogNorm(vmin=float(log_vmin), vmax=float(log_vmax))
0362
0363 fig, ax = plt.subplots(figsize=(8.8, 8.8))
0364 im = ax.imshow(
0365 Bpos,
0366 extent=[x.min(), x.max(), y.min(), y.max()],
0367 origin="lower",
0368 aspect="equal",
0369 norm=norm,
0370 cmap="gnuplot2",
0371 )
0372 cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
0373 cbar.set_label("|B| [T] (log scale)")
0374
0375
0376 step = max(1, X.shape[1] // quiver_stride_xy)
0377 Bsafe = np.where(Bmag > 0, Bmag, np.inf)
0378 Ux = Bx / Bsafe
0379 Uy = By / Bsafe
0380 ax.quiver(
0381 X[::step, ::step],
0382 Y[::step, ::step],
0383 Ux[::step, ::step],
0384 Uy[::step, ::step],
0385 scale=28,
0386 width=0.003,
0387 )
0388
0389 ax.set_xlim(xy_xlim)
0390 ax.set_ylim(xy_ylim)
0391 ax.set_xlabel("x [m]")
0392 ax.set_ylabel("y [m]")
0393 ax.set_title(f"|B| in z = {float(xy_z_plane):.2f} m plane")
0394 plt.tight_layout()
0395 (output_dir / "field_xy.png").unlink(missing_ok=True)
0396 plt.savefig(output_dir / "field_xy.png", dpi=150, bbox_inches="tight")
0397 plt.close()
0398
0399
0400 z = np.linspace(zx_zlim[0], zx_zlim[1], int(max(60, zx_nz)), dtype=np.float64)
0401 x = np.linspace(zx_xlim[0], zx_xlim[1], int(max(60, zx_nx)), dtype=np.float64)
0402 Z, Xg = np.meshgrid(z, x, indexing="xy")
0403 Y = np.full_like(Xg, float(zx_y_plane), dtype=np.float64)
0404
0405 pts_zx = np.column_stack([Xg.ravel(), Y.ravel(), Z.ravel()])
0406 B_zx = _eval_field_batch(field, pts_zx).reshape(*Xg.shape, 3)
0407 Bx, By, Bz = B_zx[..., 0], B_zx[..., 1], B_zx[..., 2]
0408 Bmag = np.sqrt(Bx**2 + By**2 + Bz**2, dtype=np.float64)
0409
0410 Bpos = np.clip(Bmag, log_vmin * 1e-2, None)
0411 norm = LogNorm(vmin=float(log_vmin), vmax=float(log_vmax))
0412
0413 fig, ax = plt.subplots(figsize=(10, 10))
0414 im = ax.imshow(
0415 Bpos,
0416 extent=[z.min(), z.max(), x.min(), x.max()],
0417 origin="lower",
0418 aspect="equal",
0419 norm=norm,
0420 cmap="gnuplot2",
0421 )
0422 divider = make_axes_locatable(ax)
0423 cax = divider.append_axes("right", size="5%", pad=0.10)
0424 cbar = plt.colorbar(im, cax=cax)
0425 cbar.set_label("|B| [T] (log scale)")
0426
0427
0428 step = max(1, Z.shape[1] // quiver_stride_zx)
0429 Bsafe = np.where(Bmag > 0, Bmag, np.inf)
0430 Uz = Bz / Bsafe
0431 Ux = Bx / Bsafe
0432 ax.quiver(
0433 Z[::step, ::step],
0434 Xg[::step, ::step],
0435 Uz[::step, ::step],
0436 Ux[::step, ::step],
0437 scale=28,
0438 width=0.003,
0439 )
0440
0441 ax.set_xlim(zx_zlim)
0442 ax.set_ylim(zx_xlim)
0443 ax.set_xlabel("z [m]")
0444 ax.set_ylabel("x [m]")
0445 ax.set_title(f"|B| in y = {float(zx_y_plane):.2f} m plane")
0446 plt.tight_layout()
0447 (output_dir / "field_zx.png").unlink(missing_ok=True)
0448 plt.savefig(output_dir / "field_zx.png", dpi=150, bbox_inches="tight")
0449 plt.close()
0450
0451 print(
0452 f"\nSaved field maps to: {output_dir}/field_xy.png and {output_dir}/field_zx.png"
0453 )
0454
0455
0456 def main():
0457 parser = argparse.ArgumentParser(
0458 description="Benchmark toroidal magnetic field performance"
0459 )
0460 parser.add_argument(
0461 "--num-evaluations",
0462 type=int,
0463 default=10000,
0464 help="Number of evaluations for single benchmark (default: 10000)",
0465 )
0466 parser.add_argument(
0467 "--batch-sizes",
0468 type=int,
0469 nargs="+",
0470 default=[1, 10, 100, 1000, 10000],
0471 help="Batch sizes to test (default: 1 10 100 1000 10000)",
0472 )
0473 parser.add_argument(
0474 "--grid-resolution",
0475 type=int,
0476 default=50,
0477 help="Grid resolution for spatial benchmark (default: 50)",
0478 )
0479 parser.add_argument(
0480 "--output-dir",
0481 type=str,
0482 default="toroidal_field_benchmark",
0483 help="Output directory for results (default: toroidal_field_benchmark)",
0484 )
0485 parser.add_argument(
0486 "--skip-plots", action="store_true", help="Skip generating performance plots"
0487 )
0488 parser.add_argument(
0489 "--plot-field",
0490 action="store_true",
0491 help="Also compute and save XY/ZX field maps",
0492 )
0493
0494
0495 parser.add_argument(
0496 "--xy-z-plane",
0497 type=float,
0498 default=0.20,
0499 help="z (m) for XY slice (default: 0.20)",
0500 )
0501 parser.add_argument(
0502 "--xy-xlim",
0503 type=float,
0504 nargs=2,
0505 default=(-10.0, 10.0),
0506 help="x limits for XY slice (m) (default: -10 10)",
0507 )
0508 parser.add_argument(
0509 "--xy-ylim",
0510 type=float,
0511 nargs=2,
0512 default=(-10.0, 10.0),
0513 help="y limits for XY slice (m) (default: -10 10)",
0514 )
0515 parser.add_argument(
0516 "--xy-nx", type=int, default=520, help="grid Nx for XY slice (default: 520)"
0517 )
0518 parser.add_argument(
0519 "--xy-ny", type=int, default=520, help="grid Ny for XY slice (default: 520)"
0520 )
0521
0522
0523 parser.add_argument(
0524 "--zx-y-plane",
0525 type=float,
0526 default=0.10,
0527 help="y (m) for ZX slice (default: 0.10)",
0528 )
0529 parser.add_argument(
0530 "--zx-zlim",
0531 type=float,
0532 nargs=2,
0533 default=(-22.0, 22.0),
0534 help="z limits for ZX slice (m) (default: -22 22)",
0535 )
0536 parser.add_argument(
0537 "--zx-xlim",
0538 type=float,
0539 nargs=2,
0540 default=(-11.0, 11.0),
0541 help="x limits for ZX slice (m) (default: -11 11)",
0542 )
0543 parser.add_argument(
0544 "--zx-nz", type=int, default=560, help="grid Nz for ZX slice (default: 560)"
0545 )
0546 parser.add_argument(
0547 "--zx-nx", type=int, default=560, help="grid Nx for ZX slice (default: 560)"
0548 )
0549
0550
0551 parser.add_argument(
0552 "--log-vmin",
0553 type=float,
0554 default=1e-4,
0555 help="LogNorm vmin for |B| (T) (default: 1e-4)",
0556 )
0557 parser.add_argument(
0558 "--log-vmax",
0559 type=float,
0560 default=4.1,
0561 help="LogNorm vmax for |B| (T) (default: 4.1)",
0562 )
0563 parser.add_argument(
0564 "--quiver-stride-xy",
0565 type=int,
0566 default=28,
0567 help="Quiver density for XY (default: 28)",
0568 )
0569 parser.add_argument(
0570 "--quiver-stride-zx",
0571 type=int,
0572 default=28,
0573 help="Quiver density for ZX (default: 28)",
0574 )
0575
0576 args = parser.parse_args()
0577
0578 print("=== Toroidal Magnetic Field Benchmark ===")
0579 print(f"ACTS version: {acts.version}")
0580
0581
0582 print("\nCreating toroidal field...")
0583 field = create_toroidal_field()
0584 print("✓ Toroidal field created successfully")
0585
0586
0587 try:
0588
0589 single_time, single_rate = benchmark_single_evaluations(
0590 field, args.num_evaluations
0591 )
0592
0593
0594 batch_results = benchmark_vectorized_evaluations(field, args.batch_sizes)
0595
0596
0597 region_results = benchmark_spatial_distribution(field, args.grid_resolution)
0598
0599
0600 print("\n=== Benchmark Summary ===")
0601 print(
0602 f"Single evaluation average: {single_time*1e6:.2f} μs ({single_rate:.0f} eval/s)"
0603 )
0604 print(
0605 f"Best batch performance: {min(r['avg_time_us'] for r in batch_results):.2f} μs"
0606 )
0607 print(
0608 f"Fastest region: {min(region_results, key=lambda x: x['avg_time_us'])['region']}"
0609 )
0610 print(
0611 f"Slowest region: {max(region_results, key=lambda x: x['avg_time_us'])['region']}"
0612 )
0613
0614
0615 if not args.skip_plots:
0616 try:
0617 plot_benchmark_results(batch_results, region_results, args.output_dir)
0618 except ImportError:
0619 print("\nWarning: matplotlib not available, skipping performance plots")
0620
0621
0622 if args.plot_field:
0623 try:
0624 plot_field_maps(
0625 field,
0626 output_dir=args.output_dir,
0627 xy_z_plane=args.xy_z_plane,
0628 xy_xlim=tuple(args.xy_xlim),
0629 xy_ylim=tuple(args.xy_ylim),
0630 xy_nx=args.xy_nx,
0631 xy_ny=args.xy_ny,
0632 zx_y_plane=args.zx_y_plane,
0633 zx_zlim=tuple(args.zx_zlim),
0634 zx_xlim=tuple(args.zx_xlim),
0635 zx_nz=args.zx_nz,
0636 zx_nx=args.zx_nx,
0637 log_vmin=args.log_vmin,
0638 log_vmax=args.log_vmax,
0639 quiver_stride_xy=args.quiver_stride_xy,
0640 quiver_stride_zx=args.quiver_stride_zx,
0641 )
0642 except ImportError:
0643 print(
0644 "\nWarning: matplotlib (extras) not available, skipping field maps"
0645 )
0646
0647 print("\n✓ Benchmark completed successfully!")
0648
0649 except Exception as e:
0650 print(f"\n❌ Benchmark failed: {e}")
0651 return 1
0652
0653 return 0
0654
0655
0656 if __name__ == "__main__":
0657 exit(main())