groundmeas 0.1.0__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.

Potentially problematic release.


This version of groundmeas might be problematic. Click here for more details.

groundmeas/__init__.py ADDED
@@ -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)