Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2026-07-16 08:08:22

0001 #!/usr/bin/env python3
0002 
0003 # Copyright (c) 2025 ACTS-Project
0004 # This file is part of ACTS.
0005 # See LICENSE for details.
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)  # For reproducible results
0039 
0040     # Generate points in cylindrical coordinates, then convert
0041     r = np.random.uniform(0.1, 5.0, num_evaluations)  # 0.1m to 5m radius
0042     phi = np.random.uniform(0, 2 * np.pi, num_evaluations)
0043     z = np.random.uniform(-10.0, 10.0, num_evaluations)  # -10m to +10m in z
0044 
0045     x = r * np.cos(phi)
0046     y = r * np.sin(phi)
0047 
0048     positions = np.column_stack((x, y, z))
0049 
0050     # Warm up - evaluate a few fields first
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     # Benchmark single evaluations
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         # Generate random test points for this batch size
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         # Create cache for this batch
0099         ctx = MagneticFieldContext()
0100         cache = field.makeCache(ctx)
0101 
0102         # Warm up
0103         for i in range(min(10, batch_size)):
0104             pos = acts.Vector3(positions[i])
0105             field.getField(pos, cache)
0106 
0107         # Benchmark this batch size
0108         # Adjust iterations based on batch size
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     # Test different spatial regions
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         # Generate grid points in this region
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         # Create cache for this region
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                 # Use phi=0 for consistency
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     # Plot 1: Batch size performance
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     # Plot 2: Regional performance
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     # Add value labels on bars
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     # Add value labels on bars
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     # XY slice params
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     # ZX slice params
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     # Viz params
0335     log_vmin=1e-4,
0336     log_vmax=4.1,
0337     quiver_stride_xy=28,  # ~ how many arrows across
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     # ---------- XY slice ----------
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)  # avoid zeros on log scale
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     # Quiver (unit-length for direction)
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     # ---------- ZX slice (Z horizontal, X vertical) ----------
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     # Quiver (projected in z–x plane)
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     # XY slice controls
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     # ZX slice controls
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     # Color/arrow controls
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     # Create toroidal field
0582     print("\nCreating toroidal field...")
0583     field = create_toroidal_field()
0584     print("✓ Toroidal field created successfully")
0585 
0586     # Run benchmarks
0587     try:
0588         # Single evaluation benchmark
0589         single_time, single_rate = benchmark_single_evaluations(
0590             field, args.num_evaluations
0591         )
0592 
0593         # Batch evaluation benchmark
0594         batch_results = benchmark_vectorized_evaluations(field, args.batch_sizes)
0595 
0596         # Spatial distribution benchmark
0597         region_results = benchmark_spatial_distribution(field, args.grid_resolution)
0598 
0599         # Print summary
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         # Generate plots if requested
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         # Field maps (XY, ZX) using ACTS field.getField
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())