simjecture 0.1.0__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.
Files changed (75) hide show
  1. conjecture_solver/__init__.py +10 -0
  2. conjecture_solver/__main__.py +4 -0
  3. conjecture_solver/action_handlers.py +459 -0
  4. conjecture_solver/adapters/__init__.py +7 -0
  5. conjecture_solver/adapters/base.py +96 -0
  6. conjecture_solver/adapters/fake.py +155 -0
  7. conjecture_solver/adapters/pic.py +196 -0
  8. conjecture_solver/adapters/warpx.py +1375 -0
  9. conjecture_solver/autonomous_research.py +1071 -0
  10. conjecture_solver/benchmarks/__init__.py +6 -0
  11. conjecture_solver/benchmarks/electrostatic_pic.py +547 -0
  12. conjecture_solver/benchmarks/kinetic_sufficiency.py +318 -0
  13. conjecture_solver/builtin_skills/warpx/SKILL.md +151 -0
  14. conjecture_solver/builtin_skills/warpx/agents/openai.yaml +4 -0
  15. conjecture_solver/builtin_skills/warpx/examples/implicit_em_smoke.py +74 -0
  16. conjecture_solver/builtin_skills/warpx/examples/minimal_smoke.py +54 -0
  17. conjecture_solver/builtin_skills/warpx/examples/openpmd_field_smoke.py +87 -0
  18. conjecture_solver/builtin_skills/warpx/manifest.json +11 -0
  19. conjecture_solver/builtin_skills/warpx/references/2d-xz-commissioning.md +145 -0
  20. conjecture_solver/builtin_skills/warpx/references/cpu-launch-tuning.md +54 -0
  21. conjecture_solver/builtin_skills/warpx/references/diagnostics.md +139 -0
  22. conjecture_solver/builtin_skills/warpx/references/gpu-launch-tuning.md +79 -0
  23. conjecture_solver/builtin_skills/warpx/references/local-cuda-deployment.md +87 -0
  24. conjecture_solver/builtin_skills/warpx/references/numerical-risks.md +32 -0
  25. conjecture_solver/builtin_skills/warpx/references/picmi-interface.md +118 -0
  26. conjecture_solver/builtin_skills/warpx/references/resource-scaling.md +24 -0
  27. conjecture_solver/builtin_skills/warpx/references/time-integration.md +58 -0
  28. conjecture_solver/builtin_skills/warpx/scripts/benchmark_cpu_threads.py +207 -0
  29. conjecture_solver/builtin_skills/warpx/scripts/benchmark_gpu.py +242 -0
  30. conjecture_solver/builtin_skills/warpx/scripts/bootstrap_local_cuda.sh +82 -0
  31. conjecture_solver/builtin_skills/warpx/scripts/probe_local_cuda.py +110 -0
  32. conjecture_solver/builtin_skills/warpx/scripts/reduced_energy_budget.py +195 -0
  33. conjecture_solver/builtin_skills/warpx/scripts/run_local_cuda.sh +39 -0
  34. conjecture_solver/campaign.py +406 -0
  35. conjecture_solver/cli.py +963 -0
  36. conjecture_solver/confirmation.py +287 -0
  37. conjecture_solver/control.py +234 -0
  38. conjecture_solver/deployment.py +874 -0
  39. conjecture_solver/discovery.py +68 -0
  40. conjecture_solver/domains/__init__.py +40 -0
  41. conjecture_solver/domains/base.py +157 -0
  42. conjecture_solver/domains/kinetic_sufficiency.py +521 -0
  43. conjecture_solver/ledger.py +238 -0
  44. conjecture_solver/lifecycle.py +131 -0
  45. conjecture_solver/literature.py +494 -0
  46. conjecture_solver/llm.py +228 -0
  47. conjecture_solver/models.py +279 -0
  48. conjecture_solver/mvp_agent.py +3212 -0
  49. conjecture_solver/mvp_claims.py +1279 -0
  50. conjecture_solver/mvp_control.py +322 -0
  51. conjecture_solver/mvp_guidance.py +172 -0
  52. conjecture_solver/mvp_launch.py +1296 -0
  53. conjecture_solver/mvp_monitor.py +1487 -0
  54. conjecture_solver/mvp_skills.py +538 -0
  55. conjecture_solver/orchestration.py +578 -0
  56. conjecture_solver/outbox.py +526 -0
  57. conjecture_solver/parameters.py +394 -0
  58. conjecture_solver/proposals.py +427 -0
  59. conjecture_solver/research_tools.py +278 -0
  60. conjecture_solver/schema_export.py +246 -0
  61. conjecture_solver/search.py +1047 -0
  62. conjecture_solver/semantics.py +146 -0
  63. conjecture_solver/tui/__init__.py +7 -0
  64. conjecture_solver/tui/app.py +202 -0
  65. conjecture_solver/tui/claim_views.py +213 -0
  66. conjecture_solver/tui/screens.py +1014 -0
  67. conjecture_solver/warpx_analysis.py +184 -0
  68. conjecture_solver/warpx_campaign.py +148 -0
  69. conjecture_solver/warpx_confirmation.py +617 -0
  70. simjecture-0.1.0.dist-info/METADATA +400 -0
  71. simjecture-0.1.0.dist-info/RECORD +75 -0
  72. simjecture-0.1.0.dist-info/WHEEL +4 -0
  73. simjecture-0.1.0.dist-info/entry_points.txt +4 -0
  74. simjecture-0.1.0.dist-info/licenses/LICENSE +202 -0
  75. simjecture-0.1.0.dist-info/licenses/NOTICE +6 -0
@@ -0,0 +1,118 @@
1
+ # PICMI construction map
2
+
3
+ WarpX exposes the PICMI interface through `from pywarpx import picmi`. A usual
4
+ program constructs objects in this order:
5
+
6
+ 1. A geometry-specific grid such as `Cartesian1DGrid`, `Cartesian2DGrid`,
7
+ `Cartesian3DGrid`, or `CylindricalGrid`, with field and particle boundary
8
+ conditions stated explicitly.
9
+ 2. An electromagnetic or electrostatic solver compatible with that grid and
10
+ the intended model.
11
+ 3. One or more `Species` objects with explicit charge/mass or a recognized
12
+ particle type, each connected to an initial distribution.
13
+ 4. A particle layout for each species, such as `GriddedLayout` or
14
+ `PseudoRandomLayout`.
15
+ 5. A `Simulation` carrying timestep or CFL choice, maximum steps, particle
16
+ shape, random-seed policy, and any WarpX-specific controls.
17
+ 6. Applied fields, collisions, lasers, or other supported components only when
18
+ demanded by the proposed experiment.
19
+ 7. Diagnostics added to the simulation before `simulation.step()`.
20
+
21
+ `simulation.write_input_file(file_name=...)` is useful for inspecting the
22
+ compiled native input without running the calculation. When representation is
23
+ scientifically important, also request `warpx_used_inputs_file` and inspect the
24
+ realized native controls rather than trusting the frontend program alone.
25
+
26
+ PICMI uses physical units for dimensional inputs. Check dimensional
27
+ conversions explicitly. Parser expressions, velocity/momentum conventions,
28
+ thermal spreads, boundary names, and dimension-dependent component ordering
29
+ are common sources of plausible but incorrect initial states.
30
+
31
+ Despite their names, `AnalyticDistribution(momentum_expressions=...)` and
32
+ `momentum_spread_expressions=...` take proper velocity, `gamma*v`, in metres
33
+ per second in this runtime. They do **not** take mechanical momentum `m*gamma*v`.
34
+ For a nonrelativistic drift, supply the drift velocity itself; never multiply it
35
+ by particle mass. Thermal spread expressions likewise use a velocity spread.
36
+ Confirm the generated `*_u*_mean_function` and `*_u*_std_function` entries in
37
+ the used-inputs record. Parser keyword arguments must actually occur in at
38
+ least one expression consumed by that object; unused keywords are rejected by
39
+ PICMI, so pass only the constants used by each distribution or field.
40
+
41
+ For the pinned 26.07 runtime, use the concrete names implemented by the
42
+ installed interface. Important multidimensional entries include
43
+ `Cartesian2DGrid`, `AnalyticDistribution`, `AnalyticInitialField`,
44
+ `ConstantAppliedField`, `AnalyticAppliedField`, and `CoulombCollisions`. Do not
45
+ invent generic aliases such as `Grid` or `BinaryCollision`. Wire the objects
46
+ with the exact methods `simulation.add_species(species, layout=layout)`,
47
+ `simulation.add_applied_field(field)`, and
48
+ `simulation.add_diagnostic(diagnostic)`. In WarpX 26.07,
49
+ `CoulombCollisions` is not a standard PICMI interaction: pass a list through
50
+ `picmi.Simulation(..., warpx_collisions=[collision])`; do not use
51
+ `simulation.add_interaction(collision)`, which routes through the standard
52
+ interaction path. Construct it as
53
+ `picmi.CoulombCollisions(name="...", species=[species_a, species_b], ...)` in
54
+ this runtime. Its constructor requires the single `species` collection;
55
+ `species1=...` and `species2=...` without that collection are rejected. Use
56
+ `CoulombLog`, `ndt_supercycle`, or `ndt_subcycle` with the exact capitalization
57
+ shown by the runtime. If `CoulombLog` is omitted, WarpX calculates it locally;
58
+ if it is prescribed, justify its physical range and measure the realized
59
+ collision effect.
60
+
61
+ For `AnalyticInitialField`, provide explicit expressions for all three magnetic
62
+ components and all three electric components, using `"0.0"` for intended
63
+ zeros. The installed constructor keyword names are exactly
64
+ `Ex_expression`, `Ey_expression`, `Ez_expression`, `Bx_expression`,
65
+ `By_expression`, and `Bz_expression`. Do not pass `Ex`/`Ey`/`Ez`/`Bx`/`By`/`Bz`
66
+ or `E_x`/`B_x`-style aliases; those are rejected as unexpected keywords before
67
+ WarpX initializes. `ConstantAppliedField` is different: it takes numeric
68
+ `Ex`/`Ey`/`Ez`/`Bx`/`By`/`Bz` (not `E1`/`E2`/`E3` and not `*_expression`).
69
+ Supplying only one analytic component can reach WarpX initialization and then
70
+ abort when another external-grid parser entry is absent. Inspect the native
71
+ input for `B_ext_grid_init_style = parse_b_ext_grid_function` (and the analogous
72
+ electric entry when used); particle-applied external fields are a different
73
+ mechanism.
74
+
75
+ For a spatially varying thermal spread in `AnalyticDistribution`, the pinned
76
+ WarpX subclass consumes `warpx_momentum_spread_expressions`, not the generic
77
+ `momentum_spread_expressions` argument inherited from `picmistandard`. The
78
+ generic argument can be accepted while disappearing from the realized native
79
+ input, leaving only the directed momentum parser. Use numeric-literal strings
80
+ for constant spreads, or make every parser symbol part of an expression path
81
+ that the installed constructor actually registers. Verify native
82
+ `momentum_distribution_type = "maxwellian"`, all three
83
+ `u*_std_function(x,y,z)` expressions, and their `/c` normalization. A custom
84
+ keyword used only by the generic spread expressions can be rejected as an
85
+ unexpected keyword. For a spatially uniform Gaussian, `rms_velocity` is a
86
+ simpler standard interface; still inspect `ux_th`, `uy_th`, and `uz_th`.
87
+
88
+ The pinned Cartesian PICMI grid accepts field boundary strings `periodic`,
89
+ `open`, `dirichlet`, `neumann`, and `absorbing_silver_mueller`, subject to
90
+ solver compatibility. Particle boundary strings are `periodic`, `absorbing`,
91
+ `reflect`, and `thermal`. These are numerical boundary behaviors, not sources.
92
+ If the physics needs continuing material or flux injection, author and
93
+ commission an explicit supported injection/source mechanism; do not relabel an
94
+ open or absorbing boundary as inflow.
95
+
96
+ Call `simulation.write_input_file(file_name="inputs")` before stepping and set
97
+ `warpx_used_inputs_file` on `Simulation` when the realized controls matter.
98
+ Inspect both records. Do not repeatedly introspect the runtime for the wiring
99
+ listed here.
100
+
101
+ `PseudoRandomLayout(seed=...)` prints a warning because WarpX 26.07 does not
102
+ honor a layout-local seed. Use the simulation/runtime seed controls, retain the
103
+ warning, and verify stochastic reproducibility rather than assuming the
104
+ requested layout seed took effect.
105
+
106
+ Before a 2D Cartesian calculation, read `2d-xz-commissioning.md` in full.
107
+
108
+ ## WarpX 1D coordinate mapping
109
+
110
+ `Cartesian1DGrid` uses `z` as its sole physical spatial axis. When using
111
+ `ParticleListDistribution`, supply longitudinal positions through `z` and
112
+ longitudinal proper velocities through `uz`; `x`, `y`, `ux`, and `uy` remain
113
+ transverse components. The longitudinal electric-field diagnostic is the `z`
114
+ component of the `E` mesh. Verify the mapping in `warpx_used_inputs_file` and
115
+ particle diagnostics when commissioning a new loading method.
116
+
117
+ Use `read_skill` on the smoke example only to learn object wiring. Select all
118
+ scientific values from the active hypothesis and commissioning reasoning.
@@ -0,0 +1,24 @@
1
+ # Resource scaling
2
+
3
+ Estimate cost before launching a calculation. For explicit PIC, particle work
4
+ typically grows with cells times macroparticles per cell times species times
5
+ steps. Field work grows with cells, components, solver iterations, and steps.
6
+ Diagnostics can dominate storage when full particles or frequent mesh dumps
7
+ are requested.
8
+
9
+ Start commissioning with the smallest case that exercises the intended
10
+ geometry, species, solver, boundaries, and diagnostic path. Increase one
11
+ relevant fidelity axis at a time. Keep heavy raw output sparse unless the
12
+ observable requires it; use reduced diagnostics for inexpensive monitoring,
13
+ but retain enough raw information to challenge the estimator.
14
+
15
+ The sandbox exposes two distinct instruments: `warpx-cpu-26.07` is a bounded
16
+ eight-thread OpenMP CPU profile, while `warpx-cuda-openpmd-26.07` is a
17
+ non-MPI, 2D CUDA profile with HDF5 openPMD output. Read `cpu-launch-tuning.md`
18
+ or `gpu-launch-tuning.md` before a long campaign. Do not assume the GPU is
19
+ faster: run a bounded commissioning case with the actual workload, then spend
20
+ its measured throughput on the fidelity axis that most limits the intended
21
+ inference. Large multidimensional, high-particle, or long-duration proposals
22
+ may still be unreachable even though WarpX supports them on other machines.
23
+ An honest instrument-unreachable result is preferable to silently weakening
24
+ the physical question.
@@ -0,0 +1,58 @@
1
+ # Electromagnetic time integration
2
+
3
+ Select the evolve scheme from the physical timescales and observable. Do not
4
+ infer a timestep from a familiar dimensional value alone. Record `dt` in
5
+ seconds and normalized to every potentially limiting plasma, cyclotron,
6
+ collision, transit, growth, drive, and diagnostic timescale.
7
+
8
+ ## Explicit electromagnetic PIC
9
+
10
+ Omitting `warpx_evolve_scheme` uses WarpX's explicit default. For an auditable
11
+ choice, construct `picmi.ExplicitEvolveScheme()` and pass it to
12
+ `picmi.Simulation(..., warpx_evolve_scheme=evolve_scheme)`.
13
+
14
+ For an explicit Yee update, enforce the multidimensional electromagnetic CFL
15
+ condition
16
+
17
+ `c*dt*sqrt(sum_i(1/dx_i**2)) < 1`.
18
+
19
+ For equal cells in two dimensions this gives `dt < dx/(c*sqrt(2))`. Also
20
+ resolve any plasma, gyro, collision, particle-crossing, or source timescale
21
+ that can affect the proposed observable. A CFL-passing step is not by itself
22
+ an accurate kinetic step.
23
+
24
+ ## Implicit electromagnetic PIC
25
+
26
+ The pinned 26.07 PICMI runtime exposes:
27
+
28
+ - `picmi.ThetaImplicitEMEvolveScheme(nonlinear_solver=..., theta=...)`;
29
+ - `picmi.SemiImplicitEMEvolveScheme(nonlinear_solver=...)`;
30
+ - `picmi.NewtonNonlinearSolver(...)` and
31
+ `picmi.PicardNonlinearSolver(...)`; and
32
+ - `picmi.GMRESLinearSolver(...)` for a Newton solve when appropriate.
33
+
34
+ Pass the selected evolve object through
35
+ `picmi.Simulation(..., warpx_evolve_scheme=evolve_scheme)`. Require nonlinear
36
+ convergence and preserve the configured tolerance, iteration limit, iteration
37
+ history, and failure state. Inspect `warpx_used_inputs_file` for
38
+ `algo.evolve_scheme = theta_implicit_em` or `semi_implicit_em`, the intended
39
+ `implicit_evolve.nonlinear_solver`, and the exact `warpx.const_dt`.
40
+
41
+ Implicit advancement removes the explicit electromagnetic CFL condition as a
42
+ stability restriction; it does not remove accuracy restrictions. The step
43
+ must still resolve the dynamics used as evidence, particle motion relevant to
44
+ the estimator, collisions or forcing as implemented, diagnostic sampling, and
45
+ changes in the represented state. Qualify the chosen step against a smaller
46
+ `dt` while holding the physical case, duration, seed policy, and estimator
47
+ fixed. Reject a run whose nonlinear solve fails or whose result depends
48
+ materially on solver tolerances.
49
+
50
+ On the local 2D CUDA/openPMD 26.07 runtime, theta-implicit Picard has
51
+ repeatedly aborted with CUDA error 700 inside current deposition when a
52
+ loaded 2D electromagnetic plasma is advanced, even when the one-step implicit
53
+ wiring smoke succeeds. That is a scheme-specific solver-path failure, not an
54
+ openPMD/HDF5 failure. If the operator forbids implicit evolution or requires
55
+ explicit electromagnetic PIC, do not spend the campaign commissioning implicit
56
+ loaded-plasma cases. Use `ExplicitEvolveScheme` (or the
57
+ explicit default), enforce the multidimensional CFL condition, and treat
58
+ implicit only as a documented unavailable path.
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+ """Benchmark one exact operator-supplied program argv across OpenMP settings."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import csv
8
+ import json
9
+ import os
10
+ import re
11
+ import statistics
12
+ import subprocess
13
+ import time
14
+ from pathlib import Path
15
+
16
+ EVOLVE_RE = re.compile(
17
+ r"Evolve time = ([0-9.eE+-]+) s;.*Avg\. per step = ([0-9.eE+-]+) s"
18
+ )
19
+ PROFILE_EVOLVE_RE = re.compile(
20
+ r"^WarpX::Evolve\(\)\s+1\s+([0-9.eE+-]+)", re.MULTILINE
21
+ )
22
+
23
+
24
+ def parse_args() -> argparse.Namespace:
25
+ parser = argparse.ArgumentParser()
26
+ parser.add_argument("--python", type=Path, required=True)
27
+ parser.add_argument("--program", type=Path, required=True)
28
+ parser.add_argument("--output", type=Path, required=True)
29
+ parser.add_argument("--threads", required=True)
30
+ parser.add_argument("--repeats", type=int, default=2)
31
+ parser.add_argument(
32
+ "--affinity", choices=("none", "close", "spread"), default="close"
33
+ )
34
+ parser.add_argument("--places", choices=("cores", "threads"), default="cores")
35
+ parser.add_argument("--required-path", action="append", required=True)
36
+ parser.add_argument("program_argv", nargs=argparse.REMAINDER)
37
+ args = parser.parse_args()
38
+ if args.program_argv[:1] == ["--"]:
39
+ args.program_argv = args.program_argv[1:]
40
+ return args
41
+
42
+
43
+ def contained_paths(values: list[str]) -> tuple[Path, ...]:
44
+ paths = tuple(Path(value) for value in values)
45
+ for path in paths:
46
+ if path.is_absolute() or ".." in path.parts:
47
+ raise ValueError("--required-path must be a contained relative path")
48
+ return paths
49
+
50
+
51
+ def amrex_capabilities(python: Path) -> dict[str, object]:
52
+ probe = subprocess.run(
53
+ [
54
+ str(python),
55
+ "-c",
56
+ (
57
+ "import json, amrex.space2d as a; "
58
+ "print(json.dumps({'have_mpi': bool(a.Config.have_mpi), "
59
+ "'have_omp': bool(a.Config.have_omp)}))"
60
+ ),
61
+ ],
62
+ check=True,
63
+ text=True,
64
+ capture_output=True,
65
+ )
66
+ return json.loads(probe.stdout)
67
+
68
+
69
+ def launch_environment(threads: int, affinity: str, places: str) -> dict[str, str]:
70
+ env = os.environ.copy()
71
+ env.update(
72
+ {
73
+ "OMP_NUM_THREADS": str(threads),
74
+ "OPENBLAS_NUM_THREADS": "1",
75
+ "MKL_NUM_THREADS": "1",
76
+ "NUMEXPR_NUM_THREADS": "1",
77
+ }
78
+ )
79
+ if affinity == "none":
80
+ env.pop("OMP_PROC_BIND", None)
81
+ env.pop("OMP_PLACES", None)
82
+ else:
83
+ env["OMP_PROC_BIND"] = affinity
84
+ env["OMP_PLACES"] = places
85
+ return env
86
+
87
+
88
+ def require_outputs(run: Path, paths: tuple[Path, ...]) -> dict[str, int]:
89
+ observed: dict[str, int] = {}
90
+ for relative in paths:
91
+ target = run / relative
92
+ if not target.is_file() or target.stat().st_size == 0:
93
+ raise RuntimeError(f"required nonempty output is absent: {relative}")
94
+ observed[relative.as_posix()] = target.stat().st_size
95
+ return observed
96
+
97
+
98
+ def timing_from_log(output: str) -> tuple[float | None, float | None]:
99
+ timings = EVOLVE_RE.findall(output)
100
+ if timings:
101
+ return tuple(float(value) for value in timings[-1]) # type: ignore[return-value]
102
+ profile = [float(value) for value in PROFILE_EVOLVE_RE.findall(output)]
103
+ return (max(profile), None) if profile else (None, None)
104
+
105
+
106
+ def run_one(
107
+ args: argparse.Namespace,
108
+ required: tuple[Path, ...],
109
+ threads: int,
110
+ repeat: int,
111
+ ) -> dict[str, object]:
112
+ run = args.output / f"omp-{threads:03d}-{args.affinity}-{args.places}-r{repeat}"
113
+ run.mkdir(parents=True, exist_ok=False)
114
+ command = [str(args.python), str(args.program), *args.program_argv]
115
+ started = time.perf_counter()
116
+ completed = subprocess.run(
117
+ command,
118
+ cwd=run,
119
+ env=launch_environment(threads, args.affinity, args.places),
120
+ text=True,
121
+ stdout=subprocess.PIPE,
122
+ stderr=subprocess.STDOUT,
123
+ )
124
+ wall_s = time.perf_counter() - started
125
+ (run / "benchmark.log").write_text(completed.stdout)
126
+ if completed.returncode:
127
+ raise RuntimeError(
128
+ f"threads={threads} repeat={repeat} exited {completed.returncode}; "
129
+ f"see {run / 'benchmark.log'}"
130
+ )
131
+ outputs = require_outputs(run, required)
132
+ evolve_s, step_s = timing_from_log(completed.stdout)
133
+ return {
134
+ "threads": threads,
135
+ "affinity": args.affinity,
136
+ "places": args.places,
137
+ "repeat": repeat,
138
+ "wall_s": wall_s,
139
+ "warpx_evolve_s": evolve_s,
140
+ "warpx_step_s": step_s,
141
+ "required_outputs": json.dumps(outputs, sort_keys=True),
142
+ "run_dir": str(run),
143
+ }
144
+
145
+
146
+ def main() -> int:
147
+ args = parse_args()
148
+ args.python = args.python.resolve()
149
+ args.program = args.program.resolve()
150
+ args.output = args.output.resolve()
151
+ required = contained_paths(args.required_path)
152
+ threads = [int(value) for value in args.threads.split(",")]
153
+ if not threads or any(value <= 0 for value in threads):
154
+ raise ValueError("--threads must contain positive integers")
155
+ if args.repeats <= 0:
156
+ raise ValueError("--repeats must be positive")
157
+ capabilities = amrex_capabilities(args.python)
158
+ mpi_world_size = int(
159
+ os.environ.get("OMPI_COMM_WORLD_SIZE")
160
+ or os.environ.get("PMI_SIZE")
161
+ or os.environ.get("MPI_LOCALNRANKS")
162
+ or "1"
163
+ )
164
+ if mpi_world_size > 1 and not capabilities["have_mpi"]:
165
+ raise RuntimeError(
166
+ "refusing MPI launch: loaded AMReX reports have_mpi=false"
167
+ )
168
+ args.output.mkdir(parents=True, exist_ok=False)
169
+ rows = [
170
+ run_one(args, required, count, repeat)
171
+ for count in threads
172
+ for repeat in range(1, args.repeats + 1)
173
+ ]
174
+ with (args.output / "runs.csv").open("w", newline="") as stream:
175
+ writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
176
+ writer.writeheader()
177
+ writer.writerows(rows)
178
+ summaries = [
179
+ {
180
+ "threads": count,
181
+ "median_wall_s": statistics.median(
182
+ float(row["wall_s"]) for row in rows if row["threads"] == count
183
+ ),
184
+ }
185
+ for count in threads
186
+ ]
187
+ result = {
188
+ "capabilities": capabilities,
189
+ "observed_mpi_world_size": mpi_world_size,
190
+ "program": str(args.program),
191
+ "program_argv": args.program_argv,
192
+ "required_paths": [path.as_posix() for path in required],
193
+ "repeats": args.repeats,
194
+ "affinity": args.affinity,
195
+ "places": args.places,
196
+ "summaries": summaries,
197
+ "best": min(summaries, key=lambda value: value["median_wall_s"]),
198
+ }
199
+ (args.output / "summary.json").write_text(
200
+ json.dumps(result, indent=2, sort_keys=True) + "\n"
201
+ )
202
+ print(json.dumps(result, indent=2, sort_keys=True))
203
+ return 0
204
+
205
+
206
+ if __name__ == "__main__":
207
+ raise SystemExit(main())
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env python3
2
+ """Benchmark exact operator-supplied program argv cases on the CUDA runtime."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import csv
8
+ import json
9
+ import os
10
+ import re
11
+ import statistics
12
+ import subprocess
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ EVOLVE_RE = re.compile(
18
+ r"Evolve time = ([0-9.eE+-]+) s;.*Avg\. per step = ([0-9.eE+-]+) s"
19
+ )
20
+ PROFILE_EVOLVE_RE = re.compile(
21
+ r"^WarpX::Evolve\(\)\s+1\s+([0-9.eE+-]+)", re.MULTILINE
22
+ )
23
+ LABEL_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
24
+
25
+
26
+ def parse_args() -> argparse.Namespace:
27
+ parser = argparse.ArgumentParser()
28
+ parser.add_argument("--python", type=Path, required=True)
29
+ parser.add_argument("--program", type=Path, required=True)
30
+ parser.add_argument("--cases", type=Path, required=True)
31
+ parser.add_argument("--output", type=Path, required=True)
32
+ parser.add_argument("--repeats", type=int, default=2)
33
+ parser.add_argument("--required-path", action="append", required=True)
34
+ return parser.parse_args()
35
+
36
+
37
+ def contained_paths(values: list[str]) -> tuple[Path, ...]:
38
+ paths = tuple(Path(value) for value in values)
39
+ for path in paths:
40
+ if path.is_absolute() or ".." in path.parts:
41
+ raise ValueError("--required-path must be a contained relative path")
42
+ return paths
43
+
44
+
45
+ def read_cases(path: Path) -> tuple[dict[str, Any], ...]:
46
+ payload = json.loads(path.read_text())
47
+ cases = payload.get("cases") if isinstance(payload, dict) else None
48
+ if not isinstance(cases, list) or not cases:
49
+ raise ValueError("cases file must contain a nonempty 'cases' list")
50
+ result = []
51
+ labels = set()
52
+ for item in cases:
53
+ if not isinstance(item, dict) or set(item) != {"label", "argv"}:
54
+ raise ValueError("each case must contain exactly 'label' and 'argv'")
55
+ label = item["label"]
56
+ argv = item["argv"]
57
+ if not isinstance(label, str) or not LABEL_RE.fullmatch(label):
58
+ raise ValueError("case labels may contain only letters, digits, ._- ")
59
+ if label in labels:
60
+ raise ValueError(f"duplicate case label: {label}")
61
+ if not isinstance(argv, list) or not all(isinstance(v, str) for v in argv):
62
+ raise ValueError(f"case {label!r} argv must be a list of strings")
63
+ labels.add(label)
64
+ result.append({"label": label, "argv": argv})
65
+ return tuple(result)
66
+
67
+
68
+ def amrex_capabilities(python: Path) -> dict[str, object]:
69
+ code = (
70
+ "import json, amrex.space2d as a; "
71
+ "print(json.dumps({'have_mpi': bool(a.Config.have_mpi), "
72
+ "'have_omp': bool(a.Config.have_omp), "
73
+ "'have_gpu': bool(a.Config.have_gpu), "
74
+ "'gpu_backend': str(a.Config.gpu_backend)}))"
75
+ )
76
+ probe = subprocess.run(
77
+ [str(python), "-c", code], check=True, text=True, capture_output=True
78
+ )
79
+ return json.loads(probe.stdout)
80
+
81
+
82
+ def gpu_identity() -> dict[str, str]:
83
+ query = subprocess.run(
84
+ [
85
+ "nvidia-smi",
86
+ "--query-gpu=name,memory.total,compute_cap,driver_version",
87
+ "--format=csv,noheader,nounits",
88
+ ],
89
+ check=True,
90
+ text=True,
91
+ capture_output=True,
92
+ )
93
+ name, memory_mib, compute_cap, driver = (
94
+ value.strip() for value in query.stdout.splitlines()[0].split(",")
95
+ )
96
+ return {
97
+ "name": name,
98
+ "memory_mib": memory_mib,
99
+ "compute_capability": compute_cap,
100
+ "driver_version": driver,
101
+ }
102
+
103
+
104
+ def capability_environment(python: Path) -> dict[str, str]:
105
+ env = os.environ.copy()
106
+ python_site = subprocess.run(
107
+ [str(python), "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"],
108
+ check=True,
109
+ text=True,
110
+ capture_output=True,
111
+ ).stdout.strip()
112
+ bundled_site = str(Path(python_site) / "pywarpx" / "site-packages")
113
+ env["PYTHONPATH"] = os.pathsep.join(
114
+ value for value in (bundled_site, python_site, env.get("PYTHONPATH", "")) if value
115
+ )
116
+ env["WARPX_PYTHON_SITE"] = python_site
117
+ env.update(
118
+ {
119
+ "OMP_NUM_THREADS": "1",
120
+ "OPENBLAS_NUM_THREADS": "1",
121
+ "MKL_NUM_THREADS": "1",
122
+ "NUMEXPR_NUM_THREADS": "1",
123
+ }
124
+ )
125
+ return env
126
+
127
+
128
+ def require_outputs(run: Path, paths: tuple[Path, ...]) -> dict[str, int]:
129
+ observed: dict[str, int] = {}
130
+ for relative in paths:
131
+ target = run / relative
132
+ if not target.is_file() or target.stat().st_size == 0:
133
+ raise RuntimeError(f"required nonempty output is absent: {relative}")
134
+ observed[relative.as_posix()] = target.stat().st_size
135
+ return observed
136
+
137
+
138
+ def timing_from_log(output: str) -> tuple[float | None, float | None]:
139
+ timings = EVOLVE_RE.findall(output)
140
+ if timings:
141
+ return tuple(float(value) for value in timings[-1]) # type: ignore[return-value]
142
+ profile = [float(value) for value in PROFILE_EVOLVE_RE.findall(output)]
143
+ return (max(profile), None) if profile else (None, None)
144
+
145
+
146
+ def run_one(
147
+ args: argparse.Namespace,
148
+ case: dict[str, Any],
149
+ required: tuple[Path, ...],
150
+ repeat: int,
151
+ env: dict[str, str],
152
+ ) -> dict[str, object]:
153
+ run = args.output / f"{case['label']}-r{repeat}"
154
+ run.mkdir(parents=True, exist_ok=False)
155
+ command = [str(args.python), str(args.program), *case["argv"]]
156
+ started = time.perf_counter()
157
+ completed = subprocess.run(
158
+ command,
159
+ cwd=run,
160
+ env=env,
161
+ text=True,
162
+ stdout=subprocess.PIPE,
163
+ stderr=subprocess.STDOUT,
164
+ )
165
+ wall_s = time.perf_counter() - started
166
+ (run / "benchmark.log").write_text(completed.stdout)
167
+ if completed.returncode:
168
+ raise RuntimeError(
169
+ f"case={case['label']} repeat={repeat} exited {completed.returncode}; "
170
+ f"see {run / 'benchmark.log'}"
171
+ )
172
+ outputs = require_outputs(run, required)
173
+ evolve_s, step_s = timing_from_log(completed.stdout)
174
+ return {
175
+ "label": case["label"],
176
+ "repeat": repeat,
177
+ "wall_s": wall_s,
178
+ "warpx_evolve_s": evolve_s,
179
+ "warpx_step_s": step_s,
180
+ "argv": json.dumps(case["argv"]),
181
+ "required_outputs": json.dumps(outputs, sort_keys=True),
182
+ "run_dir": str(run),
183
+ }
184
+
185
+
186
+ def main() -> int:
187
+ args = parse_args()
188
+ args.python = args.python.resolve()
189
+ args.program = args.program.resolve()
190
+ args.output = args.output.resolve()
191
+ cases = read_cases(args.cases.resolve())
192
+ required = contained_paths(args.required_path)
193
+ if args.repeats <= 0:
194
+ raise ValueError("--repeats must be positive")
195
+ capabilities = amrex_capabilities(args.python)
196
+ if not capabilities["have_gpu"]:
197
+ raise RuntimeError("refusing benchmark: loaded AMReX reports have_gpu=false")
198
+ if str(capabilities["gpu_backend"]).upper() != "CUDA":
199
+ raise RuntimeError(
200
+ f"this harness expects CUDA, got {capabilities['gpu_backend']}"
201
+ )
202
+ args.output.mkdir(parents=True, exist_ok=False)
203
+ env = capability_environment(args.python)
204
+ rows = [
205
+ run_one(args, case, required, repeat, env)
206
+ for case in cases
207
+ for repeat in range(1, args.repeats + 1)
208
+ ]
209
+ with (args.output / "runs.csv").open("w", newline="") as stream:
210
+ writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
211
+ writer.writeheader()
212
+ writer.writerows(rows)
213
+ summaries = [
214
+ {
215
+ "label": case["label"],
216
+ "argv": case["argv"],
217
+ "median_wall_s": statistics.median(
218
+ float(row["wall_s"])
219
+ for row in rows
220
+ if row["label"] == case["label"]
221
+ ),
222
+ }
223
+ for case in cases
224
+ ]
225
+ result = {
226
+ "capabilities": capabilities,
227
+ "gpu": gpu_identity(),
228
+ "program": str(args.program),
229
+ "required_paths": [path.as_posix() for path in required],
230
+ "repeats": args.repeats,
231
+ "summaries": summaries,
232
+ "best": min(summaries, key=lambda value: value["median_wall_s"]),
233
+ }
234
+ (args.output / "summary.json").write_text(
235
+ json.dumps(result, indent=2, sort_keys=True) + "\n"
236
+ )
237
+ print(json.dumps(result, indent=2, sort_keys=True))
238
+ return 0
239
+
240
+
241
+ if __name__ == "__main__":
242
+ raise SystemExit(main())