Validation (cross-engine)¶
The :mod:groundfield.validation module contains
:func:compare_engines, the cross-engine consistency check that
underpins ADR-0001. Given two :class:Engine instances and a
:class:World, it solves both and reports a structured
:class:EngineComparison summarising the agreement of
cluster impedances, electrode currents and surface potentials.
This is the post-solve counterpart of :mod:groundfield.diagnostics
(pre-solve structural checks).
Deviation metric (changed in 0.15.0)¶
For every cluster the report contains the worst pairwise relative deviation of the cluster impedance \(Z\) over the compared engines,
the second form holding whenever all \(Z_k\) have the same sign (the
normal case: \(\mathrm{Re}\{Z\} > 0\) for a passive grounding system).
rel_tolerance is compared against this number, so two engines that
differ by 5 % of the smaller value sit exactly on a
rel_tolerance=0.05 threshold. The same metric is used for the
optional sample_points potential comparison, per sample point.
Up to 0.14.x the deviation was measured against the ensemble mean,
\(\max_k |Z_k - \bar{Z}| / |\bar{Z}|\). For two engines that is exactly
half the true disagreement (and it shrinks further as engines are
added), so a rel_tolerance=0.05 gate silently admitted a spread of
10.5 %. Consequences of the change:
- reported deviations are roughly a factor 2 larger than in 0.14.x for the same world and engines — no physics changed, only the metric;
- a comparison that was marginally consistent under 0.14.x may now
legitimately be reported as inconsistent. Loosen
rel_toleranceonly after checking which engine is the outlier; - clusters for which at least one engine reports \(Z = 0\) have an undefined relative deviation and are now skipped with a note instead of being silently counted as agreeing.
Vacuous comparisons (changed in 0.15.0)¶
A cluster whose net injected current vanishes (\(\sum I = 0\): purely
passive observer electrodes) has no defined impedance and is skipped.
If every cluster is skipped and no sample_points potential has a
usable (non-zero) reference, nothing was validated — the typical causes
are a world without any source, or a Source.attached_to that does not
match any electrode or conductor name. Such a run now reports
is_consistent = False
notes: "No comparable quantity (every cluster impedance undefined, no
usable sample point) — comparison vacuous, nothing was
validated."
Up to 0.14.x the same situation returned is_consistent=True (the
running maximum deviation never left its initial 0.0), so a CI job
asserting report.is_consistent passed green having compared nothing.
Frequency-order behaviour¶
Since 0.5.0 :class:Engine.frequencies is order-preserving. A
non-monotonic frequency list raises a dedicated
:class:EngineFrequencyOrderWarning (subclass of
:class:UserWarning) so a single
warnings.simplefilter("once", EngineFrequencyOrderWarning) will
collapse a compare_engines 4 × 4 matrix to one emission. The
opt-in :meth:Engine.with_frequencies(*, preserve_order=True)
constructor silences the warning explicitly.
API reference¶
validation ¶
Cross-engine comparison for self-validation.
This module provides :func:compare_engines — a small convenience
helper that runs the same :class:World through several
:class:Engine configurations and checks the consistency of the
results. It implements ADR-0001 (docs/adr/0001-two-layer-method.md):
two engines side by side, validating each other.
Usage
import groundfield as gf world = gf.create_world(soil=gf.HomogeneousSoil(resistivity=100.0)) gf.create_electrode(world, "rod", name="g1", ... position=(0, 0, 0.0), length=1.5) gf.create_source(world, attached_to="g1", magnitude=1.0) report = gf.compare_engines( ... world, ... engines={ ... "image": gf.create_engine(backend="image", segment_length=0.05), ... "mom": gf.create_engine(backend="mom", segment_length=0.05), ... }, ... rel_tolerance=0.05, ... ) report.is_consistent True
At least two engines are required — a single-entry mapping raises
ValueError, because one engine cannot validate itself.
EngineComparison
dataclass
¶
EngineComparison(
results: dict[str, "FieldResult"],
rel_tolerance: float,
cluster_impedance_table: dict[
str, dict[str, float]
] = dict(),
deviations: dict[str, float] = dict(),
is_consistent: bool = False,
notes: list[str] = list(),
)
Outcome of a cross-engine comparison.
Attributes:
| Name | Type | Description |
|---|---|---|
results |
dict[str, 'FieldResult']
|
Mapping |
rel_tolerance |
float
|
Relative tolerance the results were checked against. |
cluster_impedance_table |
dict[str, dict[str, float]]
|
|
deviations |
dict[str, float]
|
|
is_consistent |
bool
|
|
notes |
list[str]
|
Diagnostic strings (e.g. "stub backend", "frequency lists do not match"). |
summary ¶
Return a line-oriented textual summary.
Source code in src/groundfield/validation.py
compare_engines ¶
compare_engines(
world: "World",
engines: dict[str, "Engine"],
*,
rel_tolerance: float = 0.05,
sample_points: np.ndarray | None = None
) -> EngineComparison
Run world through every engine and compare the results.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
world
|
'World'
|
World to evaluate. Not modified by this function. |
required |
engines
|
dict[str, 'Engine']
|
Mapping |
required |
rel_tolerance
|
float
|
Maximum allowed pairwise relative deviation of the cluster impedances (default 5 %), i.e. the gate is .. math:: over the engines :math: |
0.05
|
sample_points
|
ndarray | None
|
Optional. Array of shape |
None
|
Returns:
| Type | Description |
|---|---|
EngineComparison
|
Structured report (see :class: |
Notes
The check uses the real part of the cluster impedance at the
first frequency. Clusters with Σ I = 0 (purely passive
observers) have an undefined impedance and are skipped; the skip
is recorded in notes.
Metric definition (changed in 0.15.0). The deviation is the
full spread of the compared values normalised by the smallest
magnitude among them,
dev = (max(Z) - min(Z)) / min(|Z|), which is identical to the
worst pairwise relative deviation max_ij |Z_i - Z_j| / |Z_j|
for same-sign impedances. Up to 0.14.x the deviation was measured
against the ensemble mean
(max_k |Z_k - mean(Z)| / |mean(Z)|), which for two engines
reports exactly half the true disagreement and shrinks further as
engines are added — a rel_tolerance=0.05 gate then admitted a
10.5 % disagreement. Reported deviations are therefore roughly
twice as large as in 0.14.x for the same world; a comparison that
was marginally green may now legitimately turn red.
Vacuous comparisons (changed in 0.15.0). A report is
is_consistent=True only if at least one quantity was actually
evaluated — a cluster impedance, or a sample_points potential
with a non-zero reference. A world without sources (or with a
misspelled Source.attached_to) has no defined cluster impedance
anywhere, so nothing can be validated; such a run now yields
is_consistent=False plus a "comparison vacuous" note instead of
a false green (up to 0.14.x the running maximum deviation never left
its initial 0.0 and passed the tolerance gate).
Source code in src/groundfield/validation.py
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 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 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 | |