IO¶
The io subpackage is the boundary between groundfield and the
rest of the software family. It carries no numerical core; every
function here is a translator between an in-memory groundfield
artefact (a fit, a result) and an external representation (a JSON
file, a sister-project Pydantic model).
Mathematical and physical context¶
The reduction pipeline reads
The third step is owned by groundinsight (network-grade fault
analysis on the reduced equivalent model). The interface that closes
the loop is BusType.impedance_formula — a SymPy-compatible
string in the two free symbols f (frequency in Hz) and rho
(Bus.specific_earth_resistance in \(\Omega\,\mathrm{m}\)).
io.groundinsight produces exactly that string from either of the
two rho-f fits supported by groundfield:
- Standard form (
RhoFStandardFit) — the five-coefficient ansatz
$$ Z(\rho, f) \;=\; k_1 \rho \;+\; (k_2 + j k_3)\,f \;+\; (k_4 + j k_5)\,f\,\rho, $$
fitted across a \(\rho\)-sweep. Already in the canonical
(rho, f) symbol set.
- Vector fit (
VectorFitResult) — a Gustavsen/Semlyen rational \(Z(s)\) in the Laplace variable \(s = j\,2\pi f\) at fixed soil. The exporter performs the symbolic substitution
$$ s \;\longrightarrow\; j\,2\pi f $$
and renders the formula in \(f\) alone. The underlying soil
resistivity is recorded in the metadata block (rho_at_fit_Ohm_m)
but does not appear in the formula — a vector fit is by
construction bound to a specific soil.
Validity¶
- Frequency range \(f \le 1\,\mathrm{kHz}\) — both fit families
inherit the quasi-static assumption. The exported
BusTypeis meaningful only inside this band;groundinsightevaluates the formula for whatever frequencies are stored on itsNetwork, so the user is responsible for staying within the band. - Linearity — the underlying field model is solved on the \(\nabla \cdot (\sigma\,\nabla\Phi) = 0\) regime; saturation effects in electrodes (e.g. ionisation under impulse currents) are not modelled. Exports are therefore appropriate for protection, reduction-factor and EPR studies, not for transient surge/lightning models.
- Multi-port reduction — the current API exports a single
BusType, i.e. the driving-point impedance of one cluster.BranchType(mutual impedance) is out of scope for ADR-0008 and reserved for a follow-up.
Transport¶
Two equally supported paths, see ADR-0008 for the design rationale and the exact JSON schema (v1):
| Path | Function | groundinsight install required? |
|---|---|---|
| JSON file | save_bustype_json / load_bustype_json |
no |
| Python API | to_bustype / save_bustype_to_db |
yes (extras group [groundinsight]) |
| Hybrid | to_bustype_dict (JSON-ready dict; build & inspect) |
no |
CSV exports — groundfield.io.csv¶
Three convenience writers that turn a :class:FieldResult
(and optionally its companion :class:World) into
machine-readable, tool-agnostic CSV files. They wrap the existing
postprocess helpers — no new science here, just a clean
disk format for sharing typical results across notebooks,
spreadsheets, and downstream pipelines.
import groundfield as gf
# 1. Sample the potential along a path and dump (s, x, y, z, freq, phi).
gf.save_potential_path_csv(
result, "out/phi_radial.csv",
start=(1.0, 0.0, 0.0), direction=(1, 0, 0),
distance=30.0, n=200,
)
# 2. Dump the per-electrode summary.
gf.save_electrode_table_csv(result, "out/electrodes.csv", world=world)
# 3. Dump the per-cluster summary (members are flattened to a string).
gf.save_cluster_impedances_csv(result, "out/clusters.csv")
All writers use UTF-8, comma-separated, with a header row;
floating-point values are written at full precision (%.17g)
so files round-trip without loss of accuracy.
API reference — CSV¶
csv ¶
CSV exports for groundfield results.
This module provides three convenience writers that turn a
:class:FieldResult (and optionally its companion :class:World)
into machine-readable, tool-agnostic CSV files. They wrap the
existing postprocess helpers — no new science here, just a
clean disk format for sharing typical results across notebooks,
spreadsheets, and downstream pipelines.
Functions:
| Name | Description |
|---|---|
:func:`save_potential_path_csv` |
Sample :meth: |
:func:`save_electrode_table_csv` |
Wrap :func: |
:func:`save_cluster_impedances_csv` |
Wrap :func: |
Column-name convention
All complex-valued quantities follow the same <symbol>_re,
<symbol>_im, abs_<symbol> triple where symbol is the
physical symbol of the quantity:
- potential —
phi_re/phi_im/abs_phi; - current —
I_re/I_im/abs_I; - impedance —
Z_re/Z_im/abs_Z.
The three writers therefore deliberately use different magnitude
columns (abs_phi for the potential path, abs_I for the
electrode table, abs_Z for the cluster table). They are not
intended to be concat/merge-ed on the magnitude column
without an explicit rename; the column constants below
(:data:POTENTIAL_PATH_COLUMNS, etc.) lock the schema for
downstream consumers.
All writers use UTF-8, comma-separated, with a header row; floating-point values are written at full precision so the files round-trip without loss of accuracy.
save_cluster_impedances_csv ¶
save_cluster_impedances_csv(
result: "FieldResult",
path: str | Path,
*,
frequency_index: int = 0
) -> Path
Wrap :func:cluster_current_balance and write to CSV.
Note that the members column of
:func:cluster_current_balance carries Python lists; CSV is
a flat format, so the lists are joined into ';'-separated
strings before writing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
'FieldResult'
|
Solver output. |
required |
path
|
str | Path
|
Destination CSV path. |
required |
frequency_index
|
int
|
Index into :attr: |
0
|
Returns:
| Type | Description |
|---|---|
Path
|
|
Source code in src/groundfield/io/csv.py
save_electrode_table_csv ¶
save_electrode_table_csv(
result: "FieldResult",
path: str | Path,
*,
world: "World | None" = None,
frequency_index: int = 0
) -> Path
Wrap :func:electrode_current_table and write to CSV.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
'FieldResult'
|
Solver output. |
required |
path
|
str | Path
|
Destination CSV path. |
required |
world
|
'World | None'
|
Optional companion world; when given, the table includes
the |
None
|
frequency_index
|
int
|
Index into :attr: |
0
|
Returns:
| Type | Description |
|---|---|
Path
|
|
Source code in src/groundfield/io/csv.py
save_potential_path_csv ¶
save_potential_path_csv(
result: "FieldResult",
path: str | Path,
*,
start: tuple[float, float, float],
direction: tuple[float, float, float] = (1.0, 0.0, 0.0),
distance: float = 30.0,
n: int = 200,
frequency_indices: Sequence[int] | None = None
) -> Path
Sample the potential along a line and write to CSV.
Builds an evenly spaced set of n field points starting at
start and walking distance metres along the unit
vector of direction, evaluates
:meth:FieldResult.potential at every selected frequency
index, and writes the result to path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
'FieldResult'
|
Solver output. Must carry |
required |
path
|
str | Path
|
Destination CSV path. Parent directories are created automatically. |
required |
start
|
tuple[float, float, float]
|
Path start point |
required |
direction
|
tuple[float, float, float]
|
Direction vector. Will be normalised to unit length.
Default |
(1.0, 0.0, 0.0)
|
distance
|
float
|
Path length in metres. Default 30. |
30.0
|
n
|
int
|
Number of sample points along the path. Default 200. |
200
|
frequency_indices
|
Sequence[int] | None
|
Iterable of integer indices into
:attr: |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
The path the file was written to. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/groundfield/io/csv.py
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 | |
VTK exports — groundfield.io.vtk¶
Legacy ASCII VTK writers (no pyvista / vtk Python bindings
required). Two file flavours:
| Function | VTK dataset | Use case |
|---|---|---|
export_geometry_vtk |
POLYDATA |
inspect electrodes + conductors in ParaView |
export_field_vtk |
STRUCTURED_POINTS |
render the surface potential as a heatmap / iso |
gf.export_geometry_vtk(world, "out/world.vtk")
gf.export_field_vtk(
result, "out/phi_surface.vtk",
extent=(-30, 30, -20, 20), z=0.0, n=(200, 150),
)
The geometry export carries an integer role cell-data scalar
(0 = electrode, 1 = conductor) so colour-by-role works in
ParaView without further configuration. The field export
includes potential_re and potential_im so above-DC typical
studies remain visible.
API reference — VTK¶
vtk ¶
Legacy ASCII VTK exports for ParaView / VisIt / Mayavi.
This module writes two flavours of the legacy ASCII VTK file
format (.vtk), keeping the dependency surface zero — no
pyvista or vtk Python bindings are required:
- :func:
export_geometry_vtk— POLYDATA with the electrode wires (rod, ring, strip, mesh, grid_mesh) and the conductor line segments. Useful for "drag the world into ParaView and rotate it" inspections of large typical networks. - :func:
export_field_vtk— STRUCTURED_POINTS with the soil surface (or any horizontal slice) sampled on a regular(N_x, N_y)grid. The potential is exported as a single scalar field; ParaView's contour / colour-map filters take it from there.
Why legacy ASCII VTK?
The legacy format is the smallest common denominator: every VTK
reader since the 90s opens it, the on-disk layout is plain text
(diff-able, version-controllable), and the writer is ~30 lines
of pure-Python without any external library. For large research
runs prefer pyvista once you need binary I/O and unstructured
grids; for production-grade networks at notebook scale this format is
fast enough.
References
- Schroeder, W., Martin, K., Lorensen, B. (2006). The Visualization Toolkit, 4th ed., Kitware. Section 19.5 (Legacy file format).
- VTK File Formats reference: https://vtk.org/wp-content/uploads/2015/04/file-formats.pdf
export_field_vtk ¶
export_field_vtk(
result: "FieldResult",
path: str | Path,
*,
extent: tuple[float, float, float, float],
z: float = 0.0,
n: tuple[int, int] = (120, 120),
frequency_index: int = 0
) -> Path
Sample the potential on a horizontal grid and write a VTK file.
Evaluates :meth:FieldResult.potential on a regular
:math:N_x \times N_y grid in the plane :math:z = z_0 and
writes a DATASET STRUCTURED_POINTS (a.k.a. uniform grid)
with a single scalar field potential_re. The imaginary
part is included as a second scalar potential_im for
above-DC typical studies.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
'FieldResult'
|
Solver output. Must carry |
required |
path
|
str | Path
|
Destination |
required |
extent
|
tuple[float, float, float, float]
|
Plot extent |
required |
z
|
float
|
Slice depth in metres (default |
0.0
|
n
|
tuple[int, int]
|
Grid resolution |
(120, 120)
|
frequency_index
|
int
|
Index into :attr: |
0
|
Returns:
| Type | Description |
|---|---|
Path
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
On bad extent or non-positive grid sizes. |
Source code in src/groundfield/io/vtk.py
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 | |
export_geometry_vtk ¶
Export the world geometry as a legacy ASCII VTK PolyData file.
Writes electrodes (rods, rings, strips, mesh / grid_mesh
perimeters) and conductors as 3-D polylines. The on-disk
layout uses VTK's DATASET POLYDATA with a LINES block;
cell data carries an integer role field (0 = electrode,
1 = conductor) so colour-by-role works directly in ParaView.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
world
|
'World'
|
World to export. May be empty (the writer still produces a syntactically valid header-only file). |
required |
path
|
str | Path
|
Destination |
required |
Returns:
| Type | Description |
|---|---|
Path
|
The path the file was written to. |
Source code in src/groundfield/io/vtk.py
Example¶
JSON path (no groundinsight install needed)¶
import numpy as np
import groundfield as gf
from groundfield.postprocess.vector_fitting import rho_f_from_field_result
from groundfield.io.groundinsight import (
save_bustype_json, load_bustype_json, to_bustype_dict,
)
# 1. Run a field study and produce a vector fit.
result = ... # FieldResult from world.solve(engine)
fit = rho_f_from_field_result(result, electrode_name="g1", n_poles=3)
# 2. Export to a versioned JSON file.
save_bustype_json(
fit,
path="bus_type_substation.json",
name="SubstationBus",
description="Substation grounding grid (vector-fitted, typical ref).",
system_type="Substation",
voltage_level=20,
)
# 3. Load and inspect later (no groundinsight needed).
spec = load_bustype_json("bus_type_substation.json")
print(spec.name, spec.metadata["fit_method"])
Python API path (live groundinsight.BusType)¶
from groundfield.io.groundinsight import to_bustype, save_bustype_to_db
# Requires: pip install groundfield[groundinsight]
bus_type = to_bustype(
fit,
name="SubstationBus",
description="Substation grounding grid (vector-fitted, typical ref).",
system_type="Substation",
voltage_level=20,
)
# bus_type is a live groundinsight.models.core_models.BusType instance
# and can be wired straight into a Network:
import groundinsight as gi
net = gi.create_network(name="reference", frequencies=[50.0, 250.0])
gi.create_bus(name="bus_substation", type=bus_type, network=net,
specific_earth_resistance=100.0)
# Or persist to the groundinsight SQLite database in one call:
save_bustype_to_db(fit, db_path="grounding.db",
name="SubstationBus", system_type="Substation",
voltage_level=20)
evaluate_spec(spec, frequencies, rho) re-evaluates an exported
formula at arbitrary \((f, \rho)\) points without round-tripping through
groundinsight. Both
evaluate_spec and the companion diagnostics helper
fit_quality_summary(spec) are reachable directly as
groundfield.evaluate_spec / groundfield.fit_quality_summary
(top-level re-exports, listed in groundfield.__all__).
CSV writer column-name convention¶
The three CSV writers in groundfield.io.csv use a single naming
convention: every complex quantity is written as the
<symbol>_re / <symbol>_im / abs_<symbol> triple, where the
symbol is the physical letter of the quantity (phi for potentials,
I for currents, Z for impedances). The magnitude columns
therefore differ between writers by design — abs_phi for the
potential path, abs_I for the electrode table, abs_Z for the
cluster table — and merging two tables on the magnitude column
requires an explicit rename. The frozen column tuples
POTENTIAL_PATH_COLUMNS, ELECTRODE_TABLE_REQUIRED_COLUMNS and
CLUSTER_IMPEDANCE_REQUIRED_COLUMNS expose the schema for
regression testing.
Errors¶
groundfield.io.groundinsight.evaluate_spec validates the
:class:BusTypeSpec it receives before lambdifying the
impedance_formula. Any malformed input — non-BusTypeSpec
argument, empty formula, unparseable SymPy expression, unknown free
symbol — raises a typed :class:EvaluateSpecError
(subclass of :class:ValueError). Downstream groundinsight
consumers can catch the typed class instead of substring-matching
on str(exc). Existing except ValueError blocks keep
working unchanged.
from groundfield.io.groundinsight import (
EvaluateSpecError, evaluate_spec, load_bustype_json,
)
spec = load_bustype_json("my-bustype.json")
try:
Z = evaluate_spec(spec, frequencies=[50.0, 1000.0], rho=100.0)
except EvaluateSpecError as exc:
print("Spec rejected:", exc)
API reference¶
groundinsight ¶
Export of reduced rho-f fits to groundinsight.
This module is the canonical bridge between the field-grade
reference computation in groundfield and the reduced equivalent
network model in groundinsight.
Two equally supported transports are provided:
- JSON file — neutral, language-agnostic schema versioned via
schema_version. Produce with :func:to_bustype_dict/ :func:save_bustype_json; read back with :func:load_bustype_json. - Python API — :func:
to_bustypereturns a live :class:groundinsight.BusTypePydantic instance via a lazy import.groundinsightis therefore an optional dependency ofgroundfield(extras group[groundinsight]). The JSON path does not requiregroundinsightat all.
Mathematical background
groundinsight.BusType.impedance_formula is parsed with two free
symbols, f (frequency in Hz) and rho
(Bus.specific_earth_resistance), and the imaginary unit j.
Both fit families exported here are projected onto that symbol set:
- :class:
~groundfield.postprocess.rho_f_standard.RhoFStandardFitis already in the canonical(rho, f)form
$$ Z(\rho, f) \;=\; k_1\rho \;+\; (k_2 + j k_3)\,f \;+\; (k_4 + j k_5)\,f\,\rho. $$
- :class:
~groundfield.postprocess.vector_fitting.VectorFitResultis a rational function of the Laplace variable \(s = j\,2\pi f\). The export substitutes \(s\to j\,2\pi f\) symbolically so the resulting expression is infonly (independent ofrho); the underlying soil is recorded in the metadata block.
See also
docs/adr/0008-groundinsight-bridge.md — full design
rationale, JSON schema, and validation programme.
SCHEMA_VERSION
module-attribute
¶
Schema name and version of the JSON document produced by this module.
BusTypeSpec
dataclass
¶
BusTypeSpec(
name: str,
description: Optional[str],
system_type: str,
voltage_level: float,
impedance_formula: str,
samples: dict[str, list[float]],
metadata: dict[str, Any],
)
Neutral, in-memory representation of an exported BusType.
The spec is what :func:to_bustype_dict returns under the hood and
what :func:load_bustype_json reads back. It carries everything
necessary to either build a groundinsight.BusType (via
:func:to_bustype) or to write the on-disk JSON document.
Attributes:
| Name | Type | Description |
|---|---|---|
name, description, system_type, voltage_level |
The four scalar fields that the |
|
impedance_formula |
str
|
The fitted |
samples |
dict[str, list[float]]
|
Dict with the parallel tabular representation. Keys
|
metadata |
dict[str, Any]
|
Free-form metadata dict; populated by the exporters with
the fit method, fit quality, fit-method-specific details
(poles/residues for vector fits, \(k_1\dots k_5\) for the
standard form), the source |
from_dict
classmethod
¶
Build a :class:BusTypeSpec from a JSON-loaded dict.
Source code in src/groundfield/io/groundinsight.py
to_dict ¶
Return a JSON-ready dict matching schema v1.
Source code in src/groundfield/io/groundinsight.py
EvaluateSpecError ¶
Bases: ValueError
Raised when :func:evaluate_spec rejects a :class:BusTypeSpec.
Subclass of :class:ValueError so legacy except ValueError blocks
keep working unchanged; downstream consumers (notably the
groundinsight consumer of the bridge format) can now catch the
typed exception instead of matching against str(exc) substrings.
evaluate_spec ¶
Evaluate a :class:BusTypeSpec at arbitrary frequencies.
Re-evaluates the stored impedance_formula with the provided
rho and f values, using the same SymPy-based path
groundinsight.utils.impedance_calculator follows. Useful for
cheap sanity checks that do not require groundinsight to be
installed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
BusTypeSpec
|
Loaded spec (e.g. from :func: |
required |
frequencies
|
Sequence[float]
|
Frequencies in Hz. |
required |
rho
|
float
|
Soil resistivity in \(\Omega\,\mathrm{m}\). Ignored if the
formula does not depend on |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Complex-valued \(Z\) at every requested frequency. |
Raises:
| Type | Description |
|---|---|
EvaluateSpecError
|
Subclass of :class: |
Source code in src/groundfield/io/groundinsight.py
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 | |
fit_quality_summary ¶
One-line human-readable summary of fit quality / kind.
Source code in src/groundfield/io/groundinsight.py
load_bustype_json ¶
Load a BusType JSON document into a :class:BusTypeSpec.
Validates the schema name and version. Forward-compatibility for
future schema versions is the explicit responsibility of this
function: when schema_version differs from
:data:SCHEMA_VERSION, dispatch to the appropriate loader. Today
only v1 exists.
Source code in src/groundfield/io/groundinsight.py
save_bustype_json ¶
save_bustype_json(
fit: FitLike,
path: Union[str, Path],
*,
name: str,
system_type: str,
voltage_level: float,
description: Optional[str] = None,
decimals: int = 12,
rho_at_fit: Optional[float] = None,
electrode_name: Optional[str] = None,
soil_summary: Optional[str] = None,
indent: int = 2
) -> Path
Write the schema-v1 JSON file to disk.
Returns:
| Type | Description |
|---|---|
Path
|
The path the file was actually written to (as a
:class: |
Source code in src/groundfield/io/groundinsight.py
save_bustype_to_db ¶
save_bustype_to_db(
fit: FitLike,
*,
name: str,
system_type: str,
voltage_level: float,
description: Optional[str] = None,
decimals: int = 12,
rho_at_fit: Optional[float] = None,
electrode_name: Optional[str] = None,
soil_summary: Optional[str] = None,
overwrite: bool = False
) -> None
Convenience wrapper: build a BusType and save it to the
groundinsight SQLite store opened by gi.start_dbsession().
Requires groundinsight to be installed and a session to be
active. Raises ImportError if the package is missing and a
plain RuntimeError (from groundinsight) if no session
is open.
Source code in src/groundfield/io/groundinsight.py
to_bustype ¶
to_bustype(
fit: FitLike,
*,
name: str,
system_type: str,
voltage_level: float,
description: Optional[str] = None,
decimals: int = 12,
rho_at_fit: Optional[float] = None,
electrode_name: Optional[str] = None,
soil_summary: Optional[str] = None
)
Build a live :class:groundinsight.BusType from a fit.
Performs the same conversion as :func:to_bustype_dict, but
returns a fully-validated BusType Pydantic instance instead
of a JSON-ready dict. groundinsight is imported lazily;
a missing install raises an :class:ImportError with a clear
pointer to the optional install.
The metadata block is not carried over into the returned
BusType (the BusType schema has no metadata field) — use
:func:to_bustype_dict or :func:save_bustype_json if metadata
must be persisted.
Source code in src/groundfield/io/groundinsight.py
to_bustype_dict ¶
to_bustype_dict(
fit: FitLike,
*,
name: str,
system_type: str,
voltage_level: float,
description: Optional[str] = None,
decimals: int = 12,
rho_at_fit: Optional[float] = None,
electrode_name: Optional[str] = None,
soil_summary: Optional[str] = None
) -> dict[str, Any]
Convert a fit into the JSON-ready schema-v1 dict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fit
|
FitLike
|
A :class: |
required |
name
|
str
|
Match the four scalar fields of |
required |
system_type
|
str
|
Match the four scalar fields of |
required |
voltage_level
|
str
|
Match the four scalar fields of |
required |
description
|
str
|
Match the four scalar fields of |
required |
decimals
|
int
|
Number of significant digits to keep per coefficient when
rendering the SymPy formula. Default 12 — chosen so that the
round-trip through |
12
|
rho_at_fit
|
Optional[float]
|
Soil resistivity at which a :class: |
None
|
electrode_name
|
Optional[str]
|
Free-form metadata entries; recommended for traceability. |
None
|
soil_summary
|
Optional[str]
|
Free-form metadata entries; recommended for traceability. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
JSON-ready |