World
The :class:groundfield.World class is the top-level container for
the entire physics of a grounding problem: soil model, electrodes,
conductors, sources, boundary conditions and (since 0.6.0) the
ADR-0012 concrete-shell registry. It is intentionally free of
numerics — the numerical evaluation (backend selection, mesh
resolution, frequency list, tolerances) is configured in a
Engine and applied to the world via
world.solve(engine).
Concrete-shell registry — cleanup contract (0.7.0)
The public :attr:World.concrete_shell_corrections dict accumulates
one entry per foundation electrode that was materialised with a
non-None concrete_rho_ohm_m field (ADR-0012 V1 "lumped"
path). Re-using a single World across multiple
TnNetworkGenerator.build calls — the canonical Monte-Carlo
pattern over the moisture distribution — would otherwise leak stale
shell resistances from earlier samples into later ones. The
explicit :meth:World.reset_concrete_corrections helper clears the
registry and returns a shallow copy of the dropped entries.
import groundfield as gf
world = gf.create_world(soil=gf.HomogeneousSoil(resistivity=100.0))
# ... populate via TnNetworkGenerator.build(world=world) ...
dropped = world.reset_concrete_corrections()
# ... rebuild with a different moisture sample ...
Calling the helper on a fresh world is an idempotent no-op.
API reference
world
Central World object: container for the entire physics.
A World bundles everything that belongs to the physical description
of a grounding system:
- a soil model (:class:
groundfield.soil.SoilModel),
- one or more electrodes (:class:
groundfield.geometry.Electrode),
- optional connection conductors
(:class:
groundfield.conductors.Conductor),
- one or more sources (:class:
groundfield.sources.Source),
- the boundary conditions
(:class:
groundfield.boundary.BoundaryConditions).
The World is intentionally free of numerics. The numerical
evaluation (backend selection, mesh resolution, frequency list,
tolerances) is configured in a :class:groundfield.solver.Engine and
applied to the world via world.solve(engine).
World
Bases: BaseModel
Top-level container for a grounding field problem.
Notes
A World is usually not instantiated directly but built via
:func:groundfield.create_world. The helper methods add_* and
the top-level factories gf.create_* populate the container
incrementally.
add_conductor
add_conductor(conductor: Conductor) -> Conductor
Add a conductor.
Source code in src/groundfield/world.py
| def add_conductor(self, conductor: Conductor) -> Conductor:
"""Add a conductor."""
if any(c.name == conductor.name for c in self.conductors):
raise ValueError(f"Conductor name '{conductor.name}' already taken.")
self.conductors.append(conductor)
return conductor
|
add_electrode
add_electrode(electrode: _ElectrodeBase) -> _ElectrodeBase
Add an electrode and check for unique name.
Source code in src/groundfield/world.py
| def add_electrode(self, electrode: _ElectrodeBase) -> _ElectrodeBase:
"""Add an electrode and check for unique name."""
if any(e.name == electrode.name for e in self.electrodes):
raise ValueError(f"Electrode name '{electrode.name}' already taken.")
self.electrodes.append(electrode)
return electrode
|
add_source
add_source(source: Source) -> Source
Add a source.
Source code in src/groundfield/world.py
| def add_source(self, source: Source) -> Source:
"""Add a source."""
if any(s.name == source.name for s in self.sources):
raise ValueError(f"Source name '{source.name}' already taken.")
self.sources.append(source)
return source
|
get_conductor
get_conductor(name: str) -> Conductor
Return a conductor by name.
Source code in src/groundfield/world.py
| def get_conductor(self, name: str) -> Conductor:
"""Return a conductor by name."""
for c in self.conductors:
if c.name == name:
return c
raise KeyError(f"Conductor '{name}' not found in world.")
|
get_electrode
get_electrode(name: str) -> Electrode
Return an electrode by name.
Source code in src/groundfield/world.py
| def get_electrode(self, name: str) -> Electrode:
"""Return an electrode by name."""
for e in self.electrodes:
if e.name == name:
return e
raise KeyError(f"Electrode '{name}' not found in world.")
|
reset_concrete_corrections
reset_concrete_corrections() -> dict[str, float]
Clear and return the ADR-0012 V1 concrete-shell registry.
The :attr:concrete_shell_corrections dict accumulates one entry
per foundation-electrode anchor that the generator pipeline
materialised with a non-None concrete_rho_ohm_m field
(see ADR-0012, V1 "lumped" path). Re-using a single :class:World
across several :meth:TnNetworkGenerator.build calls — the
canonical pattern for Monte-Carlo studies that flip
concrete_rho_ohm_m per realisation — would otherwise leak
stale shell resistances from earlier samples into later ones.
The helper is the explicit, opt-in counterpart of the "build,
solve, throw away the world" pattern: when the user does
want to keep the world but re-seed the corrections, calling
world.reset_concrete_corrections() immediately before
generator.build(world=...) (or before mutating the
generator config and re-building) produces a clean slate.
Returns:
| Type |
Description |
dict[str, float]
|
The previous contents of the registry, before clearing. The
returned dict is a shallow copy so the caller can inspect or
log the dropped entries without holding a live reference to
the (now empty) registry on the world.
|
Notes
Calling this on a world that was never touched by the
concrete-encasement code path is a no-op; the returned dict is
empty.
Source code in src/groundfield/world.py
| def reset_concrete_corrections(self) -> dict[str, float]:
"""Clear and return the ADR-0012 V1 concrete-shell registry.
The :attr:`concrete_shell_corrections` dict accumulates one entry
per foundation-electrode anchor that the generator pipeline
materialised with a non-``None`` ``concrete_rho_ohm_m`` field
(see ADR-0012, V1 "lumped" path). Re-using a single :class:`World`
across several :meth:`TnNetworkGenerator.build` calls — the
canonical pattern for Monte-Carlo studies that flip
``concrete_rho_ohm_m`` per realisation — would otherwise leak
stale shell resistances from earlier samples into later ones.
The helper is the explicit, opt-in counterpart of the "build,
solve, throw away the world" pattern: when the user *does*
want to keep the world but re-seed the corrections, calling
``world.reset_concrete_corrections()`` immediately before
``generator.build(world=...)`` (or before mutating the
generator config and re-building) produces a clean slate.
Returns
-------
dict[str, float]
The previous contents of the registry, before clearing. The
returned dict is a shallow copy so the caller can inspect or
log the dropped entries without holding a live reference to
the (now empty) registry on the world.
Notes
-----
Calling this on a world that was never touched by the
concrete-encasement code path is a no-op; the returned dict is
empty.
"""
previous = dict(self.concrete_shell_corrections)
self.concrete_shell_corrections.clear()
return previous
|
set_boundary_conditions
set_boundary_conditions(
**kwargs: Any,
) -> BoundaryConditions
Update individual fields of the boundary configuration.
Parameters:
| Name |
Type |
Description |
Default |
**kwargs
|
Any
|
Fields of :class:BoundaryConditions (e.g. far_field,
surface, reference_node).
|
{}
|
Returns:
Warns:
| Type |
Description |
UserWarning
|
If any provided value differs from the defaults consumed
by the v0.2.0 integral / image-charge backends
(far_field="dirichlet", surface="neumann",
reference_node=None). The non-default value is stored
on the model and round-trips through serialisation, but
no backend reads it. The fields are reserved for the
upcoming FEM backend; see
:class:groundfield.boundary.BoundaryConditions for the
full implementation-status note.
|
UserWarning
|
If a field is reverted from a previously-set non-default
value back to the default. The previous non-default value
was never consumed by any backend, so a silent revert
would suggest a change of behaviour that the user never
actually experienced. The revert warning makes that
visible.
|
Source code in src/groundfield/world.py
| def set_boundary_conditions(self, **kwargs: Any) -> BoundaryConditions:
"""Update individual fields of the boundary configuration.
Parameters
----------
**kwargs
Fields of :class:`BoundaryConditions` (e.g. ``far_field``,
``surface``, ``reference_node``).
Returns
-------
BoundaryConditions
The updated boundary-conditions object.
Warns
-----
UserWarning
If any provided value differs from the defaults consumed
by the v0.2.0 integral / image-charge backends
(``far_field="dirichlet"``, ``surface="neumann"``,
``reference_node=None``). The non-default value is stored
on the model and round-trips through serialisation, but
no backend reads it. The fields are reserved for the
upcoming FEM backend; see
:class:`groundfield.boundary.BoundaryConditions` for the
full implementation-status note.
UserWarning
If a field is reverted from a previously-set non-default
value back to the default. The previous non-default value
was never consumed by any backend, so a silent revert
would suggest a change of behaviour that the user never
actually experienced. The revert warning makes that
visible.
"""
# Snapshot the previous boundary state so we can detect both
# "non-default value set" and "non-default value reverted to
# default" transitions on the keys the caller touched.
previous = self.boundary.model_dump()
new = self.boundary.model_copy(update=kwargs)
# Force re-validation through a fresh model construction
self.boundary = BoundaryConditions(**new.model_dump())
# Warn if the caller asked for a value the v0.2.0 backends do
# not actually implement. We only warn on the keys the caller
# touched (so a no-op call after construction stays quiet).
non_default = {
k: v
for k, v in kwargs.items()
if k in _DEFAULT_BOUNDARY_VALUES
and v != _DEFAULT_BOUNDARY_VALUES[k]
}
if non_default:
warnings.warn(
"BoundaryConditions field(s) "
f"{sorted(non_default)} set to a non-default value, "
"but the v0.2.0 integral / image-charge backends ignore "
"this setting and report potentials relative to remote "
"earth (φ → 0 at infinity, Neumann at z = 0). The value "
"is preserved on the model for forward-compatibility "
"with the upcoming FEM backend. See "
"groundfield.boundary.BoundaryConditions for the full "
"implementation-status note.",
UserWarning,
stacklevel=2,
)
# Revert detection: a key the caller now sets back to the
# default *was* previously non-default. The previous value
# never reached any backend; warning the user closes that
# silent-no-op feedback gap.
reverted = {
k: previous[k]
for k, v in kwargs.items()
if k in _DEFAULT_BOUNDARY_VALUES
and v == _DEFAULT_BOUNDARY_VALUES[k]
and previous.get(k) != _DEFAULT_BOUNDARY_VALUES[k]
}
if reverted:
warnings.warn(
"BoundaryConditions field(s) "
f"{sorted(reverted)} reverted to the default value. The "
"previous non-default setting "
f"{reverted!r} was never consumed by the v0.2.0 integral "
"/ image-charge backends, so this revert does not change "
"any computed result. See "
"groundfield.boundary.BoundaryConditions for the full "
"implementation-status note.",
UserWarning,
stacklevel=2,
)
return self.boundary
|
solve
solve(
engine: "Engine", *, snapshot_sources: bool = True
) -> "FieldResult"
Run the simulation with the given Engine.
Delegates to :meth:Engine.solve, so users may write either
world.solve(engine) or engine.solve(world).
Parameters:
| Name |
Type |
Description |
Default |
engine
|
'Engine'
|
The :class:~groundfield.solver.engine.Engine instance that
drives the backend.
|
required
|
snapshot_sources
|
bool
|
If True (default), every :attr:sources entry is
deep-copied before the backend runs and restored on exit.
This defends against backends that mutate
Source.return_to in flight (see
:class:groundfield.generators.measurement.MeasurementSetupConfig.build).
Power users who have verified that their backend does not
mutate the source list (typical in long
:func:~groundfield.engines.compare_engines sweeps or
:func:~groundfield.engines.convergence_study runs) may set
snapshot_sources=False to skip the deep-copy cost.
|
True
|
Notes
The default snapshot_sources=True makes the contract
explicit: solving never rewrites the input world. The opt-out
is documented in docs/concepts.md ("Engine re-use across
World.solve calls").
Source code in src/groundfield/world.py
| def solve(
self,
engine: "Engine",
*,
snapshot_sources: bool = True,
) -> "FieldResult":
"""Run the simulation with the given ``Engine``.
Delegates to :meth:`Engine.solve`, so users may write either
``world.solve(engine)`` or ``engine.solve(world)``.
Parameters
----------
engine
The :class:`~groundfield.solver.engine.Engine` instance that
drives the backend.
snapshot_sources
If ``True`` (default), every :attr:`sources` entry is
deep-copied before the backend runs and restored on exit.
This defends against backends that mutate
``Source.return_to`` in flight (see
:class:`groundfield.generators.measurement.MeasurementSetupConfig.build`).
Power users who have verified that their backend does not
mutate the source list (typical in long
:func:`~groundfield.engines.compare_engines` sweeps or
:func:`~groundfield.engines.convergence_study` runs) may set
``snapshot_sources=False`` to skip the deep-copy cost.
Notes
-----
The default ``snapshot_sources=True`` makes the contract
explicit: solving never rewrites the input world. The opt-out
is documented in ``docs/concepts.md`` ("Engine re-use across
``World.solve`` calls").
"""
# Local import to avoid a circular dependency at module load.
from groundfield.solver.engine import Engine
if not isinstance(engine, Engine):
raise TypeError(
f"Expected an Engine, got {type(engine).__name__}. "
"Build one with gf.create_engine(backend='image')."
)
if not snapshot_sources:
# Caller has opted out — backends are now contractually
# responsible for not mutating ``self.sources``.
return engine.solve(self)
# Snapshot every source via Pydantic's deep-copy semantics so
# backends that mutate a source field in flight cannot leak
# the change back into the caller's world.
sources_snapshot = [s.model_copy(deep=True) for s in self.sources]
try:
return engine.solve(self)
finally:
self.sources = sources_snapshot
|
summary
Compact textual summary of the world.
Source code in src/groundfield/world.py
| def summary(self) -> str:
"""Compact textual summary of the world."""
soil_str = (
f"{self.soil.kind}" if self.soil is not None else "<no soil model>"
)
return (
f"World '{self.name}': soil={soil_str}, "
f"electrodes={len(self.electrodes)}, "
f"conductors={len(self.conductors)}, "
f"sources={len(self.sources)}, "
f"boundary.far_field={self.boundary.far_field}"
)
|