Diagnostics¶
The :mod:groundfield.diagnostics module provides pre-solve
structural diagnostics for a :class:World. It is the typical
counterpart to :mod:groundfield.validation (which is the
post-solve cross-engine consistency check).
Why pre-solve diagnostics?¶
For an production-grade TN-Ortsnetz with several hundred electrodes a mistake in the geometry — an electrode placed at the wrong coordinate, a missing conductor, or a segmentation budget that silently triples the wall-clock time — should be caught before kicking off a multi-minute solver run. The three helpers below each address one common failure mode:
| Helper | What it answers |
|---|---|
world_statistics |
"How big is this thing? Counts per kind, total wire length, footprint." |
expected_segments |
"How many point-source segments will the discretiser produce?" |
check_segment_resolution |
"Will the resolution be good enough? Will memory / time blow up?" |
Example — full pre-flight check¶
import groundfield as gf
from groundfield.generators import TnNetworkGenerator, TnNetworkConfig, PenConfig
cfg = TnNetworkConfig(
soil=gf.HomogeneousSoilSpec(resistivity=100.0),
building_counts={"residential": 30},
pen=PenConfig(segment_length_m=None),
)
world = TnNetworkGenerator(cfg, seed=42).build()
engine = gf.create_engine(backend="image", segment_length=0.5)
# 1. Structural snapshot (machine-readable; pretty-print as needed).
stats = gf.world_statistics(world)
print(f"electrodes: {stats['n_electrodes']} "
f"by kind: {stats['n_electrodes_by_kind']}")
print(f"footprint: {stats['footprint_area_m2']:.0f} m^2")
print(f"wire length: {stats['total_electrode_wire_length_m']:.1f} m "
f"(electrodes) + "
f"{stats['total_conductor_length_m']:.1f} m (conductors)")
# 2. Segmentation budget.
budget = gf.expected_segments(world, engine)
print(f"predicted total segments: {budget['total']} "
f"(electrodes {budget['electrode_total']}, "
f"conductors {budget['conductor_total']})")
# 3. Quality-of-discretisation warnings (empty list = all good).
for msg in gf.check_segment_resolution(world, engine):
print(f" WARN: {msg}")
expected_segments — exactness¶
The prediction is bit-exact for the image-family backends —
image, image_2layer, image_nlayer, mom,
mom_sommerfeld, cim and bem. It mirrors
:mod:groundfield.solver.image's discretiser conventions:
- rod: \(n = \max(1, \lceil L / \Delta s \rceil)\)
- ring: \(n = \max(8, \lceil 2 \pi r / \Delta s \rceil)\)
- strip: \(n = \max(1, \lceil L / \Delta s \rceil)\)
- mesh / grid_mesh: per-wire \(n = \max(1, \lceil d_\text{axis} / \Delta s \rceil)\), summed over both wire directions
- distributed conductor: \(n = \lceil L / \Delta s_c \rceil\)
with $\Delta s_c = $
conductor.discretize_segment_length
The FEM backend (:mod:groundfield.solver.fem) uses an
axisymmetric volume mesh that is not parameterised by
segment_length; the prediction is not informative for
FEM.
check_segment_resolution — heuristics¶
Three categories of warning are surfaced:
- Thin-wire ratio \(\Delta s / r_\text{wire} \ge 5\) on every electrode and on every distributed conductor. Below this, the thin-wire average-potential self-action becomes biased.
- Electrode smaller than one segment — the smallest geometric
dimension of an electrode (rod length, ring perimeter, strip
length, mesh wire length) must be at least one
segment_length; otherwise the discretiser falls back to its lower floor. - Total segment-count budget at a soft threshold (5 000 segments — solve time runs from seconds to minutes) and a hard threshold (20 000 segments — the dense-system \(O(N^2)\) memory and \(O(N^3)\) solve scaling becomes painful).
The function never raises — use the returned list to inform the user. An empty list means no concerns detected.
API reference¶
diagnostics ¶
Pre-solve world diagnostics — counts, mesh budget, resolution checks.
This module collects structural diagnostics that work directly
on a :class:~groundfield.world.World (and optionally an
:class:~groundfield.solver.engine.Engine), without invoking the
solver. It is the typical counterpart to
:mod:groundfield.validation (which is a post-solve cross-engine
check):
- :func:
world_statistics— aggregate counts and lengths, bounding box, footprint area, conductor-length statistics. - :func:
expected_segments— predicts the number of point-source segments that the image-family discretiser will produce per electrode kind, plus a total. Useful for budgeting wall-clock cost before kicking off a 200-EFH run. - :func:
check_segment_resolution— heuristic warnings about the discretisation quality (thin-wire ratio, electrode size vs. segment length, segment-count budget). Returns a list of human-readable strings, empty when everything looks healthy.
Validity envelope
- The segment counts in :func:
expected_segmentsmirror the conventions of :mod:groundfield.solver.image(the image-family discretiser used by theimage,image_2layer,image_nlayer,mom,mom_sommerfeld,cimandbembackends). FEM does not use this discretiser; for FEM the segment count is not predictive of cost. - Wire-length / segment computations follow the physical geometry. Sub-segment overheads from cluster-bonding, KCL pseudo-nodes and lumped-conductor branches are not double-counted.
References
- ADR-0003 (
docs/adr/0003-distributed-conductor-model.md) — distributed-conductor topology used by :func:expected_segmentsfor conductors with finitediscretize_segment_length.
HARD_LIMIT
module-attribute
¶
Total segment count above which the budget warning becomes urgent.
Promoted from the private _BUDGET_HARD_THRESHOLD for a stable
public handle.
MIN_THINWIRE_RATIO
module-attribute
¶
Minimum recommended ratio of segment_length to wire_radius.
Below this, the thin-wire approximation that underpins the
average-potential method becomes increasingly biased: the
self-action integral is computed under the assumption
segment_length >> wire_radius.
SOFT_LIMIT
module-attribute
¶
Total segment count above which the user should be aware of cost.
The dense Z-matrix scales as :math:O(N^2) in memory and the
solve as :math:O(N^3) for the LU + multi-port factorisation, so
~ 5 000 segments is roughly where solve time becomes minutes
rather than seconds on a typical laptop. Promoted from the
private _BUDGET_WARN_THRESHOLD so tests and notebooks have a
stable handle for the "soft warning" threshold.
check_segment_resolution ¶
Heuristic discretisation-quality check.
Inspects the world / engine pair for common typical modelling pitfalls and returns one human-readable string per finding. The empty list means no concerns detected. Categories:
- Thin-wire ratio. Each electrode must satisfy
segment_length >= MIN_THINWIRE_RATIO * wire_radiusso that the thin-wire average-potential self-action remains valid. - Electrode smaller than one segment. An electrode whose
smallest geometric dimension is below
segment_lengthis either degenerately discretised (1 segment) or, for rings, falls back to the discretiser'smax(8, ...)floor — in either case the user probably wants a finersegment_length. - Distributed-conductor ratio. A finite
discretize_segment_lengthmust also stay aboveMIN_THINWIRE_RATIO * conductor.wire_radius. - Total segment budget. Warns when the predicted total
crosses :data:
SOFT_LIMITand again at :data:HARD_LIMIT.
The function never raises — it only reports. Use the returned list to inform the user before kicking off a long solve.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
world
|
'World'
|
World to inspect. |
required |
engine
|
'Engine'
|
Engine carrying |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
Zero or more diagnostic strings. The list is sorted by severity (budget warnings last, electrode-specific warnings first). |
Source code in src/groundfield/diagnostics.py
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 | |
expected_segments ¶
Predict the number of point-source segments after discretisation.
Mirrors the image-family discretiser
(:mod:groundfield.solver.image) so the prediction is
exact for the image / image_2layer / image_nlayer
/ mom / mom_sommerfeld / cim / bem backends.
The FEM backend (:mod:groundfield.solver.fem) uses an
axisymmetric volume mesh that is unrelated to segment_length;
for FEM this prediction is not informative.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
world
|
'World'
|
World to inspect. |
required |
engine
|
'Engine'
|
Engine carrying |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Keys:
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/groundfield/diagnostics.py
world_statistics ¶
Return a structural snapshot of a world.
Aggregates counts, lengths and the bounding box; complements
:meth:World.summary (one-line text) with a richer
machine-readable dictionary that scales to production-grade networks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
world
|
'World'
|
World to inspect. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Keys:
|
Source code in src/groundfield/diagnostics.py
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 | |