groundmeas 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
groundmeas/models.py ADDED
@@ -0,0 +1,215 @@
1
+ """
2
+ groundmeas.models
3
+ =================
4
+
5
+ Pydantic/SQLModel data models for earthing measurements.
6
+
7
+ Defines:
8
+ - Location: a measurement site with geographic coordinates.
9
+ - Measurement: a test event with metadata and related items.
10
+ - MeasurementItem: a measured data point (e.g. impedance, resistivity) with
11
+ magnitude and optional complex components.
12
+
13
+ Includes an SQLAlchemy event listener to ensure consistency between
14
+ value, value_real/value_imag, and value_angle_deg fields.
15
+ """
16
+
17
+ import logging
18
+ import math
19
+ import numpy as np
20
+ from datetime import datetime, timezone
21
+ from typing import Optional, List, Literal
22
+
23
+ from sqlalchemy import Column, String, event
24
+ from sqlmodel import SQLModel, Field, Relationship
25
+
26
+ from pydantic import field_validator
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ MeasurementType = Literal[
32
+ "prospective_touch_voltage",
33
+ "touch_voltage",
34
+ "earth_potential_rise",
35
+ "step_voltage",
36
+ "transferred_potential",
37
+ "earth_fault_current",
38
+ "earthing_current",
39
+ "shield_current",
40
+ "earthing_resistance",
41
+ "earthing_impedance",
42
+ "soil_resistivity",
43
+ ]
44
+
45
+ MethodType = Literal[
46
+ "staged_fault_test",
47
+ "injection_remote_substation",
48
+ "injection_earth_electrode",
49
+ ]
50
+
51
+ AssetType = Literal[
52
+ "substation",
53
+ "overhead_line_tower",
54
+ "cable",
55
+ "cable_cabinet",
56
+ "house",
57
+ "pole_mounted_transformer",
58
+ "mv_lv_earthing_system",
59
+ ]
60
+
61
+
62
+ class Location(SQLModel, table=True):
63
+ """
64
+ A geographic location where measurements are taken.
65
+
66
+ Attributes:
67
+ id: Auto-generated primary key.
68
+ name: Human-readable site name.
69
+ latitude: Decimal degrees latitude.
70
+ longitude: Decimal degrees longitude.
71
+ altitude: Altitude in meters (optional).
72
+ measurements: Back-reference to Measurement records for this site.
73
+ """
74
+
75
+ id: Optional[int] = Field(default=None, primary_key=True)
76
+ name: str = Field(..., description="Site name")
77
+ latitude: Optional[float] = Field(None, description="Latitude (°)")
78
+ longitude: Optional[float] = Field(None, description="Longitude (°)")
79
+ altitude: Optional[float] = Field(None, description="Altitude (m)")
80
+ measurements: List["Measurement"] = Relationship(back_populates="location")
81
+
82
+
83
+ class Measurement(SQLModel, table=True):
84
+ """
85
+ A single earthing measurement event.
86
+
87
+ Attributes:
88
+ id: Auto-generated primary key.
89
+ timestamp: UTC datetime when the measurement occurred.
90
+ location_id: FK to Location.
91
+ location: Relationship to the Location object.
92
+ method: Measurement method used.
93
+ voltage_level_kv: System voltage in kilovolts (optional).
94
+ asset_type: Type of asset under test.
95
+ fault_resistance_ohm: Fault resistance in ohms (optional).
96
+ operator: Name or identifier of the operator (optional).
97
+ description: Free-text notes (optional).
98
+ items: List of MeasurementItem objects associated to this event.
99
+ """
100
+
101
+ id: Optional[int] = Field(default=None, primary_key=True)
102
+ timestamp: datetime = Field(
103
+ default_factory=lambda: datetime.now(timezone.utc),
104
+ description="UTC timestamp of measurement",
105
+ )
106
+ location_id: Optional[int] = Field(default=None, foreign_key="location.id")
107
+ location: Optional[Location] = Relationship(back_populates="measurements")
108
+ method: MethodType = Field(
109
+ sa_column=Column(String, nullable=False), description="Measurement method"
110
+ )
111
+ voltage_level_kv: Optional[float] = Field(None, description="Voltage level in kV")
112
+ asset_type: AssetType = Field(
113
+ sa_column=Column(String, nullable=False), description="Type of asset"
114
+ )
115
+ fault_resistance_ohm: Optional[float] = Field(
116
+ None, description="Fault resistance (Ω)"
117
+ )
118
+ operator: Optional[str] = Field(None, description="Operator name")
119
+ description: Optional[str] = Field(None, description="Notes")
120
+ items: List["MeasurementItem"] = Relationship(back_populates="measurement")
121
+
122
+
123
+ class MeasurementItem(SQLModel, table=True):
124
+ """
125
+ A single data point within a Measurement.
126
+
127
+ Supports both real/imaginary and magnitude/angle representations.
128
+
129
+ Attributes:
130
+ id: Auto-generated primary key.
131
+ measurement_type: Type of this data point.
132
+ value: Scalar magnitude (Ω or other unit).
133
+ value_real: Real component, if complex (Ω).
134
+ value_imag: Imaginary component, if complex (Ω).
135
+ value_angle_deg: Phase angle in degrees (optional).
136
+ unit: Unit string, e.g. "Ω", "m".
137
+ description: Free-text notes (optional).
138
+ frequency_hz: Frequency in Hz (optional).
139
+ additional_resistance_ohm: Extra series resistance (optional).
140
+ input_impedance_ohm: Instrument input impedance (optional).
141
+ measurement_distance_m: Depth or distance for resistivity (optional).
142
+ measurement_id: FK to parent Measurement.
143
+ measurement: Relationship to the Measurement object.
144
+ """
145
+
146
+ id: Optional[int] = Field(default=None, primary_key=True)
147
+ measurement_type: MeasurementType = Field(
148
+ sa_column=Column(String, nullable=False), description="Data point type"
149
+ )
150
+ value: Optional[float] = Field(None, description="Magnitude or scalar value")
151
+ value_real: Optional[float] = Field(None, description="Real part of complex value")
152
+ value_imag: Optional[float] = Field(
153
+ None, description="Imaginary part of complex value"
154
+ )
155
+ value_angle_deg: Optional[float] = Field(None, description="Phase angle in degrees")
156
+ unit: str = Field(..., description="Unit of the measurement")
157
+ description: Optional[str] = Field(None, description="Item notes")
158
+ frequency_hz: Optional[float] = Field(None, description="Frequency (Hz)")
159
+ additional_resistance_ohm: Optional[float] = Field(
160
+ None, description="Additional series resistance (Ω)"
161
+ )
162
+ input_impedance_ohm: Optional[float] = Field(
163
+ None, description="Instrument input impedance (Ω)"
164
+ )
165
+ measurement_distance_m: Optional[float] = Field(
166
+ None, description="Depth/distance for soil resistivity (m)"
167
+ )
168
+ measurement_id: Optional[int] = Field(default=None, foreign_key="measurement.id")
169
+ measurement: Optional[Measurement] = Relationship(back_populates="items")
170
+
171
+
172
+ @event.listens_for(MeasurementItem, "before_insert", propagate=True)
173
+ @event.listens_for(MeasurementItem, "before_update", propagate=True)
174
+ def _compute_magnitude(mapper, connection, target: MeasurementItem):
175
+ """
176
+ SQLAlchemy event listener to enforce and propagate between
177
+ complex and polar representations:
178
+
179
+ - If `value` is None but real/imag are set, computes magnitude
180
+ and phase angle (degrees).
181
+ - If `value` is set and `value_angle_deg` is set, computes
182
+ `value_real` and `value_imag`.
183
+ - If neither representation is present, raises ValueError.
184
+
185
+ Raises:
186
+ ValueError: if no valid value is provided.
187
+ """
188
+ try:
189
+ # Case A: only rectangular given → compute scalar and angle
190
+ if target.value is None:
191
+ if target.value_real is not None or target.value_imag is not None:
192
+ r = target.value_real or 0.0
193
+ i = target.value_imag or 0.0
194
+ target.value = math.hypot(r, i)
195
+ target.value_angle_deg = float(np.degrees(np.arctan2(i, r)))
196
+ else:
197
+ logger.error(
198
+ "MeasurementItem %s lacks both magnitude and real/imag components",
199
+ getattr(target, "id", "<new>"),
200
+ )
201
+ raise ValueError(
202
+ "Either `value` or at least one of (`value_real`, `value_imag`) must be provided"
203
+ )
204
+ # Case B: polar given → compute rectangular components
205
+ elif target.value_angle_deg is not None:
206
+ angle_rad = math.radians(target.value_angle_deg)
207
+ target.value_real = float(target.value * math.cos(angle_rad))
208
+ target.value_imag = float(target.value * math.sin(angle_rad))
209
+ except Exception:
210
+ # Ensure that any unexpected error in conversion is logged
211
+ logger.exception(
212
+ "Failed to compute magnitude/angle for MeasurementItem %s",
213
+ getattr(target, "id", "<new>"),
214
+ )
215
+ raise
groundmeas/plots.py ADDED
@@ -0,0 +1,198 @@
1
+ # src/groundmeas/plots.py
2
+
3
+ import matplotlib.pyplot as plt
4
+ from typing import Tuple, Union, List, Dict, Any, Optional
5
+ import warnings
6
+ import numpy as np
7
+
8
+ from .analytics import (
9
+ impedance_over_frequency,
10
+ real_imag_over_frequency,
11
+ voltage_vt_epr,
12
+ )
13
+
14
+
15
+ def plot_imp_over_f(
16
+ measurement_ids: Union[int, List[int]], normalize_freq_hz: Optional[float] = None
17
+ ) -> plt.Figure:
18
+ """
19
+ Plot earthing impedance versus frequency for one or multiple measurements on a single figure.
20
+
21
+ Args:
22
+ measurement_ids: single ID or list of Measurement IDs.
23
+ normalize_freq_hz: if provided, normalize each impedance curve by its impedance at this frequency.
24
+
25
+ Returns:
26
+ A matplotlib Figure containing all curves.
27
+
28
+ Raises:
29
+ ValueError: if normalize_freq_hz is specified but a measurement lacks that frequency,
30
+ or if no data is available for a single measurement.
31
+ """
32
+ # Normalize input to list
33
+ single = isinstance(measurement_ids, int)
34
+ ids: List[int] = [measurement_ids] if single else list(measurement_ids)
35
+
36
+ # Create a single figure and axis
37
+ fig, ax = plt.subplots()
38
+
39
+ plotted = False
40
+ for mid in ids:
41
+ # Retrieve impedance-frequency map
42
+ freq_imp = impedance_over_frequency(mid)
43
+ if not freq_imp:
44
+ warnings.warn(
45
+ f"No earthing_impedance data for measurement_id={mid}; skipping curve",
46
+ UserWarning,
47
+ )
48
+ continue
49
+
50
+ # Sort frequencies
51
+ freqs = sorted(freq_imp.keys())
52
+ imps = [freq_imp[f] for f in freqs]
53
+
54
+ # Normalize if requested
55
+ if normalize_freq_hz is not None:
56
+ baseline = freq_imp.get(normalize_freq_hz)
57
+ if baseline is None:
58
+ raise ValueError(
59
+ f"Measurement {mid} has no impedance at {normalize_freq_hz} Hz for normalization"
60
+ )
61
+ imps = [val / baseline for val in imps]
62
+
63
+ # Plot the curve
64
+ ax.plot(freqs, imps, marker="o", linestyle="-", label=f"ID {mid}")
65
+ plotted = True
66
+
67
+ if not plotted:
68
+ if single:
69
+ raise ValueError(
70
+ f"No earthing_impedance data available for measurement_id={measurement_ids}"
71
+ )
72
+ else:
73
+ raise ValueError(
74
+ "No earthing_impedance data available for the provided measurement IDs."
75
+ )
76
+
77
+ # Labels and title
78
+ ax.set_xlabel("Frequency (Hz)")
79
+ ylabel = (
80
+ "Normalized Impedance" if normalize_freq_hz is not None else "Impedance (Ω)"
81
+ )
82
+ ax.set_ylabel(ylabel)
83
+ title = "Impedance vs Frequency"
84
+ if normalize_freq_hz is not None:
85
+ title += f" (Normalized @ {normalize_freq_hz} Hz)"
86
+ ax.set_title(title)
87
+
88
+ # Grid and scientific tick formatting
89
+ ax.grid(True, which="both", linestyle="--", linewidth=0.5)
90
+ ax.ticklabel_format(axis="y", style="sci", scilimits=(0, 0))
91
+
92
+ # Legend
93
+ ax.legend()
94
+ fig.tight_layout()
95
+ return fig
96
+
97
+
98
+ def plot_rho_f_model(
99
+ measurement_ids: List[int],
100
+ rho_f: Tuple[float, float, float, float, float],
101
+ rho: Union[float, List[float]] = 100,
102
+ ) -> plt.Figure:
103
+ """
104
+ Plot measured impedance and rho-f model on the same axes.
105
+
106
+ Args:
107
+ measurement_ids: list of Measurement IDs.
108
+ rho_f: tuple (k1, k2, k3, k4, k5).
109
+ rho: single float or list of rho values. For list, multiple model curves are plotted.
110
+
111
+ Returns:
112
+ A matplotlib Figure with measured and modeled impedance magnitude vs frequency.
113
+ """
114
+ # Plot measured curves
115
+ fig = plot_imp_over_f(measurement_ids)
116
+ ax = fig.axes[0]
117
+
118
+ # Gather real/imag data
119
+ rimap = real_imag_over_frequency(measurement_ids)
120
+ # Union of frequencies
121
+ all_freqs = set()
122
+ for freq_map in rimap.values():
123
+ all_freqs.update(freq_map.keys())
124
+ freqs = sorted(all_freqs)
125
+
126
+ # Unpack model coefficients
127
+ k1, k2, k3, k4, k5 = rho_f
128
+
129
+ # Normalize rho parameter to list
130
+ rhos: List[float] = [rho] if isinstance(rho, (int, float)) else list(rho)
131
+
132
+ # Plot model curves for each rho
133
+ for rho_val in rhos:
134
+ model_mag = [
135
+ abs((k1) * rho_val + (k2 + 1j * k3) * f + (k4 + 1j * k5) * rho_val * f)
136
+ for f in freqs
137
+ ]
138
+ ax.plot(
139
+ freqs, model_mag, linestyle="--", linewidth=2, label=f"Model (ρ={rho_val})"
140
+ )
141
+
142
+ ax.legend()
143
+ return fig
144
+
145
+
146
+ def plot_voltage_vt_epr(
147
+ measurement_ids: Union[int, List[int]],
148
+ frequency: float = 50.0
149
+ ) -> plt.Figure:
150
+ """
151
+ Bar‐plot of EPR and both min/max of prospective and actual touch voltages.
152
+
153
+ For each measurement ID you’ll see:
154
+ • EPR (V) at x–width
155
+ • Prospective TV max & min overlayed at x
156
+ • Actual TV max & min overlayed at x+width
157
+
158
+ All voltage bars start at zero so the longer (max) bars show their full length
159
+ behind the shorter (min) bars.
160
+ """
161
+ # 1) get the numbers
162
+ data = voltage_vt_epr(measurement_ids, frequency=frequency)
163
+ single = isinstance(measurement_ids, int)
164
+ ids: List[int] = [measurement_ids] if single else list(measurement_ids)
165
+ if single:
166
+ data = {measurement_ids: data}
167
+
168
+ # 2) prepare figure
169
+ fig, ax = plt.subplots()
170
+ x = np.arange(len(ids))
171
+ width = 0.25
172
+
173
+ # 3) EPR bars
174
+ epr = [data[mid].get("epr", 0.0) for mid in ids]
175
+ ax.bar(x - width, epr, width, label="EPR (V/A)", color="C0")
176
+
177
+ # 4) Prospective TV (V/A): max behind (semi‐transparent), min on top
178
+ vtp_max = [data[mid].get("vtp_max", 0.0) for mid in ids]
179
+ vtp_min = [data[mid].get("vtp_min", 0.0) for mid in ids]
180
+ ax.bar(x, vtp_max, width, color="C1", alpha=0.6, label="Vtp max")
181
+ ax.bar(x, vtp_min, width, color="C1", alpha=1.0, label="Vtp min")
182
+
183
+ # 5) Actual TV (V/A): max behind, min on top
184
+ vt_max = [data[mid].get("vt_max", 0.0) for mid in ids]
185
+ vt_min = [data[mid].get("vt_min", 0.0) for mid in ids]
186
+ ax.bar(x + width, vt_max, width, color="C2", alpha=0.6, label="Vt max")
187
+ ax.bar(x + width, vt_min, width, color="C2", alpha=1.0, label="Vt min")
188
+
189
+ # 6) formatting
190
+ ax.set_xticks(x)
191
+ ax.set_xticklabels([str(mid) for mid in ids])
192
+ ax.set_xlabel("Measurement ID")
193
+ ax.set_ylabel("V/A")
194
+ ax.set_title(f"EPR & Touch Voltages Min/Max @ {frequency} Hz")
195
+ ax.legend()
196
+ ax.grid(True, which="both", linestyle="--", linewidth=0.5)
197
+ fig.tight_layout()
198
+ return fig
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: groundmeas
3
+ Version: 0.3.1
4
+ Summary:
5
+ License-File: LICENSE
6
+ Author: Christian Ehlert
7
+ Author-email: christian.ehlert@mailbox.org
8
+ Requires-Python: >=3.12
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: matplotlib (>=3.10.1,<4.0.0)
14
+ Requires-Dist: numpy (>=2.2.5,<3.0.0)
15
+ Requires-Dist: pandas (>=2.2.3,<3.0.0)
16
+ Requires-Dist: prompt_toolkit (>=3.0.48,<4.0.0)
17
+ Requires-Dist: sqlite-utils (>=3.38,<4.0)
18
+ Requires-Dist: sqlmodel (>=0.0.24,<0.0.25)
19
+ Requires-Dist: typer (>=0.15.3,<0.16.0)
20
+ Description-Content-Type: text/markdown
21
+
22
+ # groundmeas: Grounding System Measurements & Analysis
23
+
24
+ **groundmeas** is a Python package for collecting, storing, analyzing, and visualizing earthing (grounding) measurement data.
25
+
26
+ ---
27
+
28
+ ## Project Description
29
+
30
+ groundmeas provides:
31
+
32
+ * **Database models & CRUD** via SQLModel/SQLAlchemy (`Location`, `Measurement`, `MeasurementItem`).
33
+ * **Export utilities** to JSON, CSV, and XML.
34
+ * **Analytics routines** for impedance-over-frequency, real–imaginary processing, rho–f model fitting, and shield-current split factors (`calculate_split_factor`).
35
+ * **Plotting helpers** for impedance vs frequency and model overlays using Matplotlib.
36
+ * **CLI** (`gm-cli`) with interactive entry, DB-backed autocomplete, listing, add/edit items and measurements, default DB config, and JSON import/export.
37
+
38
+ It’s designed to help engineers and researchers work with earthing measurement campaigns, automate data pipelines, and quickly gain insights on soil resistivity and grounding impedance behavior.
39
+
40
+ ---
41
+
42
+ ## Technical Background
43
+
44
+ In grounding studies, measurements of earth electrode impedance are taken over a range of frequencies, and soil resistivity measurements at various depths are collected.
45
+
46
+ * **Earthing Impedance** $Z$ vs. **Frequency** (f): typically expressed in Ω.
47
+ * **Soil Resistivity** (ρ) vs. **Depth** (d): used to model frequency‑dependent behavior.
48
+ * **rho–f Model**: fits the relationship
49
+
50
+ $$
51
+ Z(ρ, f) = k_1·ρ + (k_2 + j·k_3)·f + (k_4 + j·k_5)·ρ·f
52
+ $$
53
+
54
+ where $k_1…k_5$ are real coefficients determined by least‑squares across multiple measurements.
55
+
56
+ ---
57
+
58
+ ## Installation
59
+
60
+ Requires Python 3.12+:
61
+
62
+ ```bash
63
+ git clone https://github.com/Ce1ectric/groundmeas.git
64
+ cd groundmeas
65
+ poetry install
66
+ poetry shell
67
+ ```
68
+
69
+ or using pip locally:
70
+ ```bash
71
+ git clone https://github.com/Ce1ectric/groundmeas.git
72
+ cd groundmeas
73
+ pip install .
74
+ ```
75
+
76
+ Or install via pip: `pip install groundmeas`.
77
+
78
+ ---
79
+
80
+ ## Usage
81
+ ### 0. CLI quickstart
82
+
83
+ ```bash
84
+ gm-cli --db path/to/data.db add-measurement # interactive wizard with autocomplete
85
+ gm-cli --db path/to/data.db list-measurements
86
+ gm-cli --db path/to/data.db list-items 1
87
+ gm-cli --db path/to/data.db edit-measurement 1 # edit a measurement with defaults prefilled
88
+ gm-cli --db path/to/data.db import-json notebooks/measurements/foo_measurement.json
89
+ gm-cli --db path/to/data.db export-json out.json
90
+ # Add a single item to an existing measurement
91
+ gm-cli --db path/to/data.db add-item 5
92
+ # Edit an existing item
93
+ gm-cli --db path/to/data.db edit-item 42
94
+ # Analytics from CLI
95
+ gm-cli --db path/to/data.db impedance-over-frequency 1 --json-out imp.json
96
+ gm-cli --db path/to/data.db rho-f-model 1 2 3 --json-out rho.json
97
+ gm-cli --db path/to/data.db voltage-vt-epr 1 2 -f 50 --json-out vt.json
98
+ gm-cli --db path/to/data.db calculate-split-factor --earth-fault-id 10 --shield-id 11 --shield-id 12
99
+ # Plotting from CLI (writes images)
100
+ gm-cli --db path/to/data.db plot-impedance 1 2 --out imp.png
101
+ gm-cli --db path/to/data.db plot-rho-f-model 1 2 3 --out rho.png
102
+ gm-cli --db path/to/data.db plot-voltage-vt-epr 1 2 --out vt.png
103
+ # Save a default DB path (~/.config/groundmeas/config.json) so --db is optional
104
+ gm-cli set-default-db path/to/data.db
105
+ # Enable shell completion (example for zsh)
106
+ gm-cli --install-completion zsh
107
+ ```
108
+
109
+ Set `GROUNDMEAS_DB` to avoid passing `--db` each time.
110
+
111
+ ### 1. Database Setup
112
+
113
+ Initialize or connect to a SQLite database (tables will be created automatically):
114
+
115
+ ```python
116
+ from groundmeas.db import connect_db
117
+ connect_db("mydata.db", echo=True)
118
+ ```
119
+
120
+ ### 2. Creating Measurements
121
+
122
+ Insert a measurement (optionally with nested location) and its items:
123
+
124
+ ```python
125
+ from groundmeas.db import create_measurement, create_item
126
+
127
+ # Create measurement with nested Location
128
+ meas_id = create_measurement({
129
+ "timestamp": "2025-01-01T12:00:00",
130
+ "method": "staged_fault_test",
131
+ "voltage_level_kv": 10.0,
132
+ "asset_type": "substation",
133
+ "location": {"name": "Site A", "latitude": 52.0, "longitude": 13.0},
134
+ })
135
+
136
+ # Add earthing impedance item
137
+ item_id = create_item({
138
+ "measurement_type": "earthing_impedance",
139
+ "frequency_hz": 50.0,
140
+ "value": 12.3
141
+ }, measurement_id=meas_id)
142
+ ```
143
+
144
+ ### 3. Exporting Data
145
+
146
+ Export measurements (and nested items) to various formats:
147
+
148
+ ```python
149
+ from groundmeas.export import (
150
+ export_measurements_to_json,
151
+ export_measurements_to_csv,
152
+ export_measurements_to_xml,
153
+ )
154
+
155
+ export_measurements_to_json("data.json")
156
+ export_measurements_to_csv("data.csv")
157
+ export_measurements_to_xml("data.xml")
158
+ ```
159
+
160
+ ### 4. Analytics
161
+
162
+ Compute relevant connections between quantities of the earthing system:
163
+ - impedance and
164
+ - real / imaginary parts over frequency,
165
+ - fit the rho–f model
166
+ - prospective touch voltage vs. Earth Potential Rise
167
+
168
+ ```python
169
+ from groundmeas.analytics import (
170
+ impedance_over_frequency,
171
+ real_imag_over_frequency,
172
+ rho_f_model,
173
+ voltage_vt_epr,
174
+ shield_currents_for_location,
175
+ calculate_split_factor,
176
+ )
177
+
178
+ # Impedance vs frequency for a single measurement
179
+ imp_map = impedance_over_frequency(1)
180
+
181
+ # Real & Imag components for multiple measurements
182
+ ri_map = real_imag_over_frequency([1, 2, 3])
183
+
184
+ # Fit rho–f model across measurements [1,2,3]
185
+ k1, k2, k3, k4, k5 = rho_f_model([1, 2, 3])
186
+
187
+ # summarise the min, max measured touch voltages and the EPR for multiple measurements
188
+ touch_min, touch_max, epr = voltage_vt_epr([1, 2, 3])
189
+
190
+ # Gather available shield currents at a site and compute split factors
191
+ candidates = shield_currents_for_location(location_id=5, frequency_hz=50.0)
192
+ # Pick the shield_current item IDs that share the same angle reference
193
+ shield_ids = [c["id"] for c in candidates]
194
+ split = calculate_split_factor(
195
+ earth_fault_current_id=42,
196
+ shield_current_ids=shield_ids,
197
+ )
198
+ split_factor = split["split_factor"]
199
+ local_earthing_current = split["local_earthing_current"]["value"]
200
+ ```
201
+
202
+ ### 5. Plotting
203
+
204
+ Visualize raw and modeled curves:
205
+
206
+ ```python
207
+ from groundmeas.plots import plot_imp_over_f, plot_rho_f_model
208
+
209
+ # Raw impedance curves
210
+ fig1 = plot_imp_over_f([1, 2, 3])
211
+ fig1.show()
212
+
213
+ # Normalized at 50 Hz
214
+ fig2 = plot_imp_over_f(1, normalize_freq_hz=50)
215
+ fig2.show()
216
+
217
+ # Overlay rho–f model
218
+ fig3 = plot_rho_f_model([1,2,3], (k1,k2,k3,k4,k5), rho=[100, 200])
219
+ fig3.show()
220
+ ```
221
+
222
+ ## Contributing
223
+
224
+ Pull requests are welcome!
225
+ For major changes, please open an issue first to discuss.
226
+ Ensure tests pass and add new tests for your changes.
227
+
228
+ ---
229
+
230
+ ## License
231
+
232
+ MIT License. See [LICENSE](LICENSE) for details.
233
+
@@ -0,0 +1,12 @@
1
+ groundmeas/__init__.py,sha256=uIF7B3aewIWQZjigqinH4-wt8cYOWne0JhbZm1mims4,2271
2
+ groundmeas/analytics.py,sha256=atyH3KYm4hOECuq5eUR8e8JOvpS49TGcrNdP_MCmRHI,18093
3
+ groundmeas/cli.py,sha256=KP7JKpzrIwKljkWW74sGnKtg4wyPQ9kmNELhTjBek4M,30462
4
+ groundmeas/db.py,sha256=p4F8rl_CME6skcAgGWF9HgRTMxvS4dWRw9XF5z4fEk8,13709
5
+ groundmeas/export.py,sha256=cBY7zJaXXHCdRqJXwSCuhnX6ywMoo7Jbp0hhPxnEUjM,5687
6
+ groundmeas/models.py,sha256=LgnRbxdjwDtAyxcB8PaLutL4L1os1fh2P_888jDV7Uk,8270
7
+ groundmeas/plots.py,sha256=oeYp9HqIRGPdtnUDVMrL0sYGHkyF0stmAZp12WxVzbU,6424
8
+ groundmeas-0.3.1.dist-info/METADATA,sha256=VT-a_DSZ50gh0jfV-UlsomIH9gy3z4_RhU2qDXfzrMw,7142
9
+ groundmeas-0.3.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
10
+ groundmeas-0.3.1.dist-info/entry_points.txt,sha256=YW0mRPeeW_evhusVPCOW4AfFTCTBAIG1MMy91exwRZY,45
11
+ groundmeas-0.3.1.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ groundmeas-0.3.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ gm-cli=groundmeas.cli:app
3
+
File without changes