Skip to content

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:

Type Description
BoundaryConditions

The updated boundary-conditions object.

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

summary() -> str

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}"
    )