groundmeas 0.1.0__tar.gz
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.
Potentially problematic release.
This version of groundmeas might be problematic. Click here for more details.
- groundmeas-0.1.0/LICENSE +0 -0
- groundmeas-0.1.0/PKG-INFO +178 -0
- groundmeas-0.1.0/README.md +160 -0
- groundmeas-0.1.0/pyproject.toml +31 -0
- groundmeas-0.1.0/src/groundmeas/__init__.py +84 -0
- groundmeas-0.1.0/src/groundmeas/analytics.py +248 -0
- groundmeas-0.1.0/src/groundmeas/db.py +416 -0
- groundmeas-0.1.0/src/groundmeas/export.py +169 -0
- groundmeas-0.1.0/src/groundmeas/models.py +214 -0
- groundmeas-0.1.0/src/groundmeas/plots.py +137 -0
groundmeas-0.1.0/LICENSE
ADDED
|
File without changes
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: groundmeas
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Christian Ehlert
|
|
6
|
+
Author-email: christian.ehlert@mailbox.org
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Dist: matplotlib (>=3.10.1,<4.0.0)
|
|
12
|
+
Requires-Dist: numpy (>=2.2.5,<3.0.0)
|
|
13
|
+
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
14
|
+
Requires-Dist: sqlite-utils (>=3.38,<4.0)
|
|
15
|
+
Requires-Dist: sqlmodel (>=0.0.24,<0.0.25)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# groundmeas: Grounding System Measurements & Analysis
|
|
19
|
+
|
|
20
|
+
**groundmeas** is a Python package for collecting, storing, analyzing, and visualizing earthing (grounding) measurement data.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Project Description
|
|
25
|
+
|
|
26
|
+
groundmeas provides:
|
|
27
|
+
|
|
28
|
+
* **Database models & CRUD** via SQLModel/SQLAlchemy (`Location`, `Measurement`, `MeasurementItem`).
|
|
29
|
+
* **Export utilities** to JSON, CSV, and XML.
|
|
30
|
+
* **Analytics routines** for impedance-over-frequency, real–imaginary processing, and rho–f model fitting.
|
|
31
|
+
* **Plotting helpers** for impedance vs frequency and model overlays using Matplotlib.
|
|
32
|
+
|
|
33
|
+
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.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Technical Background
|
|
38
|
+
|
|
39
|
+
In grounding studies, measurements of earth electrode impedance are taken over a range of frequencies, and soil resistivity measurements at various depths are collected.
|
|
40
|
+
|
|
41
|
+
* **Earthing Impedance** $Z$ vs. **Frequency** (f): typically expressed in Ω.
|
|
42
|
+
* **Soil Resistivity** (ρ) vs. **Depth** (d): used to model frequency‑dependent behavior.
|
|
43
|
+
* **rho–f Model**: fits the relationship
|
|
44
|
+
|
|
45
|
+
$$
|
|
46
|
+
Z(ρ, f) = k_1·ρ + (k_2 + j·k_3)·f + (k_4 + j·k_5)·ρ·f
|
|
47
|
+
$$
|
|
48
|
+
|
|
49
|
+
where $k_1…k_5$ are real coefficients determined by least‑squares across multiple measurements.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
Requires Python 3.12+:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone https://github.com/Ce1ectric/groundmeas.git
|
|
59
|
+
cd groundmeas
|
|
60
|
+
poetry install
|
|
61
|
+
poetry shell
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
or using pip locally:
|
|
65
|
+
```bash
|
|
66
|
+
git clone https://github.com/Ce1ectric/groundmeas.git
|
|
67
|
+
cd groundmeas
|
|
68
|
+
pip install .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or install via pip: `pip install groundmeas`.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
### 1. Database Setup
|
|
78
|
+
|
|
79
|
+
Initialize or connect to a SQLite database (tables will be created automatically):
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from groundmeas.db import connect_db
|
|
83
|
+
connect_db("mydata.db", echo=True)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Creating Measurements
|
|
87
|
+
|
|
88
|
+
Insert a measurement (optionally with nested location) and its items:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from groundmeas.db import create_measurement, create_item
|
|
92
|
+
|
|
93
|
+
# Create measurement with nested Location
|
|
94
|
+
meas_id = create_measurement({
|
|
95
|
+
"timestamp": "2025-01-01T12:00:00",
|
|
96
|
+
"method": "staged_fault_test",
|
|
97
|
+
"voltage_level_kv": 10.0,
|
|
98
|
+
"asset_type": "substation",
|
|
99
|
+
"location": {"name": "Site A", "latitude": 52.0, "longitude": 13.0},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
# Add earthing impedance item
|
|
103
|
+
item_id = create_item({
|
|
104
|
+
"measurement_type": "earthing_impedance",
|
|
105
|
+
"frequency_hz": 50.0,
|
|
106
|
+
"value": 12.3
|
|
107
|
+
}, measurement_id=meas_id)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 3. Exporting Data
|
|
111
|
+
|
|
112
|
+
Export measurements (and nested items) to various formats:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from groundmeas.export import (
|
|
116
|
+
export_measurements_to_json,
|
|
117
|
+
export_measurements_to_csv,
|
|
118
|
+
export_measurements_to_xml,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
export_measurements_to_json("data.json")
|
|
122
|
+
export_measurements_to_csv("data.csv")
|
|
123
|
+
export_measurements_to_xml("data.xml")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 4. Analytics
|
|
127
|
+
|
|
128
|
+
Compute impedance and resistivity mappings, and fit the rho–f model:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from groundmeas.analytics import (
|
|
132
|
+
impedance_over_frequency,
|
|
133
|
+
real_imag_over_frequency,
|
|
134
|
+
rho_f_model,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Impedance vs frequency for a single measurement
|
|
138
|
+
imp_map = impedance_over_frequency(1)
|
|
139
|
+
|
|
140
|
+
# Real & Imag components for multiple measurements
|
|
141
|
+
ri_map = real_imag_over_frequency([1, 2, 3])
|
|
142
|
+
|
|
143
|
+
# Fit rho–f model across measurements [1,2,3]
|
|
144
|
+
k1, k2, k3, k4, k5 = rho_f_model([1, 2, 3])
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 5. Plotting
|
|
148
|
+
|
|
149
|
+
Visualize raw and modeled curves:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from groundmeas.plots import plot_imp_over_f, plot_rho_f_model
|
|
153
|
+
|
|
154
|
+
# Raw impedance curves
|
|
155
|
+
fig1 = plot_imp_over_f([1, 2, 3])
|
|
156
|
+
fig1.show()
|
|
157
|
+
|
|
158
|
+
# Normalized at 50 Hz
|
|
159
|
+
fig2 = plot_imp_over_f(1, normalize_freq_hz=50)
|
|
160
|
+
fig2.show()
|
|
161
|
+
|
|
162
|
+
# Overlay rho–f model
|
|
163
|
+
fig3 = plot_rho_f_model([1,2,3], (k1,k2,k3,k4,k5), rho=[100, 200])
|
|
164
|
+
fig3.show()
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Contributing
|
|
168
|
+
|
|
169
|
+
Pull requests are welcome!
|
|
170
|
+
For major changes, please open an issue first to discuss.
|
|
171
|
+
Ensure tests pass and add new tests for your changes.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
178
|
+
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# groundmeas: Grounding System Measurements & Analysis
|
|
2
|
+
|
|
3
|
+
**groundmeas** is a Python package for collecting, storing, analyzing, and visualizing earthing (grounding) measurement data.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Project Description
|
|
8
|
+
|
|
9
|
+
groundmeas provides:
|
|
10
|
+
|
|
11
|
+
* **Database models & CRUD** via SQLModel/SQLAlchemy (`Location`, `Measurement`, `MeasurementItem`).
|
|
12
|
+
* **Export utilities** to JSON, CSV, and XML.
|
|
13
|
+
* **Analytics routines** for impedance-over-frequency, real–imaginary processing, and rho–f model fitting.
|
|
14
|
+
* **Plotting helpers** for impedance vs frequency and model overlays using Matplotlib.
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Technical Background
|
|
21
|
+
|
|
22
|
+
In grounding studies, measurements of earth electrode impedance are taken over a range of frequencies, and soil resistivity measurements at various depths are collected.
|
|
23
|
+
|
|
24
|
+
* **Earthing Impedance** $Z$ vs. **Frequency** (f): typically expressed in Ω.
|
|
25
|
+
* **Soil Resistivity** (ρ) vs. **Depth** (d): used to model frequency‑dependent behavior.
|
|
26
|
+
* **rho–f Model**: fits the relationship
|
|
27
|
+
|
|
28
|
+
$$
|
|
29
|
+
Z(ρ, f) = k_1·ρ + (k_2 + j·k_3)·f + (k_4 + j·k_5)·ρ·f
|
|
30
|
+
$$
|
|
31
|
+
|
|
32
|
+
where $k_1…k_5$ are real coefficients determined by least‑squares across multiple measurements.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Requires Python 3.12+:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/Ce1ectric/groundmeas.git
|
|
42
|
+
cd groundmeas
|
|
43
|
+
poetry install
|
|
44
|
+
poetry shell
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
or using pip locally:
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/Ce1ectric/groundmeas.git
|
|
50
|
+
cd groundmeas
|
|
51
|
+
pip install .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or install via pip: `pip install groundmeas`.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### 1. Database Setup
|
|
61
|
+
|
|
62
|
+
Initialize or connect to a SQLite database (tables will be created automatically):
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from groundmeas.db import connect_db
|
|
66
|
+
connect_db("mydata.db", echo=True)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Creating Measurements
|
|
70
|
+
|
|
71
|
+
Insert a measurement (optionally with nested location) and its items:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from groundmeas.db import create_measurement, create_item
|
|
75
|
+
|
|
76
|
+
# Create measurement with nested Location
|
|
77
|
+
meas_id = create_measurement({
|
|
78
|
+
"timestamp": "2025-01-01T12:00:00",
|
|
79
|
+
"method": "staged_fault_test",
|
|
80
|
+
"voltage_level_kv": 10.0,
|
|
81
|
+
"asset_type": "substation",
|
|
82
|
+
"location": {"name": "Site A", "latitude": 52.0, "longitude": 13.0},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
# Add earthing impedance item
|
|
86
|
+
item_id = create_item({
|
|
87
|
+
"measurement_type": "earthing_impedance",
|
|
88
|
+
"frequency_hz": 50.0,
|
|
89
|
+
"value": 12.3
|
|
90
|
+
}, measurement_id=meas_id)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3. Exporting Data
|
|
94
|
+
|
|
95
|
+
Export measurements (and nested items) to various formats:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from groundmeas.export import (
|
|
99
|
+
export_measurements_to_json,
|
|
100
|
+
export_measurements_to_csv,
|
|
101
|
+
export_measurements_to_xml,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
export_measurements_to_json("data.json")
|
|
105
|
+
export_measurements_to_csv("data.csv")
|
|
106
|
+
export_measurements_to_xml("data.xml")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 4. Analytics
|
|
110
|
+
|
|
111
|
+
Compute impedance and resistivity mappings, and fit the rho–f model:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from groundmeas.analytics import (
|
|
115
|
+
impedance_over_frequency,
|
|
116
|
+
real_imag_over_frequency,
|
|
117
|
+
rho_f_model,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Impedance vs frequency for a single measurement
|
|
121
|
+
imp_map = impedance_over_frequency(1)
|
|
122
|
+
|
|
123
|
+
# Real & Imag components for multiple measurements
|
|
124
|
+
ri_map = real_imag_over_frequency([1, 2, 3])
|
|
125
|
+
|
|
126
|
+
# Fit rho–f model across measurements [1,2,3]
|
|
127
|
+
k1, k2, k3, k4, k5 = rho_f_model([1, 2, 3])
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 5. Plotting
|
|
131
|
+
|
|
132
|
+
Visualize raw and modeled curves:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from groundmeas.plots import plot_imp_over_f, plot_rho_f_model
|
|
136
|
+
|
|
137
|
+
# Raw impedance curves
|
|
138
|
+
fig1 = plot_imp_over_f([1, 2, 3])
|
|
139
|
+
fig1.show()
|
|
140
|
+
|
|
141
|
+
# Normalized at 50 Hz
|
|
142
|
+
fig2 = plot_imp_over_f(1, normalize_freq_hz=50)
|
|
143
|
+
fig2.show()
|
|
144
|
+
|
|
145
|
+
# Overlay rho–f model
|
|
146
|
+
fig3 = plot_rho_f_model([1,2,3], (k1,k2,k3,k4,k5), rho=[100, 200])
|
|
147
|
+
fig3.show()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Contributing
|
|
151
|
+
|
|
152
|
+
Pull requests are welcome!
|
|
153
|
+
For major changes, please open an issue first to discuss.
|
|
154
|
+
Ensure tests pass and add new tests for your changes.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "groundmeas"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Christian Ehlert",email = "christian.ehlert@mailbox.org"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"sqlmodel (>=0.0.24,<0.0.25)",
|
|
12
|
+
"sqlite-utils (>=3.38,<4.0)",
|
|
13
|
+
"pandas (>=2.2.3,<3.0.0)",
|
|
14
|
+
"numpy (>=2.2.5,<3.0.0)",
|
|
15
|
+
"matplotlib (>=3.10.1,<4.0.0)"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.poetry]
|
|
19
|
+
packages = [{include = "groundmeas", from = "src"}]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
[tool.poetry.group.dev.dependencies]
|
|
23
|
+
typer = "^0.15.3"
|
|
24
|
+
pytest = "^8.3.5"
|
|
25
|
+
black = "^25.1.0"
|
|
26
|
+
ipykernel = "^6.29.5"
|
|
27
|
+
pytest-cov = "^6.1.1"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
31
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
groundmeas
|
|
3
|
+
==========
|
|
4
|
+
|
|
5
|
+
A Python package for managing, storing, analyzing, and plotting earthing measurements.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- SQLite + SQLModel (Pydantic) data models for Measurement, MeasurementItem, and Location.
|
|
9
|
+
- CRUD operations with simple `connect_db`, `create_*`, `read_*`, `update_*`, and `delete_*` APIs.
|
|
10
|
+
- Analytics: impedance vs frequency, real/imag mappings, and rho–f modeling.
|
|
11
|
+
- Plotting helpers wrapping matplotlib for quick visualizations.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
import groundmeas as gm
|
|
15
|
+
|
|
16
|
+
gm.connect_db("ground.db")
|
|
17
|
+
mid = gm.create_measurement({...})
|
|
18
|
+
items, ids = gm.read_items_by(measurement_id=mid)
|
|
19
|
+
fig = gm.plot_imp_over_f(mid)
|
|
20
|
+
fig.show()
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
# Configure a library logger with a NullHandler by default
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
logger.addHandler(logging.NullHandler())
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
__author__ = "Ce1ectric"
|
|
31
|
+
__license__ = "MIT"
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from .db import (
|
|
35
|
+
connect_db,
|
|
36
|
+
create_measurement,
|
|
37
|
+
create_item,
|
|
38
|
+
read_measurements,
|
|
39
|
+
read_measurements_by,
|
|
40
|
+
read_items_by,
|
|
41
|
+
update_measurement,
|
|
42
|
+
update_item,
|
|
43
|
+
delete_measurement,
|
|
44
|
+
delete_item,
|
|
45
|
+
)
|
|
46
|
+
from .models import Location, Measurement, MeasurementItem
|
|
47
|
+
from .analytics import (
|
|
48
|
+
impedance_over_frequency,
|
|
49
|
+
real_imag_over_frequency,
|
|
50
|
+
rho_f_model,
|
|
51
|
+
)
|
|
52
|
+
from .plots import plot_imp_over_f, plot_rho_f_model
|
|
53
|
+
except ImportError as e:
|
|
54
|
+
logger.error("Failed to import groundmeas submodule: %s", e)
|
|
55
|
+
raise
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
# database
|
|
59
|
+
"connect_db",
|
|
60
|
+
"create_measurement",
|
|
61
|
+
"create_item",
|
|
62
|
+
"read_measurements",
|
|
63
|
+
"read_measurements_by",
|
|
64
|
+
"read_items_by",
|
|
65
|
+
"update_measurement",
|
|
66
|
+
"update_item",
|
|
67
|
+
"delete_measurement",
|
|
68
|
+
"delete_item",
|
|
69
|
+
# data models
|
|
70
|
+
"Location",
|
|
71
|
+
"Measurement",
|
|
72
|
+
"MeasurementItem",
|
|
73
|
+
# analytics
|
|
74
|
+
"impedance_over_frequency",
|
|
75
|
+
"real_imag_over_frequency",
|
|
76
|
+
"rho_f_model",
|
|
77
|
+
# plotting
|
|
78
|
+
"plot_imp_over_f",
|
|
79
|
+
"plot_rho_f_model",
|
|
80
|
+
# metadata
|
|
81
|
+
"__version__",
|
|
82
|
+
"__author__",
|
|
83
|
+
"__license__",
|
|
84
|
+
]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
groundmeas.analytics
|
|
3
|
+
====================
|
|
4
|
+
|
|
5
|
+
Analytics functions for the groundmeas package. Provides routines to fetch and
|
|
6
|
+
process impedance and resistivity data for earthing measurements, and to fit
|
|
7
|
+
and evaluate rho–f models.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import itertools
|
|
11
|
+
import logging
|
|
12
|
+
import warnings
|
|
13
|
+
from typing import Dict, Union, List, Tuple
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from .db import read_items_by
|
|
18
|
+
|
|
19
|
+
# configure module‐level logger
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def impedance_over_frequency(
|
|
24
|
+
measurement_ids: Union[int, List[int]],
|
|
25
|
+
) -> Union[Dict[float, float], Dict[int, Dict[float, float]]]:
|
|
26
|
+
"""
|
|
27
|
+
Build a mapping from frequency (Hz) to impedance magnitude (Ω).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
measurement_ids: A single measurement ID or a list of IDs for which
|
|
31
|
+
to retrieve earthing_impedance data.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
If a single ID is provided, returns:
|
|
35
|
+
{ frequency_hz: impedance_value, ... }
|
|
36
|
+
If multiple IDs, returns:
|
|
37
|
+
{ measurement_id: { frequency_hz: impedance_value, ... }, ... }
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
RuntimeError: if retrieving items from the database fails.
|
|
41
|
+
"""
|
|
42
|
+
single = isinstance(measurement_ids, int)
|
|
43
|
+
ids: List[int] = [measurement_ids] if single else list(measurement_ids)
|
|
44
|
+
all_results: Dict[int, Dict[float, float]] = {}
|
|
45
|
+
|
|
46
|
+
for mid in ids:
|
|
47
|
+
try:
|
|
48
|
+
items, _ = read_items_by(
|
|
49
|
+
measurement_id=mid, measurement_type="earthing_impedance"
|
|
50
|
+
)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.error("Error reading impedance items for measurement %s: %s", mid, e)
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
f"Failed to load impedance data for measurement {mid}"
|
|
55
|
+
) from e
|
|
56
|
+
|
|
57
|
+
if not items:
|
|
58
|
+
warnings.warn(
|
|
59
|
+
f"No earthing_impedance measurements found for measurement_id={mid}",
|
|
60
|
+
UserWarning,
|
|
61
|
+
)
|
|
62
|
+
all_results[mid] = {}
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
freq_imp_map: Dict[float, float] = {}
|
|
66
|
+
for item in items:
|
|
67
|
+
freq = item.get("frequency_hz")
|
|
68
|
+
value = item.get("value")
|
|
69
|
+
if freq is None:
|
|
70
|
+
warnings.warn(
|
|
71
|
+
f"MeasurementItem id={item.get('id')} missing frequency_hz; skipping",
|
|
72
|
+
UserWarning,
|
|
73
|
+
)
|
|
74
|
+
continue
|
|
75
|
+
try:
|
|
76
|
+
freq_imp_map[float(freq)] = float(value)
|
|
77
|
+
except Exception:
|
|
78
|
+
warnings.warn(
|
|
79
|
+
f"Could not convert item {item.get('id')} to floats; skipping",
|
|
80
|
+
UserWarning,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
all_results[mid] = freq_imp_map
|
|
84
|
+
|
|
85
|
+
return all_results[ids[0]] if single else all_results
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def real_imag_over_frequency(
|
|
89
|
+
measurement_ids: Union[int, List[int]],
|
|
90
|
+
) -> Union[Dict[float, Dict[str, float]], Dict[int, Dict[float, Dict[str, float]]]]:
|
|
91
|
+
"""
|
|
92
|
+
Build a mapping from frequency to real & imaginary components.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
measurement_ids: A single measurement ID or list of IDs.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
If single ID:
|
|
99
|
+
{ frequency_hz: {"real": real_part, "imag": imag_part}, ... }
|
|
100
|
+
If multiple IDs:
|
|
101
|
+
{ measurement_id: { frequency_hz: {...}, ... }, ... }
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
RuntimeError: if retrieving items from the database fails.
|
|
105
|
+
"""
|
|
106
|
+
single = isinstance(measurement_ids, int)
|
|
107
|
+
ids: List[int] = [measurement_ids] if single else list(measurement_ids)
|
|
108
|
+
all_results: Dict[int, Dict[float, Dict[str, float]]] = {}
|
|
109
|
+
|
|
110
|
+
for mid in ids:
|
|
111
|
+
try:
|
|
112
|
+
items, _ = read_items_by(
|
|
113
|
+
measurement_id=mid, measurement_type="earthing_impedance"
|
|
114
|
+
)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.error("Error reading impedance items for measurement %s: %s", mid, e)
|
|
117
|
+
raise RuntimeError(
|
|
118
|
+
f"Failed to load impedance data for measurement {mid}"
|
|
119
|
+
) from e
|
|
120
|
+
|
|
121
|
+
if not items:
|
|
122
|
+
warnings.warn(
|
|
123
|
+
f"No earthing_impedance measurements found for measurement_id={mid}",
|
|
124
|
+
UserWarning,
|
|
125
|
+
)
|
|
126
|
+
all_results[mid] = {}
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
freq_map: Dict[float, Dict[str, float]] = {}
|
|
130
|
+
for item in items:
|
|
131
|
+
freq = item.get("frequency_hz")
|
|
132
|
+
r = item.get("value_real")
|
|
133
|
+
i = item.get("value_imag")
|
|
134
|
+
if freq is None:
|
|
135
|
+
warnings.warn(
|
|
136
|
+
f"MeasurementItem id={item.get('id')} missing frequency_hz; skipping",
|
|
137
|
+
UserWarning,
|
|
138
|
+
)
|
|
139
|
+
continue
|
|
140
|
+
try:
|
|
141
|
+
freq_map[float(freq)] = {
|
|
142
|
+
"real": float(r) if r is not None else None,
|
|
143
|
+
"imag": float(i) if i is not None else None,
|
|
144
|
+
}
|
|
145
|
+
except Exception:
|
|
146
|
+
warnings.warn(
|
|
147
|
+
f"Could not convert real/imag for item {item.get('id')}; skipping",
|
|
148
|
+
UserWarning,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
all_results[mid] = freq_map
|
|
152
|
+
|
|
153
|
+
return all_results[ids[0]] if single else all_results
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def rho_f_model(
|
|
157
|
+
measurement_ids: List[int],
|
|
158
|
+
) -> Tuple[float, float, float, float, float]:
|
|
159
|
+
"""
|
|
160
|
+
Fit the rho–f model:
|
|
161
|
+
Z(ρ,f) = k1*ρ + (k2 + j*k3)*f + (k4 + j*k5)*ρ*f
|
|
162
|
+
|
|
163
|
+
Enforces that at f=0 the impedance is purely real (→ k1*ρ).
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
measurement_ids: List of measurement IDs to include in the fit.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
A tuple (k1, k2, k3, k4, k5) of real coefficients.
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
ValueError: if no soil_resistivity or no impedance overlap.
|
|
173
|
+
RuntimeError: if the least-squares solve fails.
|
|
174
|
+
"""
|
|
175
|
+
# 1) Gather real/imag data
|
|
176
|
+
rimap = real_imag_over_frequency(measurement_ids)
|
|
177
|
+
|
|
178
|
+
# 2) Gather available depths → ρ
|
|
179
|
+
rho_map: Dict[int, Dict[float, float]] = {}
|
|
180
|
+
depth_choices: List[List[float]] = []
|
|
181
|
+
|
|
182
|
+
for mid in measurement_ids:
|
|
183
|
+
try:
|
|
184
|
+
items, _ = read_items_by(
|
|
185
|
+
measurement_id=mid, measurement_type="soil_resistivity"
|
|
186
|
+
)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error("Error reading soil_resistivity for %s: %s", mid, e)
|
|
189
|
+
raise RuntimeError(
|
|
190
|
+
f"Failed to load soil_resistivity for measurement {mid}"
|
|
191
|
+
) from e
|
|
192
|
+
|
|
193
|
+
dt = {
|
|
194
|
+
float(it["measurement_distance_m"]): float(it["value"])
|
|
195
|
+
for it in items
|
|
196
|
+
if it.get("measurement_distance_m") is not None
|
|
197
|
+
and it.get("value") is not None
|
|
198
|
+
}
|
|
199
|
+
if not dt:
|
|
200
|
+
raise ValueError(f"No soil_resistivity data for measurement {mid}")
|
|
201
|
+
rho_map[mid] = dt
|
|
202
|
+
depth_choices.append(list(dt.keys()))
|
|
203
|
+
|
|
204
|
+
# 3) Select depths minimizing spread
|
|
205
|
+
best_combo, best_spread = None, float("inf")
|
|
206
|
+
for combo in itertools.product(*depth_choices):
|
|
207
|
+
spread = max(combo) - min(combo)
|
|
208
|
+
if spread < best_spread:
|
|
209
|
+
best_spread, best_combo = spread, combo
|
|
210
|
+
|
|
211
|
+
selected_rhos = {
|
|
212
|
+
mid: rho_map[mid][depth] for mid, depth in zip(measurement_ids, best_combo)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# 4) Assemble design matrices & response vectors
|
|
216
|
+
A_R, yR, A_X, yX = [], [], [], []
|
|
217
|
+
|
|
218
|
+
for mid in measurement_ids:
|
|
219
|
+
rho = selected_rhos[mid]
|
|
220
|
+
for f, comp in rimap.get(mid, {}).items():
|
|
221
|
+
R = comp.get("real")
|
|
222
|
+
X = comp.get("imag")
|
|
223
|
+
if R is None or X is None:
|
|
224
|
+
continue
|
|
225
|
+
A_R.append([rho, f, rho * f])
|
|
226
|
+
yR.append(R)
|
|
227
|
+
A_X.append([f, rho * f])
|
|
228
|
+
yX.append(X)
|
|
229
|
+
|
|
230
|
+
if not A_R:
|
|
231
|
+
raise ValueError("No overlapping impedance data available for fitting")
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
A_R = np.vstack(A_R)
|
|
235
|
+
A_X = np.vstack(A_X)
|
|
236
|
+
R_vec = np.asarray(yR)
|
|
237
|
+
X_vec = np.asarray(yX)
|
|
238
|
+
|
|
239
|
+
kR, *_ = np.linalg.lstsq(A_R, R_vec, rcond=None) # [k1, k2, k4]
|
|
240
|
+
kX, *_ = np.linalg.lstsq(A_X, X_vec, rcond=None) # [k3, k5]
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error("Least-squares solve failed: %s", e)
|
|
243
|
+
raise RuntimeError("Failed to solve rho-f least-squares problem") from e
|
|
244
|
+
|
|
245
|
+
k1, k2, k4 = kR
|
|
246
|
+
k3, k5 = kX
|
|
247
|
+
|
|
248
|
+
return float(k1), float(k2), float(k3), float(k4), float(k5)
|