Repository navigation
Profiling examples: polar vs tensor-product splines (Poisson, CurlCurl, GyrokineticPoisson; cylinder, torus); pproc of polar fields - #759
Merged
Conversation
…l; cylinder, torus); pproc of polar fields
- New profiling cases Poisson/CurlCurl polar_{cylinder,torus}_strong_scaling: one params file per
model and geometry, variants (spline space, preconditioner, poloidal solver) selected with flags,
set by the submit scripts (--variant). Polar splines on IGAPolarCylinder/IGAPolarTorus versus
tensor-product splines on HollowCylinder/HollowTorus (a1=1e-2); manufactured solutions with the
largest gradient on the axis (Gaussian centered w/sqrt(2) off the axis), RHS by sympy; errors
also saved close to the axis.
- Output: load polar fields from the saved extracted coefficients and recover the polar vector
(as for restarts); before, polar fields evaluated to zero (or failed). Test test_pproc_polar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts once per snapshot, cached; parallel pproc in test; --precond choices) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es (cylinder/ScrewPinch, torus/AdhocTorus)
- polar_{cylinder,torus}_strong_scaling/params_gyrokinetic_poisson.py with the flags of the Poisson
and CurlCurl examples, and submit scripts with --variant.
- Manufactured solution with the largest gradient on the axis and a non-zero average <phi>
(exact for the cylinder, quadrature in theta for the torus); rho from sympy in Cartesian
coordinates, with the equilibrium checked against the struphy equilibrium at startup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mory), wider equilibrium checks, polar_mass_ilu variant Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd geometry for the existing cases The per-variant folders of the Poisson, CurlCurl and GyrokineticPoisson cube/hollow strong scaling cases differed only in the preconditioner. They are merged into one params file per model and geometry (flags --precond and --num-elements; name and description from the preconditioner) and one submit script with --variant (default none). The labels of the former variants are kept, so that the result histories in the profiling-data repo continue. 19 params files and 19 submit scripts removed (about -6300 lines); all 27 former variants give identical iterations and errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
spossann
added this pull request to stack #763
October 13, 2026 05:45
max-models
approved these changes
Oct 13, 2026
max-models
left a comment
Member
There was a problem hiding this comment.
In these submit scripts, there are way more parameters than we had in the old ones. It's not really a problem, but sometimes it may be hard to know exactly what was used when inspecting the results. Looks clear enough to my human eyes though.
spossann
added a commit
that referenced
this pull request
Oct 13, 2026
…ines in particle propagators) (#761) Stacked on #759 (`polar-profiling`). It retargets to `devel` once #758/#759 are merged. ## Summary Particle markers can now run on domains with a **pole at η1 = 0** (no hole around the axis), with polar splines for the FEEC parts. Examples are `IGAPolarCylinder` and `IGAPolarTorus` (HollowCylinder/HollowTorus with `a1=0` also work in kernels). Before this PR, `Vlasov` and `GuidingCenter` failed on such domains: several propagators did not handle `PolarVector`, no marker boundary condition handled the axis, and the kernels divided by det DF = 0. This prepares the Vlasov/GuidingCenter profiling examples (next PR, stacked on this one). ### Marker push through the axis - **New `update_position_stage`** (`pic/pushing/pusher_utilities_kernels.py`): the stage update of the explicit Runge–Kutta pushers (PushEta, explicit GC bxE\* and B\*) is done in ξ = η1·(cos 2πη2, sin 2πη2), keeping η3. - In these coordinates the mapping and the marker velocity dξ/dt = (∂ξ/∂η)·dη/dt are regular at the pole, and markers move through the axis. - Straight lines through the axis of a cylinder are exact (tested to 1e-10). - Without a pole the update is unchanged. - **Axis map** in `apply_kinetic_bc_marker` (Pyccel and CUDA) and `Particles.apply_kinetic_bc`: - A marker with η1 < 0 is mapped to (−η1, η2 + ½), with unchanged velocities. - η1 is kept ≥ 1e-10, also after loading (Sobol loading starts at η = 0). - The boundary condition of axis 0 then applies at η1 = 1 only. - **New field `MarkerArguments.pole`**, also in `CudaMarkerArguments` and `pusher_args.cuh`. - **Not supported yet:** pole domains on the CuPy backend, and `poc`/`pol_period` ≠ 1. Both raise `NotImplementedError`. ### Polar splines in particle propagators - **PushVxB:** the projected equilibrium `b2` (already tensor-product) was extracted a second time, which failed with polar splines. Only the `b2` variable is extracted now. - **PushGuidingCenterBxEstar/Parallel:** φ, the electric field and b̃ are passed to the kernels as tensor-product coefficients (`PolarVector` has no `_data`). The gradients use `derham.grad`, boundary operators included, followed by E^T. Without polar splines nothing changes. ### Other fixes found along the way - **Reflecting boundary:** only Cartesian velocities (vdim = 3) are reflected. For Particles5D the columns 3:6 (v∥, μ, **weight**) were overwritten. - **`CanonicalMaxwellian2D`:** removed an unused read of `domain.params["a1"]`, which raised a KeyError on IGAPolarTorus. - **`AdhocTorus`, `AdhocTorusQPsi`:** the ψ derivatives were 0/0 (NaN) on the magnetic axis. Off the axis they are unchanged (4e-16); on the axis they take the correct limits. - Note: AdhocTorusQPsi's interpolated ψ has ψ′(0) ≈ −6e-5 ≠ 0, so its field is not exactly regular on the axis. That is pre-existing and not changed here. - **`GuidingCenter`:** `en_fB` summed over holes and ghosts instead of the valid markers. Each removed marker added +1 to `en_tot`. ### Disc loading under MPI (pre-existing bug, not pole-related) With `spatial="disc"` on more than one process, markers were lost in the first push or the run hung: - **Disc transform after the selection:** `pseudo_random` loading selected the uniform draws on each process's domain and applied η1 → √η1 afterwards. That moved many markers to another process's domain. - Without sorting boxes they were not sent there, so the first push evaluated the fields outside the local domain. - Example: GuidingCenter on 4 processes removed 15% of the markers in the first step. - The transform is now applied before the domain check. - **Sort after loading:** markers are sent to their process even without sorting boxes (not for restart loading). Sobol loading draws on the whole domain on each process, so it needed this too. - **Marker array size:** the number of markers per process was estimated with the uniform measure. With the disc measure 2η1, processes at larger η1 overflowed the marker array: a ValueError on some processes and a hang on the others. - **New test:** `test_draw_disc_on_own_process` (pseudo_random and sobol_standard) in `test_draw_parallel.py`. ### Known limitations - **Discrete-gradient GC pushers:** they only get the axis map. Their mid-points in η are not regular near the axis. - In the test (ε = 1), relative energy errors near the axis reach 1e-3 to 5e-2. - With the explicit RK4 pushers, markers that cross the axis conserve their energy to about 1e-6. - **Buffer size:** with few markers per process, the discrete-gradient pushers need a larger `bufsize` on 4 ranks (load-imbalance abort in the box sort). This is also the case on a torus with a hole, so it is not pole-related. ## Tests New file `src/struphy/pic/tests/test_pole.py`, which passes serially and on 4 MPI ranks: - the axis map in the kernel and in Python, including a marker with η1 > 1; - reflection for vdim = 2 and 3; - PushEta straight lines through the axis (forward Euler, Heun2, RK4) against the exact Cartesian end points; - Vlasov on IGAPolarCylinder with ScrewPinch and polar splines: kinetic energy conserved to 1e-12; - GuidingCenter on IGAPolarTorus with AdhocTorus and polar splines, explicit and discrete-gradient pushers: energies per marker, compared by ID, including markers that cross the axis. Also run locally: - `test_pushers.py`, `test_reflect.py`, `test_kernel_setup.py`, `test_kernel_backends.py`, `test_accum_gc_gradB.py`, `test_maxwellians.py` and `fields_background/tests` (serial); - the default-params runs of Vlasov and GuidingCenter. A separate review agent checked the branch before the push. Its findings are fixed in the second commit: - the Python axis map caught η1 > 1; - the clamp covered only one loading branch; - the GC gradients had dropped the boundary operators. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Follow-up (stacked): #762 adds the Vlasov/GuidingCenter profiling examples that use this. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
spossann
added a commit
that referenced
this pull request
Oct 13, 2026
…ingCenter (cylinder/torus without hole) (#762) Stacked on #761 (`particle-pole`, which is stacked on #759). It retargets once those are merged. ## Summary New profiling examples for the particle-tracing models **Vlasov** and **GuidingCenter**. They are long-time equilibrium tests with binning plots in position and velocity space, and the propagator algorithms are compared through variants. The layout is the same as for the existing cases (#759): - one params file plus one submit script per model and geometry; - the variant is chosen with `--variant`; - rank counts 1, 2, 4, …, 256; - post-processing writes `.png`/`.npy` files to `sim_<id>/results`. ### Cases - **`Vlasov/cuboid_strong_scaling`** - Setup: Maxwellian3D with constant moments (n=1, u=0, vth=1) in a periodic cube (Cuboid, side 2π) with a uniform B along z (HomogenSlab). This is an exact equilibrium. - Binning: η1, (η1, η2), v1, v3 and (v1, v2), compared with the initial and the exact profiles. - Variants: push_vxb analytic/implicit; push_eta forward Euler/Heun2/Heun3/RK4; Strang splitting; Sobol loading. - **`GuidingCenter/polar_cylinder_strong_scaling`** - Geometry: straight cylinder **without hole** (IGAPolarCylinder, C1 polar splines) with ScrewPinch (Lz = 2πR0, pressure offset p0 so that vth > 0 at the edge). - Initial condition: GyroMaxwellian2D with n(r) and vth(r) = √(p/n) from the equilibrium. It is an exact equilibrium, because the GC drifts keep r constant. Markers move through the axis. - Binning: η1, (η1, η2), v∥, μ and (η1, v∥). The radial profile is also compared with the exact one, n/|B0|. - **`GuidingCenter/polar_torus_strong_scaling`** - Geometry: full torus **without hole** (IGAPolarTorus, polar splines) with AdhocTorus. - Initial condition: CanonicalMaxwellian2D with n and vth as functions of ψc (an exact equilibrium). The density vanishes at the edge, where markers on orbits that leave the plasma are removed. - Control: a GyroMaxwellian2D with the same profiles in the flux label of the position. It is not an equilibrium in a torus. - **GuidingCenter variants:** discrete gradient 1st order (fixed point, Newton), 2nd order, explicit RK4 and Heun3. The torus adds GyroMaxwellian controls. - Default grid: 16×48×48, decomposed only in η2/η3, so 1–256 processes work. ppc 25, dt 0.2, T = 100. - **`profiling/examples/particle_diagnostics.py`:** shared post-processing. - Binning plots at the first and last time (2D slices also as difference maps). - Relative L2 deviation from t=0 and from the exact profile over time. - Relative drift of `kinetic_energy`/`en_tot`; scalars that start at 0, such as lost markers, are plotted as absolute values. ### Notes - **No hole at r = 0:** a Simulation needs polar splines whenever the domain has a pole, and polar splines need a spline mapping. So the cases use IGAPolarCylinder/IGAPolarTorus. HollowCylinder/HollowTorus with a1=0 cannot be used. - **Why not CircularTokamak for the cylinder:** it is a toroidal field (B_φ ∝ 1/R, circular flux surfaces around R = R0), and its ψ is not force-balanced: Δ*ψ ≠ 0 with constant p and g. Adding a pressure p(ψ) cannot balance it. The force-balanced ScrewPinch is used instead. - **Discrete-gradient GC pushers near the axis:** they are less accurate there. They only get the axis map from #761, not the regular ξ update. - **Marker loss on several processes:** with disc loading, the first test runs lost markers on several processes. That is fixed in #761 (disc loading under MPI). ## Checks - Tiny smoke runs (2 and 4 processes) of all cases complete and write the results. - On the cylinder with 4 processes (discrete gradient and explicit), the radial profile changes by about 1e-6 per step, no marker is lost and `en_tot` is constant. - Vlasov: `kinetic_energy` is constant and the v3 distribution is unchanged to 1e-16. - The flags of all 19 variants are accepted by the params files. - Two separate review agents checked the examples. Their findings are fixed: - the decomposition at 128/256 processes; - the marker loss under MPI (in #761); - the plot of scalars that start at zero; - the edge losses in the torus; - the exact radial profile; - the B0 used for loading. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
spossann
added a commit
that referenced
this pull request
Oct 21, 2026
…l, GyrokineticPoisson; cylinder, torus); pproc of polar fields (#759) ## Summary Profiling examples that show what polar splines can do: Poisson, CurlCurl and GyrokineticPoisson on a cylinder and on a torus. The manufactured solution has its **largest gradient on the axis r = 0**. Each case compares: - C1 polar splines on IGAPolarCylinder / IGAPolarTorus (no hole); - tensor-product splines on HollowCylinder / HollowTorus with a 1e-2 hole, the geometry of the existing profiling examples. Stacked on #758 (polar StiffnessPreconditioner). The base is `polar-stiffness`, so this diff shows only the new commits. ### Layout - **One params file per model and geometry:** `profiling/examples/{Poisson,CurlCurl}/polar_{cylinder,torus}_strong_scaling/params_*.py`. Command-line flags select the variant: - `--splines polar|tensor` - `--precond` - `--poloidal-solver schur|lu|ilu` - `--hole-bc free|dirichlet` - `--num-elements` - **One submit script per model and geometry, with `--variant`:** `submit_{poisson,curl_curl}_polar_{cylinder,torus}_strong_scaling.py`. The variant table sets the flags through `ProfilingCase.launch(param_flags=...)`, and the script is run once per variant. Several `ProfilingCase`s can't share one process: without `--upload`, `setup_run` deletes the shared output folder. - **Variants:** - Poisson: `tensor_{none,stiffness,multigrid}`, `polar_{none,tensor_mass,mass,mass_ilu,stiffness,stiffness_ilu}`. - CurlCurl: `tensor_{none,stiffness,hiptmair_xu,multigrid}`, `polar_{none,tensor_mass,mass,stiffness,stiffness_lu}`. - There are no ILU variants for CurlCurl, because ILU is not robust there at σ = 1e-2 (see #758). - **Grid:** (64, 66, 64), degree (3, 3, 3), for both spline spaces. Polar splines need a multiple of 3 elements in η2. - **Exact solution:** a Cartesian Gaussian (w = 0.2) centred w/√2 off the axis, where its gradient is largest. The right-hand side is derived with sympy; for CurlCurl, E = g c. On top of the existing outputs, the error close to the axis (r < 0.1) and `variant.json` are saved. - **BC at the hole (tensor-product variants):** - Poisson uses the natural BC. - CurlCurl uses Dirichlet. With the natural BC, the BC at the hole contradicts the solution, whose curl is largest on the axis. With σ = 1e-2 the error then grows to O(10³). ### Fix: post-processing of polar fields `Output._load_femfields` wrote the saved coefficients into a `PolarVector`. That either failed, or the field evaluated to zero, because `SplineFunction.__call__` re-extracts from the polar vector. Now the saved tensor-product (extracted) coefficients are loaded into `vector_stencil`, and the polar vector is recovered with the restart extraction operator: once per snapshot, cached per space. New test `test_pproc_polar` (serial and parallel pproc) fails without the fix. ### GyrokineticPoisson - `profiling/examples/GyrokineticPoisson/polar_{cylinder,torus}_strong_scaling/params_gyrokinetic_poisson.py` and their submit scripts, with the same flags. - **Equilibria**, as in the existing hollow GKP examples: - ScrewPinch (a=1, R0=1, Lz=2π), `which_geometry="cylindrical"`; - AdhocTorus (a=1, R0=3, q_kind=0, p_kind=1), `which_geometry="toroidal"`. - Defaults `stab_mat="M0ad_withT"` and `diffusion_mat="M1gyro"`. - **Manufactured solution:** the same off-axis Gaussian, times a modulation along η3, so the average ⟨φ⟩ is **non-zero**. - Cylinder: ⟨φ⟩ is exact. - Torus: ⟨φ⟩(r) = ∫g R dθ / ∫R dθ, tabulated by trapezoidal quadrature in θ. - ρ is computed with sympy in Cartesian coordinates. At startup, the symbolic n0, p and B0 are checked against the struphy equilibrium (`n_xyz`, `p_xyz`, `b_xyz`). - Leaving ⟨φ⟩ out of ρ raises the error from 6e-4 to 36%, so the averaged term is really tested. - **Variants:** `tensor_{none,stiffness,multigrid}`, `polar_{none,tensor_mass,mass,mass_ilu,stiffness,stiffness_ilu}`. Small grid (16 × 18 × 16, serial): | variant | cylinder: its | torus: its | |---|---|---| | polar, stiffness (Schur / ILU) | 7 / 7 | 5 / 5 | | polar, none | 55 | 76 | | polar, polar mass | 419 | 311 | | polar, tensor mass | 738 | 521 | | tensor-product, none | 63 | 82 | | tensor-product, stiffness | 82 | 72 | | tensor-product, multigrid | 264 | 226 | The error in φ depends only on the spline space: polar 4.9e-4 (cylinder) and 6.2e-4 (torus), tensor-product 1.5e-2 and 1.4e-2. Torus at 32 × 33 × 32, 4 ranks, stiffness preconditioner: | splines | its | solve time | error in φ | peak memory (4 ranks) | |---|---|---|---|---| | polar | 4 | 3.1 s | 4.0e-5 | 7.0 GB | | tensor-product | 109 | 31 s | 2.2e-2 | 5.6 GB | - **Full-size runs (64 × 66 × 64) are for the cluster.** They did not fit into 15 GB on a workstation; already at 32³ the tensor-product run takes 5.6 GB. - **The polar stiffness preconditioner is not exact for GKP** (4–7 iterations rather than 2), because it drops the C^23 (η2–η3) coupling of M1gyro, see #758. - **The tensor-product preconditioners are worse than none** on the hollow cylinder with the free hole BC: stiffness 82, multigrid 264, none 63. ### Compacted layout of the existing cube/hollow examples - The existing Poisson, CurlCurl and GyrokineticPoisson cube/hollow strong scaling cases now use the same layout as the polar ones: - one params file per model and geometry (`{geo}_strong_scaling/params_*.py`, flags `--precond` and `--num-elements`); - one submit script per model and geometry (`--variant`, default `none`). - Removed: 19 per-variant params folders and 19 submit scripts (about −6300 lines). - **The labels of the former variants are unchanged** (e.g. `poisson_hollow_cylinder_strong_scaling_multigrid`), so the result histories in the profiling-data repo continue. Names and descriptions are also byte-identical. - The flags of a run are recorded in the packaged job script (`slurm_script_str`). - **Verified:** - All 27 former variants (at 16³) give identical CG iterations and errors with the old and the new files. - A review compared each variant's effective parameters, names, descriptions and labels against the old files. The explicit `multigrid=MultiGridOptions()` was removed because it equals the propagator default. ## Results Small grid (16 × 18 × 16, serial; Poisson cylinder 16 × 18 × 8), Stiffness preconditioner: | case | polar: iterations, error | tensor-product (hole): iterations, error | |---|---|---| | Poisson cylinder | 2, 8.8e-4 | 81 (no pc), 1.35e-2 | | Poisson torus | 2, 5.4e-4 | 89 (no pc), 1.36e-2 | | CurlCurl cylinder | 6, 1.4e-3 | 65, 1.24 | | CurlCurl torus | 8, 1.3e-3 | 118, 24.6 | Full size, Poisson cylinder, 64 × 66 × 64, 4 ranks, Stiffness preconditioner: | splines | iterations | solve time | max error in φ | |---|---|---|---| | polar | 2 | 4.0 s | 2.1e-6 | | tensor-product | 192 | 158 s | 2.9e-2 | The tensor-product error does not converge under refinement because of the hole. Notes for interpreting the runs: - At full size, the unpreconditioned CurlCurl variants (and probably `polar_tensor_mass`) will hit `maxiter = 10000`. Their timings then measure 10000 iterations, not a solve. - The mass-matrix preconditioners are worse than none for Poisson (they approximate M0, not the Laplacian). They are kept as reference variants. - MultiGrid on the hollow torus with the free hole BC is worse than no preconditioner for Poisson. This is not a regression of this PR. ## Tests - `test_pproc_polar` (new): serial and 2 ranks. - `post_processing/tests/test_output.py` and `test_pproc.py` (including `--with-mpi` on 2 ranks) pass. - All six params files run in serial and on 2 ranks. All 17 GKP variants run at small size. The reviewer ran all 37 variants at small size: all exit 0. - Two separate review passes (Poisson/CurlCurl, and GKP with an independent finite-difference check of the operator) found no bugs; their suggestions are applied. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
spossann
added a commit
that referenced
this pull request
Oct 21, 2026
…ines in particle propagators) (#761) Stacked on #759 (`polar-profiling`). It retargets to `devel` once #758/#759 are merged. ## Summary Particle markers can now run on domains with a **pole at η1 = 0** (no hole around the axis), with polar splines for the FEEC parts. Examples are `IGAPolarCylinder` and `IGAPolarTorus` (HollowCylinder/HollowTorus with `a1=0` also work in kernels). Before this PR, `Vlasov` and `GuidingCenter` failed on such domains: several propagators did not handle `PolarVector`, no marker boundary condition handled the axis, and the kernels divided by det DF = 0. This prepares the Vlasov/GuidingCenter profiling examples (next PR, stacked on this one). ### Marker push through the axis - **New `update_position_stage`** (`pic/pushing/pusher_utilities_kernels.py`): the stage update of the explicit Runge–Kutta pushers (PushEta, explicit GC bxE\* and B\*) is done in ξ = η1·(cos 2πη2, sin 2πη2), keeping η3. - In these coordinates the mapping and the marker velocity dξ/dt = (∂ξ/∂η)·dη/dt are regular at the pole, and markers move through the axis. - Straight lines through the axis of a cylinder are exact (tested to 1e-10). - Without a pole the update is unchanged. - **Axis map** in `apply_kinetic_bc_marker` (Pyccel and CUDA) and `Particles.apply_kinetic_bc`: - A marker with η1 < 0 is mapped to (−η1, η2 + ½), with unchanged velocities. - η1 is kept ≥ 1e-10, also after loading (Sobol loading starts at η = 0). - The boundary condition of axis 0 then applies at η1 = 1 only. - **New field `MarkerArguments.pole`**, also in `CudaMarkerArguments` and `pusher_args.cuh`. - **Not supported yet:** pole domains on the CuPy backend, and `poc`/`pol_period` ≠ 1. Both raise `NotImplementedError`. ### Polar splines in particle propagators - **PushVxB:** the projected equilibrium `b2` (already tensor-product) was extracted a second time, which failed with polar splines. Only the `b2` variable is extracted now. - **PushGuidingCenterBxEstar/Parallel:** φ, the electric field and b̃ are passed to the kernels as tensor-product coefficients (`PolarVector` has no `_data`). The gradients use `derham.grad`, boundary operators included, followed by E^T. Without polar splines nothing changes. ### Other fixes found along the way - **Reflecting boundary:** only Cartesian velocities (vdim = 3) are reflected. For Particles5D the columns 3:6 (v∥, μ, **weight**) were overwritten. - **`CanonicalMaxwellian2D`:** removed an unused read of `domain.params["a1"]`, which raised a KeyError on IGAPolarTorus. - **`AdhocTorus`, `AdhocTorusQPsi`:** the ψ derivatives were 0/0 (NaN) on the magnetic axis. Off the axis they are unchanged (4e-16); on the axis they take the correct limits. - Note: AdhocTorusQPsi's interpolated ψ has ψ′(0) ≈ −6e-5 ≠ 0, so its field is not exactly regular on the axis. That is pre-existing and not changed here. - **`GuidingCenter`:** `en_fB` summed over holes and ghosts instead of the valid markers. Each removed marker added +1 to `en_tot`. ### Disc loading under MPI (pre-existing bug, not pole-related) With `spatial="disc"` on more than one process, markers were lost in the first push or the run hung: - **Disc transform after the selection:** `pseudo_random` loading selected the uniform draws on each process's domain and applied η1 → √η1 afterwards. That moved many markers to another process's domain. - Without sorting boxes they were not sent there, so the first push evaluated the fields outside the local domain. - Example: GuidingCenter on 4 processes removed 15% of the markers in the first step. - The transform is now applied before the domain check. - **Sort after loading:** markers are sent to their process even without sorting boxes (not for restart loading). Sobol loading draws on the whole domain on each process, so it needed this too. - **Marker array size:** the number of markers per process was estimated with the uniform measure. With the disc measure 2η1, processes at larger η1 overflowed the marker array: a ValueError on some processes and a hang on the others. - **New test:** `test_draw_disc_on_own_process` (pseudo_random and sobol_standard) in `test_draw_parallel.py`. ### Known limitations - **Discrete-gradient GC pushers:** they only get the axis map. Their mid-points in η are not regular near the axis. - In the test (ε = 1), relative energy errors near the axis reach 1e-3 to 5e-2. - With the explicit RK4 pushers, markers that cross the axis conserve their energy to about 1e-6. - **Buffer size:** with few markers per process, the discrete-gradient pushers need a larger `bufsize` on 4 ranks (load-imbalance abort in the box sort). This is also the case on a torus with a hole, so it is not pole-related. ## Tests New file `src/struphy/pic/tests/test_pole.py`, which passes serially and on 4 MPI ranks: - the axis map in the kernel and in Python, including a marker with η1 > 1; - reflection for vdim = 2 and 3; - PushEta straight lines through the axis (forward Euler, Heun2, RK4) against the exact Cartesian end points; - Vlasov on IGAPolarCylinder with ScrewPinch and polar splines: kinetic energy conserved to 1e-12; - GuidingCenter on IGAPolarTorus with AdhocTorus and polar splines, explicit and discrete-gradient pushers: energies per marker, compared by ID, including markers that cross the axis. Also run locally: - `test_pushers.py`, `test_reflect.py`, `test_kernel_setup.py`, `test_kernel_backends.py`, `test_accum_gc_gradB.py`, `test_maxwellians.py` and `fields_background/tests` (serial); - the default-params runs of Vlasov and GuidingCenter. A separate review agent checked the branch before the push. Its findings are fixed in the second commit: - the Python axis map caught η1 > 1; - the clamp covered only one loading branch; - the GC gradients had dropped the boundary operators. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Follow-up (stacked): #762 adds the Vlasov/GuidingCenter profiling examples that use this. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
spossann
added a commit
that referenced
this pull request
Oct 21, 2026
…ingCenter (cylinder/torus without hole) (#762) Stacked on #761 (`particle-pole`, which is stacked on #759). It retargets once those are merged. ## Summary New profiling examples for the particle-tracing models **Vlasov** and **GuidingCenter**. They are long-time equilibrium tests with binning plots in position and velocity space, and the propagator algorithms are compared through variants. The layout is the same as for the existing cases (#759): - one params file plus one submit script per model and geometry; - the variant is chosen with `--variant`; - rank counts 1, 2, 4, …, 256; - post-processing writes `.png`/`.npy` files to `sim_<id>/results`. ### Cases - **`Vlasov/cuboid_strong_scaling`** - Setup: Maxwellian3D with constant moments (n=1, u=0, vth=1) in a periodic cube (Cuboid, side 2π) with a uniform B along z (HomogenSlab). This is an exact equilibrium. - Binning: η1, (η1, η2), v1, v3 and (v1, v2), compared with the initial and the exact profiles. - Variants: push_vxb analytic/implicit; push_eta forward Euler/Heun2/Heun3/RK4; Strang splitting; Sobol loading. - **`GuidingCenter/polar_cylinder_strong_scaling`** - Geometry: straight cylinder **without hole** (IGAPolarCylinder, C1 polar splines) with ScrewPinch (Lz = 2πR0, pressure offset p0 so that vth > 0 at the edge). - Initial condition: GyroMaxwellian2D with n(r) and vth(r) = √(p/n) from the equilibrium. It is an exact equilibrium, because the GC drifts keep r constant. Markers move through the axis. - Binning: η1, (η1, η2), v∥, μ and (η1, v∥). The radial profile is also compared with the exact one, n/|B0|. - **`GuidingCenter/polar_torus_strong_scaling`** - Geometry: full torus **without hole** (IGAPolarTorus, polar splines) with AdhocTorus. - Initial condition: CanonicalMaxwellian2D with n and vth as functions of ψc (an exact equilibrium). The density vanishes at the edge, where markers on orbits that leave the plasma are removed. - Control: a GyroMaxwellian2D with the same profiles in the flux label of the position. It is not an equilibrium in a torus. - **GuidingCenter variants:** discrete gradient 1st order (fixed point, Newton), 2nd order, explicit RK4 and Heun3. The torus adds GyroMaxwellian controls. - Default grid: 16×48×48, decomposed only in η2/η3, so 1–256 processes work. ppc 25, dt 0.2, T = 100. - **`profiling/examples/particle_diagnostics.py`:** shared post-processing. - Binning plots at the first and last time (2D slices also as difference maps). - Relative L2 deviation from t=0 and from the exact profile over time. - Relative drift of `kinetic_energy`/`en_tot`; scalars that start at 0, such as lost markers, are plotted as absolute values. ### Notes - **No hole at r = 0:** a Simulation needs polar splines whenever the domain has a pole, and polar splines need a spline mapping. So the cases use IGAPolarCylinder/IGAPolarTorus. HollowCylinder/HollowTorus with a1=0 cannot be used. - **Why not CircularTokamak for the cylinder:** it is a toroidal field (B_φ ∝ 1/R, circular flux surfaces around R = R0), and its ψ is not force-balanced: Δ*ψ ≠ 0 with constant p and g. Adding a pressure p(ψ) cannot balance it. The force-balanced ScrewPinch is used instead. - **Discrete-gradient GC pushers near the axis:** they are less accurate there. They only get the axis map from #761, not the regular ξ update. - **Marker loss on several processes:** with disc loading, the first test runs lost markers on several processes. That is fixed in #761 (disc loading under MPI). ## Checks - Tiny smoke runs (2 and 4 processes) of all cases complete and write the results. - On the cylinder with 4 processes (discrete gradient and explicit), the radial profile changes by about 1e-6 per step, no marker is lost and `en_tot` is constant. - Vlasov: `kinetic_energy` is constant and the v3 distribution is unchanged to 1e-16. - The flags of all 19 variants are accepted by the params files. - Two separate review agents checked the examples. Their findings are fixed: - the decomposition at 128/256 processes; - the marker loss under MPI (in #761); - the plot of scalars that start at zero; - the edge losses in the torus; - the exact radial profile; - the B0 used for loading. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Profiling examples that show what polar splines can do: Poisson, CurlCurl and GyrokineticPoisson on a cylinder and on a torus. The manufactured solution has its largest gradient on the axis r = 0. Each case compares:
Stacked on #758 (polar StiffnessPreconditioner). The base is
polar-stiffness, so this diff shows only the new commits.Layout
profiling/examples/{Poisson,CurlCurl}/polar_{cylinder,torus}_strong_scaling/params_*.py. Command-line flags select the variant:--splines polar|tensor--precond--poloidal-solver schur|lu|ilu--hole-bc free|dirichlet--num-elements--variant:submit_{poisson,curl_curl}_polar_{cylinder,torus}_strong_scaling.py. The variant table sets the flags throughProfilingCase.launch(param_flags=...), and the script is run once per variant. SeveralProfilingCases can't share one process: without--upload,setup_rundeletes the shared output folder.tensor_{none,stiffness,multigrid},polar_{none,tensor_mass,mass,mass_ilu,stiffness,stiffness_ilu}.tensor_{none,stiffness,hiptmair_xu,multigrid},polar_{none,tensor_mass,mass,stiffness,stiffness_lu}.variant.jsonare saved.Fix: post-processing of polar fields
Output._load_femfieldswrote the saved coefficients into aPolarVector. That either failed, or the field evaluated to zero, becauseSplineFunction.__call__re-extracts from the polar vector. Now the saved tensor-product (extracted) coefficients are loaded intovector_stencil, and the polar vector is recovered with the restart extraction operator: once per snapshot, cached per space. New testtest_pproc_polar(serial and parallel pproc) fails without the fix.GyrokineticPoisson
profiling/examples/GyrokineticPoisson/polar_{cylinder,torus}_strong_scaling/params_gyrokinetic_poisson.pyand their submit scripts, with the same flags.which_geometry="cylindrical";which_geometry="toroidal".stab_mat="M0ad_withT"anddiffusion_mat="M1gyro".n_xyz,p_xyz,b_xyz).tensor_{none,stiffness,multigrid},polar_{none,tensor_mass,mass,mass_ilu,stiffness,stiffness_ilu}.Small grid (16 × 18 × 16, serial):
The error in φ depends only on the spline space: polar 4.9e-4 (cylinder) and 6.2e-4 (torus), tensor-product 1.5e-2 and 1.4e-2.
Torus at 32 × 33 × 32, 4 ranks, stiffness preconditioner:
Compacted layout of the existing cube/hollow examples
{geo}_strong_scaling/params_*.py, flags--precondand--num-elements);--variant, defaultnone).poisson_hollow_cylinder_strong_scaling_multigrid), so the result histories in the profiling-data repo continue. Names and descriptions are also byte-identical.slurm_script_str).multigrid=MultiGridOptions()was removed because it equals the propagator default.Results
Small grid (16 × 18 × 16, serial; Poisson cylinder 16 × 18 × 8), Stiffness preconditioner:
Full size, Poisson cylinder, 64 × 66 × 64, 4 ranks, Stiffness preconditioner:
The tensor-product error does not converge under refinement because of the hole.
Notes for interpreting the runs:
polar_tensor_mass) will hitmaxiter = 10000. Their timings then measure 10000 iterations, not a solve.Tests
test_pproc_polar(new): serial and 2 ranks.post_processing/tests/test_output.pyandtest_pproc.py(including--with-mpion 2 ranks) pass.🤖 Generated with Claude Code