Bug fixes, correctness guards, performance and test coverage from an in-depth review - #623
Bug fixes, correctness guards, performance and test coverage from an in-depth review#623oschulz wants to merge 39 commits into
Conversation
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.
|
Tests seemed to have failed because some values of |
fhagemann
left a comment
There was a problem hiding this comment.
I went through this PR commit by commit, commented on some things and flagged some things to cross-check for later..
| # 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]) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Is there any other way testing this without introducing a new dependency?
| 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.""")) |
There was a problem hiding this comment.
Throwing an error in is_depleted would be a breaking change for me.
There was a problem hiding this comment.
Reverted in b2a80dd — is_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.
|
|
||
| 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) |
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Yes, I think this would be a good change since it avoids potential unit problems
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.
|
Thanks for the thorough commit-by-commit review — all comments are answered inline; the requested changes landed in three commits:
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 |
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 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:
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: |
|
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):
|
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.
|
Second round of the external review is in; both residual defects it flagged were verified and are now fixed on the branch:
The reviewer also retracted its earlier "keep 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) |
There was a problem hiding this comment.
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.
| ref_ρ = has_density ? get_charge_density(obj, pt) : zero(T) | |
| ref_ρ = has_density && ref ? get_charge_density(obj, pt) : zero(T) |
There was a problem hiding this comment.
Fine for me.
We might want to add some tolerance to ref = pt in obj to guard against rounding errors.
There was a problem hiding this comment.
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))) |
There was a problem hiding this comment.
| (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))) |
| 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"])`.")) |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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
|
|
||
| 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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 thisscaleimplementation in the first place and keep the original time step of Δt here?
| 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 |
There was a problem hiding this comment.
Wouldn't this be like this :
| 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 |
There was a problem hiding this comment.
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.
| * `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 |
There was a problem hiding this comment.
| * `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) | ||
|
|
There was a problem hiding this comment.
Why is this not only check_φ_symmetry && _check_φ_symmetry(det, world) ?
There was a problem hiding this comment.
I think this is because we always simulate weighting potentials in full 2π, except for if its a 2D calculation.
SolidStateDetectors.jl/src/Simulation/Simulation.jl
Lines 325 to 330 in 664a55c
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." |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
SolidStateDetectors.jl/src/Simulation/Depletion.jl
Lines 387 to 390 in 5a0e7a7
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 |
Also, I don't understand the merge conflicts.. |
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
ConeMantle(≈74° off for SI-scale tapers) andEllipsoidMantle(consumed by drift reflections and E-field plots);ConeMantlesingle-intersection sign; two-frameframe_transformationcomposed in the wrong order; partial-φ serialization crash; several parsing holes now raiseConfigFileError.e_φon the last φ slice of every non-φ-symmetric map);searchsortednearestnever snapped across the periodic seam; missingmultiplicitymethod for periodic Cartesian axes; per-point allocations in the E-field builder.estimate_depletion_voltagecrash when both candidates lay in range; capacitance matrix could not store its documentedmissingentries; unknown trapping-model names silently disabled trapping.acosNaN;PtypePNJunctionImpurityDensity + offsetapplied the offset twice;cluster_detector_hitsskipped 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 (theCGD_CylGridexample silently simulated its cuboid in 2D).is_depletedthrows when the potential was computed without depletion handling (was warn +true, silently disabling theestimate_depletion_voltagesafety check).ArgumentErrorinstead of an assert.spacing_surface_refinementparses inunits.length(was silently meters — breaking for configs that relied on that).Performance (results bit-identical)
intree walk (Speed up initialization of boundary conditions onto the grid #85), plus a single cylindrical→Cartesian conversion per grid point.Features
z: {from, to}extents as an alternative to a centered heighth.Tests and documentation
AbstractGPUArraydispatch path was untested (thepseudoGPUArraytrick never reaches it) — new JLArray testsets run it end-to-end and reproduce the CPU results bit-identically on both grid types.InactiveLayerChargeDriftModelmobilities 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).test/benchmarksrefreshed to current APIs (CUDAKernels→CUDA; 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