Skip to content

Bug fixes, correctness guards, performance and test coverage from an in-depth review - #623

Open
oschulz wants to merge 39 commits into
mainfrom
improvements
Open

Bug fixes, correctness guards, performance and test coverage from an in-depth review#623
oschulz wants to merge 39 commits into
mainfrom
improvements

Conversation

@oschulz

@oschulz oschulz commented Jul 20, 2026

Copy link
Copy Markdown
Member

In-depth review of the package: verified bug fixes, new correctness guards, performance work, and a test-coverage push. 27 commits, each scoped and with regression tests; the full suite passes under CI-like conditions (Julia 1.12, forced bounds checks, Geant4 testset included). Solver changes are bit-identical unless stated.

Bug fixes

  • Geometry/CSG: surface normals of tapered ConeMantle (≈74° off for SI-scale tapers) and EllipsoidMantle (consumed by drift reflections and E-field plots); ConeMantle single-intersection sign; two-frame frame_transformation composed in the wrong order; partial-φ serialization crash; several parsing holes now raise ConfigFileError.
  • Fields/axes: cylindrical E-field used a wrong denominator at the periodic φ-wrap (wrong e_φ on the last φ slice of every non-φ-symmetric map); searchsortednearest never snapped across the periodic seam; missing multiplicity method for periodic Cartesian axes; per-point allocations in the E-field builder.
  • SOR: the periodic boundary condition on the compressed red-black dimension copied ghost rows into ghost rows — wrapped interior values never entered the update.
  • GDML export (all verified against Geant4 readback): placement rotations were never emitted, so rotated primitives — including every partial-φ segment — collapsed onto φ = 0; polyhedra radii written as circumradius where GDML expects apothems (hexagons 4/3 too large in volume); boolean second-operand offsets lost or expressed in the wrong frame.
  • Detector construction/API: doped-contact auto-detection could never run; estimate_depletion_voltage crash when both candidates lay in range; capacitance matrix could not store its documented missing entries; unknown trapping-model names silently disabled trapping.
  • Charge transport: ADL2016 NaN velocities at zero field; unclamped acos NaN; PtypePNJunctionImpurityDensity + offset applied the offset twice; cluster_detector_hits skipped DBSCAN exactly for low-multiplicity events; floating-boundary drift corrupted shared step vectors across charges.

Correctness guards (behavior changes)

  • Grid(sim) rejects 2D and periodic φ-wedge grids for detectors without the corresponding φ-symmetry (the CGD_CylGrid example silently simulated its cuboid in 2D).
  • is_depleted throws when the potential was computed without depletion handling (was warn + true, silently disabling the estimate_depletion_voltage safety check).
  • Out-of-semiconductor charge-cloud centers throw a catchable ArgumentError instead of an assert.
  • spacing_surface_refinement parses in units.length (was silently meters — breaking for configs that relied on that).

Performance (results bit-identical)

  • GPU SOR iteration loop ~9× (3.4 → 0.37 ms/iteration, 3.4M-point cylindrical grid, A100): removed per-iteration host syncs, replaced the r = 0 boundary condition (getindex + gemv + broadcast with host round-trips) by one kernel, fused the ~10 ghost-face broadcasts into a single kernel per color, static workgroup sizes. The loop is now device-bound.
  • ADL2016 electron velocities 38× by precomputing the valley mass-tensor square roots.
  • Point classification ~2×: conservative per-object bounding spheres skip the CSG in tree walk (Speed up initialization of boundary conditions onto the grid #85), plus a single cylindrical→Cartesian conversion per grid point.

Features

  • Cones and prisms accept z: {from, to} extents as an alternative to a centered height h.

Tests and documentation

  • Line coverage measured at 74.6% overall; the notable gap closed: the whole AbstractGPUArray dispatch path was untested (the pseudoGPUArray trick never reaches it) — new JLArray testsets run it end-to-end and reproduce the CPU results bit-identically on both grid types.
  • Boundary-condition ghost-row semantics pinned per axis role and boundary-type combination, including CPU methods ≡ GPU op tables (groundwork for Boundary conditions as data, not methods #617).
  • InactiveLayerChargeDriftModel mobilities verified against the literature and pinned numerically — the code is correct; Dai et al. (2023) has a 10¹⁰ exponent typo in its neutral-impurity prefactor (details in the source comments).
  • r = 0 depletion handling: investigated in depth and deliberately kept, with documented semantics and regression tests for the ±0.5% depletion-voltage brackets (analysis in Depletion handling: clamp bistability and the hidden 0 V floor #618).
  • test/benchmarks refreshed to current APIs (CUDAKernelsCUDA; the SOR benchmark's GPU row was silently measuring the CPU path).

Related issues: #85 (partially addressed), #617/#618 (analysis from this review; groundwork tests included, refactors pending discussion), #619#622 (filed from review findings, not addressed here).

🤖 Generated with Claude Code

oschulz and others added 28 commits July 17, 2026 23:22
The tapered-cone normal used the raw local (x, y) (magnitude r) against
the unit-scaled slope dr/dz, tilting normals of SI-scale geometry almost
into +-z. The ellipsoid normal used sign(x_i)*(x_i/r_i)^2 instead of the
gradient x_i/r_i^2. Both now return normalized gradient-direction
normals. Add flip for outwards EllipsoidMantle and regression tests
against analytic normals.

Created by generative AI.
At r = 0 the cylindrical red-black layout (z, phi, r) has its
radially-inward neighbor in tuple slot 5, pointing at a ghost entry that
no boundary condition ever writes. The mask replaced slot 1 (a valid
z-neighbor) instead, so handle_depletion included the stale ghost value
in its neighbor-extremum check for every grid point on the r = 0 axis.
Mask slot 5 with the outward neighbor instead. Affects CPU and GPU
backends (shared helper).

Created by generative AI.
- SolidStateDetector: doped-contact auto-detection referenced an
  undefined variable and never injected the detected contact ids, so
  omitting doped_contact_id always crashed; it now defaults to the
  contact(s) at maximum potential as documented.
- InactiveLayerChargeDriftModel: the semiconductor temperature (already
  in internal units) was re-parsed with the config input units, shifting
  it for non-Kelvin units and making the mismatch check throw spuriously.
- Semiconductor: unknown charge_trapping_model names silently disabled
  trapping; now a ConfigFileError. Missing model key no longer raises a
  raw KeyError.
- Simulation: invalid grid coordinates hit bare @Assert with a string
  argument (TypeError at runtime); now a ConfigFileError with the
  intended message.
- estimate_depletion_voltage: crashed with only() when both voltage
  candidates lay inside the search range.
- calculate_capacitance_matrix: could never store its documented missing
  entries (MethodError); eltype narrows when all entries exist.

Add config-robustness tests covering these paths.

Created by generative AI.
The forward difference from the last phi column to phi = 0 divided by
(axphi[1] - axphi[end]), a large negative non-distance, instead of the
wrap gap (cyclic + axphi[1] - axphi[end]) that the mirror branch at
iphi == 1 already uses. This corrupted e_phi on the last phi slice of
every cylindrical field calculation; invisible for phi-symmetric
detectors (e_phi = 0), wrong for segmented ones.

Also: default use_nthreads to nthreads() instead of threadid(), keep
the NaN/Inf guard type-stable for Float32, and store SVectors directly
instead of allocating a temporary Vector per grid point.

Add an analytic uniform-field regression test covering the wrap column.

Created by generative AI.
- ADL2016 getVe: honor the Emag_threshold argument; zero field returned
  NaN instead of zero velocity (2006 model already guarded).
- ADL getVh: clamp the acos argument; rounding could push it outside
  [-1, 1] for fields along the rotated crystal z-axis.
- PtypePNJunctionImpurityDensity: offset corrections were added to both
  sub-models, shifting the summed density by twice the offset.
- cluster_detector_hits: detectors with 2-3 hits skipped DBSCAN entirely
  and were never clustered.
- Floating-boundary drift handling: compute each crossing from the
  unscaled step vectors and apply one common slow-down factor instead of
  rescaling mid-loop, which corrupted crossings of subsequent charges
  and compounded the scaling.
- simulate_waveforms: accept unitless edep columns (as documented) and
  plain-Real time steps (dt.val crashed on Real).
- readsiggen: missing taper_length keys no longer raise KeyError.
- parse_config_file: mention .config in the unsupported-format error.

Created by generative AI.
- Box: config check tested hZ twice and never hY; missing entries now
  raise ConfigFileError instead of KeyError or falling through.
- RegularPrism: the height assert accepted 'z' but only 'h' was parsed.
- Cone/Torus Dictionary: serializing a full-phi primitive carrying a
  pure-Z rotation crashed on phi0 + nothing; keep the rotation entry.
- Polycone _in: remove dead partial-phi branch referencing an undefined
  variable (Polycone is full-phi by construction).
- TorusMantle: phi-partial mantles now raise a clear ArgumentError from
  distance_to_surface instead of MethodError; drop the dead guard.
- ConeMantle intersection: single-intersection branch lost the sign of
  D1 (used sqrt(D3^2 S^2 - D2^2) = |D1|); use the -D1*L1 quadratic
  limit.
- frame_transformation(a, b): the two half-transforms were composed in
  the wrong order, wrong for any distinct frames.
- CartesianPoint{T}(v::AbstractVector) ignored the requested precision.
- _in_phi: avoid Float64 promotion in the Float32 hot path.

Created by generative AI.
- searchsortednearest on periodic axes now also snaps across the seam
  (a phi just below the period snapped to the last tick instead of the
  first); find_closest_gridpoint passes the axis so the periodic method
  is actually used.
- check_grid: periodic Cartesian y/z axes require an even tick count for
  red-black parity, mirroring the cylindrical phi guard.
- multiplicity: add the missing method for periodic Cartesian axes
  (capacitance calculations raised MethodError on periodic grids).
- even_tick_axis no longer mutates and aliases the input axis ticks.
- r=0 volume weight: replace a Float32 literal of 1/(2pi) with
  inv(T(2pi)) for full precision in Float64 setups.

Created by generative AI.
- Correct parameter order in the RegularPrism docstring and typos in
  TorusMantle/EllipticalSurface annotations.
- intersection docstrings for cone/ellipsoid/torus mantles claimed NaN
  returns for non-intersecting lines; describe the actual behavior.
- Fix broken code examples of get_electron_and_hole_contribution and
  plot_electron_and_hole_contribution (package name, variable names)
  and of PtypePNJunctionImpurityDensity in the manual (missing
  doped-contact-id argument).
- min_tick_distance docstrings described defaults the code no longer
  uses.
- Lithium-diffusion parameter docstrings: render the formulas correctly
  (raw strings; the plain strings mangled the LaTeX via string escapes).
- Reword misleading temperature-scaling comments (anisotropy is
  preserved, not dropped).
- CartesianVector docstring example used CartesianPoint.

Created by generative AI.
- CGD stored-energy check used an atol larger than the expected value
  (any result in [0, 1.4e-5] passed, including zero); use rtol.
- Two-step weighting-potential refinement compared only array sizes;
  also compare grids and values.
- test_gpu_backend.jl relied on T leaking from a previously included
  test file; define it locally.
- Load test config files via @__DIR__ instead of cwd-relative paths.
- Replace duplicated copy-paste assertions in the partial-tube tests
  with distinct points on the φ-boundaries of the wedge.

Created by generative AI.
The periodic wrap for the compressed dimension (Cartesian x, and the
cylindrical z-axis routed through the same method) copied ghost rows
into ghost rows: row 1 read row end (a ghost) and row end read the
just-overwritten row 1. The update loop therefore never received
wrapped interior values. In compressed indexing real index 1 sits at
row 2 and real index N at row size-1, so the wrap is ghost[1] <-
interior[end-1] and ghost[end] <- interior[2], matching all other
methods of this file. Add a red-black layout unit test pinning this.

Also drop unused red-black helpers (AbstractRBArray, RBEven/RBOdd,
rb_bool_*, the non-extended RBArray constructors).

Created by generative AI.
- table_utils: drop unused remove_column, rename_fields/rename_cols,
  sort_by_time, cluster_by_time, add_physics_event_numbers and
  group_by_evt_number (no callers anywhere).
- ChargeDrift: drop the unused DriftPath struct and the bogus 500 m
  default diffusion length of the internal diffusion kick (the only
  caller always passes it).
- Depletion: delete ~200 lines of commented-out superseded code.
- calculate_weighting_potential!: remove the silently-ignored
  n_points_in_phi keyword; passing it now fails loudly.
- PointTypes: an inner constructor now suppresses the auto-generated
  outer constructor that the convenience constructor overwrote (future
  Julia versions reject such overwrites during precompilation).

Created by generative AI.
Many source files carried CRLF (or, after recent edits, mixed) line
endings. Purely mechanical change.

Created by generative AI.
Verified against Geant4 itself (fresh-process GDML readback; placement
rotation matrices, solid volumes, and 200-300-point random in/out sweeps
in the world frame agree with the SSD geometry for rotated, boolean and
polyhedral test cases plus the shipped examples):

- physvol placements never emitted rotations, so rotated primitives
  (e.g. partial-phi segments, which store their start angle as a
  rotation) collapsed onto phi = 0. GDML placement rotations are
  passive; write RotZYX angles of the transposed rotation.
- GDML polyhedra radii are apothems; SSD RegularPrism.r is the vertex
  distance. Hexagonal prisms exported 4/3 too large in volume.
- Boolean solids: the second-operand offset was only emitted when the
  first solid's own origin was nonzero (!) and was expressed in the
  world frame; the rotation used X-Y-Z angles where the reader composes
  Rz Ry Rx and applies them actively, rotating the translation as well.

Also add a generic get_label_name fallback and fix two sample-surface
plot recipes referencing an undefined label variable.

Created by generative AI.
The example originally named its phi axis 'y' (ignored, so the full-2pi
default applied); a later rename to 'phi' kept 'from: 0, to: 0', which
means 2D phi-symmetric - wrong for a cuboid detector. Restore a full
phi range.

New tests: CGD on cylindrical vs Cartesian grid (active-volume
agreement, would have caught this), get_2pi_potential periodic and
reflecting extensions against analytic phi-dependence, NBodyChargeCloud
energy conservation, and numeric velocities of the simple drift models.

Created by generative AI.
- Drop the host-side backend synchronization between the red/black
  kernels and the boundary-condition updates on GPU backends: stream
  ordering already guarantees correctness, and the synchronizations
  stalled the queue twice per iteration. The task-based KA CPU backend
  keeps its explicit synchronization.
- Replace the r = 0 azimuthal-average boundary condition on GPU arrays
  (getindex + CUBLAS gemv + broadcast per color, allocating temporaries
  and forcing host round trips every iteration) with one small kernel.

Per-iteration profile of a 3.4M-point cylindrical grid on an A100:
host-to-device copies 8011 -> 11, pool allocations 12016 -> 16 per
1000 iterations; the update loop is now bound by the SOR kernels
themselves (~2.6e9 point updates/s, up from ~1.0e9 end-to-end before).
CPU and GPU results agree to convergence tolerance (verified with and
without depletion handling).

Created by generative AI.
…odel

getVe recomputed sqrt of the four constant reciprocal mass tensors via
generic (allocating) matrix square roots on every call and summed the
scattering weights inside the valley loop. Store sqrtgamma in the model
(a 6-argument convenience constructor keeps all construction sites and
the temperature-scaling path unchanged) and hoist the sum.

getVe: 20.8 us -> 0.55 us per call (38x) with identical results.

Created by generative AI.
On GPU backends the ghost-face updates ran as ~10 view broadcasts per
red-black color; together with per-call dynamic launch configuration
this left the iteration loop host-bound. Encode each face update as
(active, source row, factor) - a 1:1 transcription of the existing
broadcast methods - and apply all six faces in a single kernel launch
after the r = 0 kernel. Instantiate the SOR kernels with a static
workgroup size, which removes the per-launch configuration cost.

The iteration loop of a 3.4M-point cylindrical grid on an A100 drops
from ~3.4 ms to ~0.37 ms per iteration (~9x) and is now bound by the
device time of the SOR kernels. Verified against the CPU backend for
cylindrical grids (2D, full-2pi periodic with r = 0, reflecting
phi-wedge) and Cartesian grids, with and without depletion handling;
Cartesian and 2D results are bit-identical, 3D cylindrical agrees to
convergence tolerance. The multi-ghost corner entries the fused faces
may write concurrently are never read by the SOR stencil.

Created by generative AI.
Reverts the behavioral part of 'Fix r=0 neighbor masking in SOR
depletion handling' and documents what the original code actually does:
at r = 0 the sub-axis ghost entry (always 0) acts as a potential floor
in the extremum check of handle_depletion. This anchors the axial
channel - without it, the overshoot clamp settles into a stable
spurious undepleted state there (flat channels pin each point to its
lower z-neighbor; the inverted-coax example then reports undepleted
even at 1.05x its depletion voltage, while exact superposition analysis
shows no bulk extremum above the estimate) - while still flagging axis
points that the impurity term would push below 0 V.

The floor is only exact while the grounded contact sits at 0 V; a
proper generalization should use the extrema of the applied potentials
(noted in the review report), and the clamp bistability in flat regions
deserves its own look.

Created by generative AI.
Whether a cylindrical simulation runs in 2D (or on a reduced phi wedge)
was decided purely by the grid's phi interval; nothing verified that the
detector actually has the corresponding symmetry, so a config mistake
silently produced wrong fields (as happened to the CGD_CylGrid example
for years). Grid construction now runs a sampled phi-invariance check of
all field-relevant objects and raises a ConfigFileError with a clear
message for 2D grids of non-symmetric detectors and for periodic wedges
without the matching n-fold periodicity. One mismatching sample is
tolerated for robustness against points on object surfaces; reflecting
wedges (mirror symmetry) are not yet checked.

Created by generative AI.
Add conservative bounding spheres to the CSG module (primitive origin
plus slightly inflated extremum; union merges, difference keeps the
minuend's sphere, intersection the smaller one) and use them in the
potential-calculation setup loops to skip the expensive CSG tree walk
for grid points that are provably outside an object.

Point classification on a 3.4M-point BEGe grid drops from 2.2 s to
1.1 s; resulting potentials, point types and impurity scales are
bit-identical. Conservativeness (pt in geometry implies pt in sphere)
is tested against sampled surface points of all example geometries.

Created by generative AI.
Add JLArray-based testsets: as a real AbstractGPUArray on CPU memory,
JLArray runs the fused boundary-condition kernels, the r0 kernel and the
device convergence checks that the abstract pseudoGPUArray type never
reached (Adapt leaves its arrays as plain Arrays). Cylindrical and
Cartesian solves reproduce the CPU results bit-identically.

Add test_boundary_conditions.jl pinning every ghost-row method exactly
per axis role and boundary-type combination, the equivalence of the CPU
broadcast methods with the GPU ghost-face op tables, and the r0 kernel
against its CPU reference. The r-axis end-2 source row is pinned as
current behavior (issue #617).

Also cover temperature-model selection with the vacuum no-op scaling,
virtual drift volumes (dead and arbitrary, including the Val-id user
hook and its error path), and the geometrical axis weights interface.

Created by generative AI.
is_depleted now throws an ArgumentError when the electric potential was
calculated without depletion handling instead of warning and returning
true; the depletion state is simply unknown then, and the previous
behavior silently disabled the safety check in
estimate_depletion_voltage.

The charge-cloud-center check in move_charges_inside_semiconductor! now
throws an ArgumentError instead of an assert, making the failure a
documented, catchable part of the API (asserts may be compiled out and
are not meant for input validation).

Created by generative AI.
Bare numbers in spacing_surface_refinement were interpreted as meters
regardless of units.length, so mm-based configs got a silently 1000x
different surface refinement. Parse them like every other length in the
config; values with explicit unit strings are unaffected. Breaking for
configs that relied on the implicit meters.

Created by generative AI.
Cones and regular prisms accept an explicit extent along their local z
axis as an alternative to the centered height h; the parsed interval
sets the half-height and shifts the primitive origin along its rotated
z axis. The previously dead parse_height_of_primitive helper now
implements this uniformly and rejects ambiguous inputs (both h and z,
neither, h as an interval, or z as a scalar) with a ConfigFileError.

Created by generative AI.
The bounding-sphere culling already needs the Cartesian form of each
grid point; pass it to the containment test as well instead of letting
every visited primitive re-convert the CylindricalPoint (the remaining
unchecked point of issue #85). Results are bit-identical by
construction. The measured effect after culling is small (~2% setup
time on a 1.1M-point BEGe grid) since culled points skip the tree walk
entirely; it mainly benefits many-object detectors where points survive
several objects' sphere tests.

Created by generative AI.
Verified against the sources: the hole formulas match Dai et al. (2023)
eqs. 5-8 after cm-to-SI conversion, with the seemingly odd neutral-
impurity expression being Sclar's form with light+heavy hole Erginsoy
constants; it collapses to 4.455e21/Nn * (T^0.5 + 4.281*T^-0.5), i.e.
4.46e19 in the paper's cm-units, so the paper's printed 4.46e29 is an
exponent typo, not a bug in this code. The electron formulas match
Ma et al. (arXiv:1705.09562) exactly. Add source comments and a numeric
regression testset pinning both mobilities and the drift directions.

Created by generative AI.
Note in the material table that only Pb carries a conductor-like eps_r:
floating Al/Cu passives act as dielectrics, not conductors (grounded or
biased metal parts are unaffected).

Bring the stale test/benchmarks scripts back to the current APIs:
replace the retired CUDAKernels imports with CUDA, drop the removed
KernelAbstractions.get_device import, fix plot_benchmarks loading the
potential-calculation results under the wrong JLD2 key, and in the SOR
update benchmark stop shadowing the hardware-backend loop variable with
the KA backend and derive via_KernelAbstractions from the device array
type instead of hardcoding false (the GPU row measured the wrong path).
Loop body validated on CPU and CUDA; the full scripts remain operator
tools with multi-hour runtimes and were not re-run end to end.

Created by generative AI.
@fhagemann

fhagemann commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Tests seemed to have failed because some values of spacing_surface_refinement were parsed in finer units (mm instead of m), resulting in huge grids and out-of-memory errors.

Comment thread src/World/World.jl
@fhagemann fhagemann added bug Something isn't working enhancement Improvement of existing features performance Performance improvements notation Notation and Interface convenience Improve user-friendliness tests Add tests to increase code coverage labels Jul 21, 2026
Comment thread src/Simulation/Simulation.jl

@fhagemann fhagemann left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went through this PR commit by commit, commented on some things and flagged some things to cross-check for later..

Comment thread src/ConstructiveSolidGeometry/SurfacePrimitives/ConeMantle.jl
Comment thread src/ConstructiveSolidGeometry/SurfacePrimitives/ConeMantle.jl
Comment thread src/ConstructiveSolidGeometry/SurfacePrimitives/EllipsoidMantle.jl
Comment thread src/ConstructiveSolidGeometry/SurfacePrimitives/EllipsoidMantle.jl
Comment on lines +239 to +243
# Cylindrical red-black layout is (z, φ, r): at r = 0 (i == 1) the
# radially-inward neighbor np[5] is a ghost entry no boundary condition
# ever writes; replace it by the outward neighbor so the stale value
# cannot skew the extremum check of handle_depletion.
return (np[1], np[2], np[3], np[4], ifelse(i == 1, np[6], np[5]), np[6])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also looks correct, but it seems to change pinch-off.
We should thoroughly test if this change agrees better/worse with the result obtained when running the simulation in Cartesian coordinates.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope of this one: the literal was the Float32 value of 1/2π used in the volume-weight computation even when T == Float64 — Float32 simulations are bit-identical before/after, and Float64 cylindrical grids get a ~1e-8-relative correction on the r = 0 volume weights only. Agreed that a cylindrical-vs-Cartesian pinch-off comparison is the right validation for the r = 0 machinery as a whole (together with #618); happy to set that up as a follow-up.

Comment thread test/test_gpu_backend.jl
Comment on lines +6 to +14
using JLArrays

T = Float32

# pseudoGPUArray flips `via_KernelAbstractions` so the SOR kernel runs through
# KernelAbstractions, but as an abstract type it survives Adapt unchanged: the
# setup arrays stay `Array`s and none of the `AbstractGPUArray`-dispatched code
# (fused boundary-condition kernels, r0 kernel, device convergence checks) runs.
# The JLArray testsets below cover those paths with a real GPUArray on CPU memory.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is adding JLArrays for the tests for CPU/GPU comparison.
@oschulz: can this be reverted once KernelAbstractions@0.10 is released (#497) ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not KA-related, unfortunately: JLArrays is here as a real AbstractGPUArray storage type, so that Adapt.adapt actually converts the setup arrays and the <:GPUArrays.AbstractGPUArray-dispatched methods (fused boundary-condition kernels, r0 kernel, device convergence checks) run at all. pseudoGPUArray, being an abstract type, survives Adapt unchanged — the arrays stay plain Arrays and those code paths are never entered, which is why they showed 0% coverage. KernelAbstractions 0.10 changes how kernels execute, not how storage dispatches, so this stays useful until CI has real GPUs (then it could go).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any other way testing this without introducing a new dependency?

Comment thread src/ScalarPotentials/PointTypes.jl Outdated
Comment on lines +129 to +134
else
throw(ArgumentError("""
The electric potential was not calculated with depletion handling enabled, so the depletion state is unknown.
Run the electric potential calculation with `depletion_handling = true` first.
When loading simulations from <v0.11.1, the depletion handling state is ambiguous if no `undepleted_bit` was set,
in this case use `set_point_type_depletion_handling!(sim, true)` as needed."""))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing an error in is_depleted would be a breaking change for me.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted in b2a80ddis_depleted warns and returns true again for potentials without depletion handling. The strictness moved to where it protects something: estimate_depletion_voltage with check_for_depletion = true now asserts has_depletion_handling itself (opt-out via check_for_depletion = false), so its safety check can no longer be silently disabled by the unknown state — without any change to is_depleted's public contract.

Comment thread src/World/World.jl

spacing_surface_refinement = if haskey(dict, "spacing_surface_refinement")
ntuple(i -> _parse_value(T, dict["spacing_surface_refinement"][i], internal_length_unit), 3)
ntuple(i -> _parse_value(T, dict["spacing_surface_refinement"][i], input_units.length), 3)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claudiaalvgar This would require spacing_surface_refinement to either be defined with units or follow the units given in units.length in the config file.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Factual context for the decision: yes — bare numbers now follow units.length from the config, consistent with every other length in the file (previously they were silently interpreted as metres, which for a mm-based config inflated the refinement spacing by 10³). Values with explicit units keep working via _parse_value. It is the breaking-but-consistent interpretation, so it should be called out in the release notes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think this would be a good change since it avoids potential unit problems

Comment thread docs/src/man/csg.md Outdated
Comment thread src/ConstructiveSolidGeometry/IO.jl
oschulz added 3 commits July 21, 2026 21:48
Restore remove_column, rename_fields/rename_cols, sort_by_time,
cluster_by_time, add_physics_event_numbers and group_by_evt_number in
table_utils.jl (still in use downstream; their removal is deferred to
the next breaking release), and the commented-out old depletion-voltage
estimation methods in Depletion.jl as reference until the effect of the
r0 depletion handling on estimated and actual depletion voltages is
fully understood.

Created by generative AI.
is_depleted again warns and returns true when the potential lacks
depletion handling (throwing was breaking for existing users).
estimate_depletion_voltage now asserts has_depletion_handling itself
when check_for_depletion is set, so its safety check can no longer be
silently disabled by the unknown depletion state.

Created by generative AI.
- calculate_capacitance_matrix requires all weighting potentials up
  front instead of carrying Missing entries through the result.
- Grid(sim) gets a check_phi_symmetry escape hatch; the symmetry error
  messages point to it.
- Cartesian (:periodic, :periodic) axis multiplicity warns like the
  (:reflecting, :reflecting) case (it would be infinite in principle).
- Docstring examples use evt/sim naming again; the gamma-half variable
  is named sqrtgamma; the PointTypes inner-constructor comment actually
  explains the phantom type parameter; the z-extent docs mention tubes.

Created by generative AI.
@oschulz

oschulz commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Thanks for the thorough commit-by-commit review — all comments are answered inline; the requested changes landed in three commits:

  • 3ce0e5e — restored the table utilities (remove_column, rename_cols, sort_by_time, cluster_by_time, add_physics_event_numbers, group_by_evt_number) and the commented-out old depletion-voltage reference implementations.
  • b2a80ddis_depleted is non-breaking again (warn + true); estimate_depletion_voltage now asserts has_depletion_handling itself when check_for_depletion is set.
  • 0d1d581 — capacitance matrix per your suggestion (assert all weighting potentials, plain matrix), Grid(sim; check_φ_symmetry = false) escape hatch for the new symmetry guard, (:periodic, :periodic) multiplicity warns like (:reflecting, :reflecting), evt/sim docstring naming, sqrtγ variable name, clarified PointTypes constructor comment, tubes mentioned in the z-extent docs.

Of the "flagged for later" items, several are already pinned by tests on this branch (boundary-condition ghost-row semantics incl. the GPU op tables and the r0 kernel, the periodic-wrap red-black layout, φ-boundary containment) — details in the inline replies. The two genuinely open decisions remain #617 (r-axis end-2 vs end-1, missing z-axis combinations) and #618 (r0 depletion handling), plus your cylindrical-vs-Cartesian pinch-off cross-check suggestion, which I'm happy to set up.

oschulz added 4 commits July 21, 2026 23:29
The cylindrical E-field gradient and the interpolation helpers assumed the
φ axis starts at 0: the seam wrap distance used the interval's right
endpoint as the period, and the interpolators appended a knot at 2π. Both
are wrong for full-circle intervals like [-π, π) or [π/6, 13π/6), which
config files can produce via phi.from - the [-π, π] axes of the analytic
capacitor examples previously got a periodic interpolation period of 3π.
Use the interval width and ticks[1] + 2π instead, skip the wrap knot when
the ticks already contain it, and add a shifted-interval regression test.

Created by generative AI.
The 2D branch only sampled rotations by multiples of π/4, so any C8-
symmetric shape (e.g. an 8-pointed star of two rotated boxes) passed as
axisymmetric. Add two shifts incommensurate with 2π to reject shapes with
merely discrete rotational symmetry. Also compare the impurity/charge
density models of semiconductor and passives at the shifted sample points,
since a φ-dependent density invalidates a reduced grid just like an
asymmetric geometry. Tests cover both new rejection paths.

Created by generative AI.
When several charge clouds crossed surfaces in the same drift step, each
floating-boundary cloud was committed at its own admissible step fraction
while Δt was rescaled by the minimum fraction, leaving cloud positions
ahead of the common timestamp. Split the handling into a scan pass that
determines each crossing cloud's admissible fraction and a commit pass
that moves all clouds consistently with the common minimal fraction.
Single-cloud boundary handling is unchanged.

Created by generative AI.
arXiv:1705.09562 is by H. Mei et al., not "Ma et al.", and the package
implements the Sclar variant of the neutral-impurity term as presented
there, not the paper's adopted model verbatim.

Created by generative AI.
@oschulz

oschulz commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

@oschulz commissioned an independent external review of this PR (an AI reviewer, reviewed and verified before acting on it). Three genuine defects it found in code this PR touches are now fixed on the branch, plus one attribution error:

  • 462c2c1 — φ period: the cylindrical E-field seam gradient used the φ interval's right endpoint as the angular period, and the interpolation helpers appended a hardcoded knot. Both are wrong for full-circle intervals not starting at 0 (phi: {from: -180, to: 180} parses fine). Notably, the [-π, π] axes of the analytic capacitor tests previously got a periodic interpolation period of in the capacitance integrators. Now the interval width / ticks[1] + 2π are used, with a shifted-interval regression test.
  • b109241 — 2D symmetry guard: the guard sampled only π/4-multiple rotations, so any C8-symmetric shape (test now includes an 8-pointed star of two rotated boxes) passed as axisymmetric, and it ignored φ-dependence of impurity/charge density models. It now also samples two rotations incommensurate with 2π and compares the density models at the shifted points; docs state its heuristic (sampled) nature.
  • e072c8c — charge-cloud/timestep sync: when several clouds crossed surfaces in the same step, each was committed at its own admissible fraction while Δt was rescaled by the minimum fraction, so positions ran ahead of the common timestamp. The handling is now scan-then-commit with the common fraction. Single-cloud behavior (the usual case, and everything the test suite exercised) is unchanged.
  • d2f5a2e: arXiv:1705.09562 is Mei et al. (not "Ma"), and the electron neutral-impurity term is the Sclar variant presented there — comment corrected.

Also from that review, verified but not fixed here: the interior E-field derivative uses an unweighted average of one-sided slopes, which is first-order inaccurate at grid-refinement transitions. That's pre-existing and physics-affecting, so it's filed separately as #625 rather than folded into this PR.

Test status: test_electric_field, test_config_robustness, test_charge_drift_models, test_diffusion, test_isochrone, test_real_detectors, test_analytic_solutions, test_depletion, test_platonic_solids all pass locally on the branch.

@oschulz

oschulz commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

The same external review raised four points that are decisions rather than clear-cut fixes, so asking here instead of acting on them (@fhagemann, @oschulz):

  1. is_depleted on unknown state: the review argues that "depletion handling was not run" should not map to true + warning, since unknown ≠ depleted — it suggests a tri-state / missing return, with the legacy behavior behind a compatibility keyword. This PR deliberately kept the non-breaking warn+true per the earlier review round (with strictness moved into estimate_depletion_voltage). Keep it as is, or is a breaking tri-state acceptable for the next minor release?

  2. Branch history around 6d576c13/f87dc4ee: the do-and-revert pair means one intermediate commit changes depletion behavior, which makes git bisect across this branch slightly hazardous. Options: leave the honest history (the two commits document each other, and Depletion handling: clamp bistability and the hidden 0 V floor #618 will supersede both), or squash the pair on merge. Preference?

  3. Charge-transport event stepping: e072c8c fixes cloud positions running ahead of the rescaled Δt, but stays within the current surface-drift heuristic. The review's fuller proposal — compute all proposed trajectories without mutation, advance every cloud (including ones headed for a contact) to the earliest event fraction, resolve only that event, repeat — would also make electrode-arrival times sub-step accurate, at the cost of a real behavioral change to multi-site waveforms. Worth an issue / future PR, or out of scope for the package's accuracy targets?

  4. GPU validation policy: the JLArrays tests added here exercise the generic-array dispatch paths but not device execution semantics (stream ordering, synchronization). Is there any prospect of a CUDA runner in CI (even scheduled-only), and should the CPU/GPU comparison tests state explicit atol/rtol instead of the default ? (For the record, the fused-kernel refactor gave bit-identical CPU/JLArray results locally.)

oschulz added 2 commits July 22, 2026 00:08
The cylindrical interpolation helpers closed the φ axis at ticks[1] + 2π
regardless of the axis range. For a symmetry-reduced periodic wedge the
repeat period is the interval width, so the wrap knot must sit at the
wedge end - otherwise evaluations between the last tick and the wedge end
(reachable e.g. through the capacitance integration over the closed wedge
grid) interpolate across one giant cell spanning the rest of the circle.
Non-periodic axes keep the full-circle closure, since a mirror-symmetric
extension is not a plain periodic repetition. Vector fields on reduced
grids remain meaningful only within their φ range (Cartesian components
do not transform trivially); noted in a comment.

Created by generative AI.
The commit pass placed each surface-drifting cloud at the common minimal
fraction but backed off individually if that position was blocked (non-
convex surfaces), leaving such a cloud behind the common timestamp. Now
the common fraction is shrunk iteratively until it is admissible for
every crossing cloud before anything is committed, so all cloud positions
correspond to the same time. Clouds collected on a contact keep their
crossing position; their arrival time remains accurate only to within one
Δt (full event stepping is tracked in the PR discussion).

Created by generative AI.
@oschulz

oschulz commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Second round of the external review is in; both residual defects it flagged were verified and are now fixed on the branch:

  • 72121f2 — wedge interpolation period: the interpolation helpers still closed the φ axis at for symmetry-reduced periodic wedges, whose repeat period is the interval width. This was reachable: the capacitance integration evaluates the weighting-potential interpolators over the closed wedge grid, so points between the last tick and the wedge end landed in one giant cell spanning the rest of the circle. Periodic axes now wrap at ticks[1] + width(interval); non-periodic (mirror-symmetric) axes keep the full-circle closure, since a mirror extension is not a plain periodic repetition — a correct mirror extension would be a separate improvement. Regression test with a 3-fold wedge added. Vector fields on reduced grids remain meaningful only inside their φ range (Cartesian components don't transform trivially under the reduced symmetry); this is now noted in a comment, and the drift path always uses full-circle fields.
  • 664a55c — common step fraction admissibility: in the previous fix, a surface-drifting cloud whose position at the common fraction was blocked (non-convex surface) was backed off individually, ending up behind the common timestamp. The common fraction is now shrunk iteratively until it is admissible for every crossing cloud before anything is committed. Also documented in-code: clouds collected on a contact keep their crossing position, so electrode arrival times are accurate only to within one Δt — the full event-stepping question remains open above (question 3).

The reviewer also retracted its earlier "keep 6d576c13 reverted" recommendation after seeing the corrected #618 ground truth, so both reviews now agree with the direction discussed there (adopt the r=0 neighbor fix, re-anchor the depletion tests, initialize from the converged linear solution — awaiting @fhagemann). The stale problem statement in #618's body now carries a prominent correction notice, and #625 got a sharper title ("at abrupt grid-spacing changes") plus additional suggested tests.

Test status: full affected set re-run and passing (electric_field incl. new wedge test, charge_drift_models, real_detectors, analytic_solutions, diffusion, isochrone).

z = zint.left + (iz - T(0.5)) / n_z * width(zint)
pt = CylindricalPoint{T}(r, φ, z)
ref = pt in obj
ref_ρ = has_density ? get_charge_density(obj, pt) : zero(T)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would restrict the density check to interior points, this preserves the intended symmetry test while avoiding unnecessary (and potentially invalid) get_charge_density calls outside the object. I think this would also fix one of the failing tests.

Suggested change
ref_ρ = has_density ? get_charge_density(obj, pt) : zero(T)
ref_ρ = has_density && ref ? get_charge_density(obj, pt) : zero(T)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine for me.
We might want to add some tolerance to ref = pt in obj to guard against rounding errors.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also looks like the tests are failing because of this (trying to evaluate SplineBouleImpurityDensity somewhere outside of the detector).

for Δφ in φ_shifts
spt = CylindricalPoint{T}(r, φ + Δφ, z)
if (spt in obj) != ref ||
(has_density && !isapprox(get_charge_density(obj, spt), ref_ρ, rtol = T(1e-3)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(has_density && !isapprox(get_charge_density(obj, spt), ref_ρ, rtol = T(1e-3)))
(has_density && ref && !isapprox(get_charge_density(obj, spt), ref_ρ, rtol = T(1e-3)))

Comment on lines +105 to +110
elseif haskey(ctm_dict, "model") && ctm_dict["model"] == "Boggs"
BoggsChargeTrappingModel{T}(ctm_dict, temperature)

elseif haskey(ctm_dict, "model") && !haskey(ctm_dict, "model_inactive") && ctm_dict["model"] == "ConstantLifetime"
elseif haskey(ctm_dict, "model") && ctm_dict["model"] == "ConstantLifetime"
ConstantLifetimeChargeTrappingModel{T}(ctm_dict)

elseif haskey(ctm_dict, "model")
throw(ConfigFileError("There is no charge trapping model called `$(ctm_dict["model"])`."))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the main change is that now with an unrecognized "model" and no "model_inactive" throws a ConfigFileError while before, the default code fell to NoChargeTrappingModel, trapping was just disabled automatically. The rest of the behaviour (discussed in PR #532 ) I think is unaffected, since those always have model_inactive present

Comment on lines +151 to +156
if doped_geometry_for_imp
config_detector["semiconductor"]["impurity_density"]["doped_contact_id"] = collect(imp_doped_contact_id)
end
if doped_geometry_for_drift
config_detector["semiconductor"]["charge_drift_model"]["surface_impurity_density"]["doped_contact_id"] = collect(drift_doped_contact_id)
end

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a change for when the user did not explicitly supply doped_contact_id in the sub-dict, so I think auto-inferring it is the right call. Maybe add a @warn when it falls back to this, so the user is informed

Comment thread src/World/World.jl

spacing_surface_refinement = if haskey(dict, "spacing_surface_refinement")
ntuple(i -> _parse_value(T, dict["spacing_surface_refinement"][i], internal_length_unit), 3)
ntuple(i -> _parse_value(T, dict["spacing_surface_refinement"][i], input_units.length), 3)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think this would be a good change since it avoids potential unit problems

if verbose @warn("Handling of charge at floating boundary did not work as intended. Start Position (Cart): $(startpos[n])") end
cd_point_type = CD_BULK # treated as internal error below
else
scale = min(scale, s)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scale here is the smallest admissible fraction from any floating-boundary cloud, but it's applied to every cloud in the batch below, even ones that never touched a boundary.
Δt would reset to its original value on the next call, but if the boundary cloud stays stuck near that corner for several steps in a row, unrelated clouds keep getting throttled too and may not finish within max_nsteps. Is that coupling intentional, or should normal clouds keep moving at full speed while only the crossing clouds get held back?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are arguments for both implementations:

  • slowing all charge clouds down: let's assume the 'second' charge cloud is close to the one at the floating boundary. Then, we might want to keep them on the same time scales to still know their position when it comes to calculating effects of self-repulsion.
  • I see the problem with running into max_nsteps, and this also slowing down the charge drift simulation unnecessarily.. Maybe we can avoid having this scale implementation in the first place and keep the original time step of Δt here?

Comment on lines +249 to +252
n_mismatch = 0
for obj in objects
has_density = obj isa Semiconductor || obj isa Passive
for ir in 1:n_r, iφ in 1:n_φ, iz in 1:n_z

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be like this :

Suggested change
n_mismatch = 0
for obj in objects
has_density = obj isa Semiconductor || obj isa Passive
for ir in 1:n_r, iφ in 1:n_φ, iz in 1:n_z
for obj in objects
has_density = obj isa Semiconductor || obj isa Passive
n_mismatch = 0 # tolerance per object
for ir in 1:n_r, iφ in 1:n_φ, iz in 1:n_z

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That depends if we allow for one mismatch in total for the whole detector, or per object.
Again, that's a choice that we have to make, ideally based on some experience running this function over presumably φ-invariant detectors.
I like your suggestion, because it might be more robust against rounding precision errors, that might affect multiple object of the SolidStateDetector. However, assume that a detector spans 3/4 of the φ-range, then sampling with n_φ might result in one mismatch PER object, and still pass with your suggestion.

Comment on lines +228 to +231
* `for_weighting_potential::Bool = false`: Grid will be optimized for the calculation of
an [`ElectricPotential`](@ref) if set to `true`, and of a [`WeightingPotential`](@ref)
if set to `false`.
* `check_φ_symmetry::Bool = true`: For cylindrical grids with an empty or reduced periodic

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* `for_weighting_potential::Bool = false`: Grid will be optimized for the calculation of
an [`ElectricPotential`](@ref) if set to `true`, and of a [`WeightingPotential`](@ref)
if set to `false`.
* `check_φ_symmetry::Bool = true`: For cylindrical grids with an empty or reduced periodic
* `for_weighting_potential::Bool = false`: Grid will be optimized for the calculation of
a [`WeightingPotential`](@ref) if set to `true`, and of an [`ElectricPotential`](@ref)
if set to `false`.
* `check_φ_symmetry::Bool = true`: For cylindrical grids with an empty or reduced periodic

world_Δs = width.(world.intervals)
world_Δr, world_Δφ, world_Δz = world_Δs
check_φ_symmetry && !for_weighting_potential && _check_φ_symmetry(det, world)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not only check_φ_symmetry && _check_φ_symmetry(det, world) ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is because we always simulate weighting potentials in full 2π, except for if its a 2D calculation.

if for_weighting_potential && world_Δφ > 0
world_φ_int = SSDInterval{T, :closed, :open, :periodic, :periodic}(0, 2π)
world_Δφ = width(world_φ_int)
else
world_φ_int = world.intervals[2]
end

so it doesn't matter if the simulation is φ-invariant on the "reduced" interval. For example, the :BEGe example config file is φ-symmetric over 120°, but the weighting potentials are not.

function calculate_capacitance_matrix(sim::Simulation{T}; consider_multiplicity::Bool = true) where {T}
@assert !ismissing(sim.ϵ_r) "The electric potential needs to be calculated first."
@assert !ismissing(sim.weighting_potentials) "The weighting_potentials needs to be calculated first."
@assert !ismissing(sim.weighting_potentials) && !any(ismissing, sim.weighting_potentials) "All weighting potentials need to be calculated first."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that can happen here is that reconverge_electric_potential=true can change the depletion state, and hence sim.ϵ_r but sim.weighting_potentials are never updated here. A later calculate_capacitance_matrix call will silently mix the new ϵ_r with stale weighting potentials, since it only checks !ismissing, not consistency. Is this something we want to take into account?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any suggestion on how this could be done? It's kind of tricky to know if ϵ_r has been updated or not.

if verbose && !ismissing(sim.electric_field)
@info "The bias voltage was adjusted and the electric potential was modified accordingly to reflect this change.\n" *
"Please run `calculate_electric_field!` to reflect this change also in the electric field."
end

We could add another @info to make the user aware of this?

if verbose && any(!ismissing, sim.weighting_potentials)
     @info "The bias voltage was adjusted and the electric potential was modified accordingly to reflect this change.\n" * 
           "Please run `calculate_weighting_potential!` to reflect this change also in the weighting potentials." 
 end 

@fhagemann

Copy link
Copy Markdown
Collaborator

Also, I don't understand the merge conflicts..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working convenience Improve user-friendliness enhancement Improvement of existing features notation Notation and Interface performance Performance improvements tests Add tests to increase code coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants