PyTracerLab 0.2.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.
- PyTracerLab/__init__.py +8 -0
- PyTracerLab/__main__.py +7 -0
- PyTracerLab/gui/__init__.py +10 -0
- PyTracerLab/gui/app.py +15 -0
- PyTracerLab/gui/controller.py +419 -0
- PyTracerLab/gui/database.py +16 -0
- PyTracerLab/gui/main_window.py +75 -0
- PyTracerLab/gui/state.py +122 -0
- PyTracerLab/gui/tabs/file_input.py +436 -0
- PyTracerLab/gui/tabs/model_design.py +322 -0
- PyTracerLab/gui/tabs/parameters.py +131 -0
- PyTracerLab/gui/tabs/simulation.py +300 -0
- PyTracerLab/gui/tabs/solver_params.py +697 -0
- PyTracerLab/gui/tabs/tracer_tracer.py +331 -0
- PyTracerLab/gui/tabs/widgets.py +106 -0
- PyTracerLab/model/__init__.py +52 -0
- PyTracerLab/model/model.py +680 -0
- PyTracerLab/model/registry.py +25 -0
- PyTracerLab/model/solver.py +1335 -0
- PyTracerLab/model/units.py +626 -0
- pytracerlab-0.2.0.dist-info/METADATA +37 -0
- pytracerlab-0.2.0.dist-info/RECORD +26 -0
- pytracerlab-0.2.0.dist-info/WHEEL +5 -0
- pytracerlab-0.2.0.dist-info/entry_points.txt +5 -0
- pytracerlab-0.2.0.dist-info/licenses/LICENSE +21 -0
- pytracerlab-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
"""Base classes for model units."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Dict
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy import integrate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Unit(ABC):
|
|
14
|
+
"""Abstract base class for a model unit.
|
|
15
|
+
|
|
16
|
+
Concrete units represent hydrological transport schemata and must expose
|
|
17
|
+
and accept their **local** parameter values via a mapping. Units are
|
|
18
|
+
intentionally unaware of optimization bounds; those live in the Model's
|
|
19
|
+
parameter registry.
|
|
20
|
+
|
|
21
|
+
Notes
|
|
22
|
+
-----
|
|
23
|
+
- Implementations must keep local parameter names *stable* over time so that
|
|
24
|
+
the Model's registry stays consistent.
|
|
25
|
+
- Names should be short (e.g., ``"mtt"``, ``"eta"``).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def param_values(self) -> Dict[str, float]:
|
|
30
|
+
"""Return current local parameter values.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
Dict[str, float]
|
|
35
|
+
Mapping from local parameter name to value.
|
|
36
|
+
"""
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def set_param_values(self, values: Dict[str, float]) -> None:
|
|
41
|
+
"""Set one or more local parameter values.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
values : Dict[str, float]
|
|
46
|
+
Mapping from local parameter name to new value. Keys not present
|
|
47
|
+
are ignored.
|
|
48
|
+
"""
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
|
|
51
|
+
def get_block(
|
|
52
|
+
self, h: np.ndarray, tau: np.ndarray, dt: float, lambda_: float, prod: bool = False
|
|
53
|
+
) -> np.ndarray:
|
|
54
|
+
"""Get 1-dt block response."""
|
|
55
|
+
# area = float(h.sum() * dt)
|
|
56
|
+
area = integrate.trapezoid(h, dx=dt)
|
|
57
|
+
if not np.isfinite(area) or area <= 0:
|
|
58
|
+
raise ValueError(f"Impulse response has non-positive/invalid area: {area}")
|
|
59
|
+
h /= area
|
|
60
|
+
|
|
61
|
+
# decay / production
|
|
62
|
+
if prod:
|
|
63
|
+
h *= 1 - np.exp(-lambda_ * tau)
|
|
64
|
+
else:
|
|
65
|
+
h *= np.exp(-lambda_ * tau)
|
|
66
|
+
|
|
67
|
+
step = np.cumsum(h) * dt
|
|
68
|
+
step = integrate.cumulative_trapezoid(h, dx=dt, initial=0)
|
|
69
|
+
block = np.append(step[0], np.subtract(step[1:], step[:-1]))
|
|
70
|
+
|
|
71
|
+
return block
|
|
72
|
+
|
|
73
|
+
def normalize_response(self, h: np.ndarray, dt: float) -> np.ndarray:
|
|
74
|
+
"""Normalize impulse response to unit area for conservation of mass."""
|
|
75
|
+
area = float(h.sum() * dt)
|
|
76
|
+
if not np.isfinite(area) or area <= 0:
|
|
77
|
+
raise ValueError(f"Impulse response has non-positive/invalid area: {area}")
|
|
78
|
+
h /= area
|
|
79
|
+
|
|
80
|
+
return h
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def get_impulse_response(self, tau: np.ndarray, dt: float, lambda_: float) -> np.ndarray:
|
|
84
|
+
"""Evaluate the unit's impulse response on a time grid.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
tau : ndarray
|
|
89
|
+
Non-negative time axis (same spacing as simulation time grid).
|
|
90
|
+
dt : float
|
|
91
|
+
Time step size of the discretization.
|
|
92
|
+
lambda_ : float
|
|
93
|
+
Decay constant (1 / time units of ``tau``).
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
ndarray
|
|
98
|
+
Impulse response sampled at ``tau``.
|
|
99
|
+
"""
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class EPMUnit(Unit):
|
|
105
|
+
"""Exponential Piston-Flow Model (EPM) unit.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
mtt : float
|
|
110
|
+
Mean travel time.
|
|
111
|
+
eta : float
|
|
112
|
+
Ratio of total volume to the exponential reservoir (>= 1). ``eta=1``
|
|
113
|
+
reduces to a pure exponential model; ``eta>1`` adds a piston component.
|
|
114
|
+
PREFIX : str
|
|
115
|
+
Prefix for local parameter names. Helper for GUI.
|
|
116
|
+
PARAMS : List[Dict[str, Any]]
|
|
117
|
+
List of (default) parameter definitions. Helper for GUI.
|
|
118
|
+
|
|
119
|
+
Note: The parameter key for the mean travel time (``mtt``) is used in the
|
|
120
|
+
GUI explicitly. The GUI assumes the parameter is given in years and
|
|
121
|
+
internally converts it.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
mtt: float
|
|
125
|
+
eta: float
|
|
126
|
+
PREFIX = "epm"
|
|
127
|
+
# The parameter keys are used explicitly in the GUI! Changing them will lead
|
|
128
|
+
# to the GUI not displaying parameter units properly.
|
|
129
|
+
PARAMS = [
|
|
130
|
+
{"key": "mtt", "label": "Mean Transit Time", "default": 10.0, "bounds": (0.0, 10000.0)},
|
|
131
|
+
{"key": "eta", "label": "Eta", "default": 1.1, "bounds": (1.0, 2.0)},
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
def param_values(self) -> Dict[str, float]:
|
|
135
|
+
"""Get parameter values.
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
Dict[str, float]
|
|
140
|
+
Mapping from local parameter name to value.
|
|
141
|
+
"""
|
|
142
|
+
return {"mtt": float(self.mtt), "eta": float(self.eta)}
|
|
143
|
+
|
|
144
|
+
def set_param_values(self, values: Dict[str, float]) -> None:
|
|
145
|
+
"""Set one or more local parameter values.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
values : Dict[str, float]
|
|
150
|
+
Mapping from local parameter name to new value. Keys not present
|
|
151
|
+
are ignored.
|
|
152
|
+
"""
|
|
153
|
+
if "mtt" in values:
|
|
154
|
+
self.mtt = float(values["mtt"])
|
|
155
|
+
if "eta" in values:
|
|
156
|
+
self.eta = float(values["eta"])
|
|
157
|
+
|
|
158
|
+
def get_impulse_response(
|
|
159
|
+
self, tau: np.ndarray, dt: float, lambda_: float, prod: bool = False
|
|
160
|
+
) -> np.ndarray:
|
|
161
|
+
"""EPM impulse response with decay.
|
|
162
|
+
|
|
163
|
+
The continuous-time EPM response (without decay) is
|
|
164
|
+
``h(τ) = (η/mtt) * exp(-η τ / mtt + η - 1)`` for
|
|
165
|
+
``τ >= mtt*(1 - 1/η)`` and ``0`` otherwise. We also apply
|
|
166
|
+
an exponential decay term ``exp(-λ τ)``.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
tau : ndarray
|
|
171
|
+
Non-negative time axis (same spacing as simulation time grid).
|
|
172
|
+
dt : float
|
|
173
|
+
Time step size of the discretization.
|
|
174
|
+
lambda_ : float
|
|
175
|
+
Decay constant (1 / time units of ``tau``).
|
|
176
|
+
prod : bool, optional
|
|
177
|
+
If True, calculate production response (used to simulate 3He
|
|
178
|
+
production from 3H decay).
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
ndarray
|
|
183
|
+
Impulse response evaluated at ``tau``.
|
|
184
|
+
"""
|
|
185
|
+
# check for edge cases
|
|
186
|
+
if self.eta <= 1.0 or self.mtt <= 0.0:
|
|
187
|
+
return np.zeros_like(tau)
|
|
188
|
+
|
|
189
|
+
# Note: a non-zero response at h[0] corresponds to a response at the
|
|
190
|
+
# same time step as the forcing. This is usually not what we want, so
|
|
191
|
+
# we need to make sure the first time bin is 0.
|
|
192
|
+
|
|
193
|
+
# We don't need to shift the age grid here because we (almost surely)
|
|
194
|
+
# avoid having non-zero response at h[0] anyways for the EPM.
|
|
195
|
+
|
|
196
|
+
# base EPM shape
|
|
197
|
+
h_prelim = (self.eta / self.mtt) * np.exp(-self.eta * tau / self.mtt + self.eta - 1.0)
|
|
198
|
+
cutoff = self.mtt * (1.0 - 1.0 / self.eta)
|
|
199
|
+
h = np.where(tau <= cutoff, 0.0, h_prelim)
|
|
200
|
+
|
|
201
|
+
# get response for constant-input block of length dt
|
|
202
|
+
h = self.get_block(h, tau, dt, lambda_, prod)
|
|
203
|
+
|
|
204
|
+
return h
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass
|
|
208
|
+
class ExEPMUnit(Unit):
|
|
209
|
+
"""Explicit xponential Piston-Flow Model (EPM) unit.
|
|
210
|
+
This model is essentially the same as the EPMUnit, but the EPM ratio
|
|
211
|
+
(total volume / exponential volume or total area / area receiving
|
|
212
|
+
recharge) is defined via two parameters instead of one aggregated
|
|
213
|
+
parameter. Those two parameters are directly related and can never be
|
|
214
|
+
estimated simultaneously.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
mtt : float
|
|
219
|
+
Mean travel time.
|
|
220
|
+
exp_part: float
|
|
221
|
+
Area receiving recharge or exponential volume of the system.
|
|
222
|
+
piston_part: float
|
|
223
|
+
Area not receiving recharge or piston-flow volume of the system.
|
|
224
|
+
PREFIX : str
|
|
225
|
+
Prefix for local parameter names. Helper for GUI.
|
|
226
|
+
PARAMS : List[Dict[str, Any]]
|
|
227
|
+
List of (default) parameter definitions. Helper for GUI.
|
|
228
|
+
|
|
229
|
+
Note: The parameter key for the mean travel time (``mtt``) is used in the
|
|
230
|
+
GUI explicitly. The GUI assumes the parameter is given in years and
|
|
231
|
+
internally converts it.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
mtt: float
|
|
235
|
+
exp_part: float
|
|
236
|
+
piston_part: float
|
|
237
|
+
PREFIX = "epm"
|
|
238
|
+
# The parameter keys are used explicitly in the GUI! Changing them will lead
|
|
239
|
+
# to the GUI not displaying parameter units properly.
|
|
240
|
+
PARAMS = [
|
|
241
|
+
{"key": "mtt", "label": "Mean Transit Time", "default": 10.0, "bounds": (0.0, 10000.0)},
|
|
242
|
+
{"key": "exp_part", "label": "Exponential Part", "default": 0.5, "bounds": (0.0, 100.0)},
|
|
243
|
+
{"key": "piston_part", "label": "Piston Part", "default": 1.0, "bounds": (0.0, 100.0)},
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
def param_values(self) -> Dict[str, float]:
|
|
247
|
+
"""Get parameter values.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
Dict[str, float]
|
|
252
|
+
Mapping from local parameter name to value.
|
|
253
|
+
"""
|
|
254
|
+
return {
|
|
255
|
+
"mtt": float(self.mtt),
|
|
256
|
+
"exp_part": float(self.exp_part),
|
|
257
|
+
"piston_part": float(self.piston_part),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def set_param_values(self, values: Dict[str, float]) -> None:
|
|
261
|
+
"""Set one or more local parameter values.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
values : Dict[str, float]
|
|
266
|
+
Mapping from local parameter name to new value. Keys not present
|
|
267
|
+
are ignored.
|
|
268
|
+
"""
|
|
269
|
+
if "mtt" in values:
|
|
270
|
+
self.mtt = float(values["mtt"])
|
|
271
|
+
if "exp_part" in values:
|
|
272
|
+
self.exp_part = float(values["exp_part"])
|
|
273
|
+
if "piston_part" in values:
|
|
274
|
+
self.piston_part = float(values["piston_part"])
|
|
275
|
+
|
|
276
|
+
def get_impulse_response(
|
|
277
|
+
self, tau: np.ndarray, dt: float, lambda_: float, prod: bool = False
|
|
278
|
+
) -> np.ndarray:
|
|
279
|
+
"""ExEPM impulse response with decay.
|
|
280
|
+
|
|
281
|
+
The continuous-time EPM response (without decay) is
|
|
282
|
+
``h(τ) = (η/mtt) * exp(-η τ / mtt + η - 1)`` for
|
|
283
|
+
``τ >= mtt*(1 - 1/η)`` and ``0`` otherwise. We also apply
|
|
284
|
+
an exponential decay term ``exp(-λ τ)``.
|
|
285
|
+
|
|
286
|
+
Parameters
|
|
287
|
+
----------
|
|
288
|
+
tau : ndarray
|
|
289
|
+
Non-negative time axis (same spacing as simulation time grid).
|
|
290
|
+
dt : float
|
|
291
|
+
Time step size of the discretization.
|
|
292
|
+
lambda_ : float
|
|
293
|
+
Decay constant (1 / time units of ``tau``).
|
|
294
|
+
prod : bool, optional
|
|
295
|
+
If True, calculate production response (used to simulate 3He
|
|
296
|
+
production from 3H decay).
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
ndarray
|
|
301
|
+
Impulse response evaluated at ``tau``.
|
|
302
|
+
"""
|
|
303
|
+
# calculate eta
|
|
304
|
+
eta = (self.piston_part / self.exp_part) + 1
|
|
305
|
+
|
|
306
|
+
# check for edge cases
|
|
307
|
+
if eta <= 1.0 or self.mtt <= 0.0:
|
|
308
|
+
return np.zeros_like(tau)
|
|
309
|
+
|
|
310
|
+
# Note: a non-zero response at h[0] corresponds to a response at the
|
|
311
|
+
# same time step as the forcing. This is usually not what we want, so
|
|
312
|
+
# we need to make sure the first time bin is 0.
|
|
313
|
+
|
|
314
|
+
# We don't need to shift the age grid here because we (almost surely)
|
|
315
|
+
# avoid having non-zero response at h[0] anyways for the EPM.
|
|
316
|
+
|
|
317
|
+
# base EPM shape
|
|
318
|
+
h_prelim = (eta / self.mtt) * np.exp(-eta * tau / self.mtt + eta - 1.0)
|
|
319
|
+
cutoff = self.mtt * (1.0 - 1.0 / eta)
|
|
320
|
+
h = np.where(tau < cutoff, 0.0, h_prelim)
|
|
321
|
+
|
|
322
|
+
# get response for constant-input block of length dt
|
|
323
|
+
h = self.get_block(h, tau, dt, lambda_, prod)
|
|
324
|
+
|
|
325
|
+
return h
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@dataclass
|
|
329
|
+
class DMUnit(Unit):
|
|
330
|
+
"""Dispersion Model (DM) unit.
|
|
331
|
+
|
|
332
|
+
Parameters
|
|
333
|
+
----------
|
|
334
|
+
mtt : float
|
|
335
|
+
Mean travel time.
|
|
336
|
+
DP : float
|
|
337
|
+
Dispersion parameter. Represents the inverse of the Peclet number.
|
|
338
|
+
Also represents the ratio of the dispersion coefficient to the
|
|
339
|
+
velocity and outlet / sampling position
|
|
340
|
+
PREFIX : str
|
|
341
|
+
Prefix for local parameter names. Helper for GUI.
|
|
342
|
+
PARAMS : List[Dict[str, Any]]
|
|
343
|
+
List of (default) parameter definitions. Helper for GUI.
|
|
344
|
+
|
|
345
|
+
Note: The parameter key for the mean travel time (``mtt``) is used in the
|
|
346
|
+
GUI explicitly. The GUI assumes the parameter is given in years and
|
|
347
|
+
internally converts it.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
mtt: float
|
|
351
|
+
DP: float
|
|
352
|
+
PREFIX = "dm"
|
|
353
|
+
# The parameter keys are used explicitly in the GUI! Changing them will lead
|
|
354
|
+
# to the GUI not displaying parameter units properly.
|
|
355
|
+
PARAMS = [
|
|
356
|
+
{"key": "mtt", "label": "Mean Transit Time", "default": 10.0, "bounds": (1.0, 10000.0)},
|
|
357
|
+
{"key": "DP", "label": "Dispersion Param.", "default": 1.0, "bounds": (0.0001, 10.0)},
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
def param_values(self) -> Dict[str, float]:
|
|
361
|
+
"""Get parameter values.
|
|
362
|
+
|
|
363
|
+
Returns
|
|
364
|
+
-------
|
|
365
|
+
Dict[str, float]
|
|
366
|
+
Mapping from local parameter name to value.
|
|
367
|
+
"""
|
|
368
|
+
return {"mtt": float(self.mtt), "DP": float(self.DP)}
|
|
369
|
+
|
|
370
|
+
def set_param_values(self, values: Dict[str, float]) -> None:
|
|
371
|
+
"""Set one or more local parameter values.
|
|
372
|
+
|
|
373
|
+
Parameters
|
|
374
|
+
----------
|
|
375
|
+
values : Dict[str, float]
|
|
376
|
+
Mapping from local parameter name to new value. Keys not present
|
|
377
|
+
are ignored.
|
|
378
|
+
"""
|
|
379
|
+
if "mtt" in values:
|
|
380
|
+
self.mtt = float(values["mtt"])
|
|
381
|
+
if "DP" in values:
|
|
382
|
+
self.DP = float(values["DP"])
|
|
383
|
+
|
|
384
|
+
def get_impulse_response(
|
|
385
|
+
self, tau: np.ndarray, dt: float, lambda_: float, prod: bool = False
|
|
386
|
+
) -> np.ndarray:
|
|
387
|
+
"""DM impulse response with decay.
|
|
388
|
+
|
|
389
|
+
The continuous-time DM response (without decay) is
|
|
390
|
+
``h(τ) = (1/mtt) * 1 / (sqrt(K)) * exp((1 - τ / mtt)^2 / K)`` with
|
|
391
|
+
``K = 4 pi DP (τ / mtt)``. We also apply an exponential decay
|
|
392
|
+
term ``exp(-λ τ)``.
|
|
393
|
+
|
|
394
|
+
Parameters
|
|
395
|
+
----------
|
|
396
|
+
tau : ndarray
|
|
397
|
+
Non-negative time axis (same spacing as simulation time grid).
|
|
398
|
+
dt : float
|
|
399
|
+
Time step size of the discretization.
|
|
400
|
+
lambda_ : float
|
|
401
|
+
Decay constant (1 / time units of ``tau``).
|
|
402
|
+
prod : bool, optional
|
|
403
|
+
If True, calculate production response (used to simulate 3He
|
|
404
|
+
production from 3H decay).
|
|
405
|
+
|
|
406
|
+
Returns
|
|
407
|
+
-------
|
|
408
|
+
ndarray
|
|
409
|
+
Impulse response evaluated at ``tau``.
|
|
410
|
+
"""
|
|
411
|
+
# Check for edge cases
|
|
412
|
+
if self.DP <= 0.0 or self.mtt <= 0.0:
|
|
413
|
+
return np.zeros_like(tau)
|
|
414
|
+
|
|
415
|
+
# Note: a non-zero response at h[0] corresponds to a response at the
|
|
416
|
+
# same time step as the forcing. This is usually not what we want, so
|
|
417
|
+
# we need to make sure the first time bin is 0.
|
|
418
|
+
|
|
419
|
+
# The transfer function breaks down as τ tends towards 0
|
|
420
|
+
# We therefore prepare a result array for h and fill it up, starting
|
|
421
|
+
# with the non-zero values
|
|
422
|
+
h = np.zeros_like(tau)
|
|
423
|
+
|
|
424
|
+
# Pre-compute terms
|
|
425
|
+
buffer = 1
|
|
426
|
+
a = tau[buffer:] * np.sqrt(4 * np.pi * self.DP * tau[buffer:] / self.mtt)
|
|
427
|
+
b = -((1 - tau[buffer:] / self.mtt) ** 2.0 / (4 * self.DP * tau[buffer:] / self.mtt))
|
|
428
|
+
|
|
429
|
+
h[buffer:] = 1 / a * np.exp(b)
|
|
430
|
+
|
|
431
|
+
# get response for constant-input block of length dt
|
|
432
|
+
h = self.get_block(h, tau, dt, lambda_, prod)
|
|
433
|
+
|
|
434
|
+
return h
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@dataclass
|
|
438
|
+
class EMUnit(Unit):
|
|
439
|
+
"""Exponential Model (EM) unit.
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
mtt : float
|
|
444
|
+
Mean travel time.
|
|
445
|
+
PREFIX : str
|
|
446
|
+
Prefix for local parameter names. Helper for GUI.
|
|
447
|
+
PARAMS : List[Dict[str, Any]]
|
|
448
|
+
List of (default) parameter definitions. Helper for GUI.
|
|
449
|
+
|
|
450
|
+
Note: The parameter key for the mean travel time (``mtt``) is used in the
|
|
451
|
+
GUI explicitly. The GUI assumes the parameter is given in years and
|
|
452
|
+
internally converts it.
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
mtt: float
|
|
456
|
+
PREFIX = "em"
|
|
457
|
+
# The parameter keys are used explicitly in the GUI! Changing them will lead
|
|
458
|
+
# to the GUI not displaying parameter units properly.
|
|
459
|
+
PARAMS = [
|
|
460
|
+
{"key": "mtt", "label": "Mean Transit Time", "default": 10.0, "bounds": (0.0, 10000.0)},
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
def param_values(self) -> Dict[str, float]:
|
|
464
|
+
"""Get parameter values.
|
|
465
|
+
|
|
466
|
+
Returns
|
|
467
|
+
-------
|
|
468
|
+
Dict[str, float]
|
|
469
|
+
Mapping from local parameter name to value.
|
|
470
|
+
"""
|
|
471
|
+
return {"mtt": float(self.mtt)}
|
|
472
|
+
|
|
473
|
+
def set_param_values(self, values: Dict[str, float]) -> None:
|
|
474
|
+
"""Set one or more local parameter values.
|
|
475
|
+
|
|
476
|
+
Parameters
|
|
477
|
+
----------
|
|
478
|
+
values : Dict[str, float]
|
|
479
|
+
Mapping from local parameter name to new value. Keys not present
|
|
480
|
+
are ignored.
|
|
481
|
+
"""
|
|
482
|
+
if "mtt" in values:
|
|
483
|
+
self.mtt = float(values["mtt"])
|
|
484
|
+
|
|
485
|
+
def get_impulse_response(
|
|
486
|
+
self, tau: np.ndarray, dt: float, lambda_: float, prod: bool = False
|
|
487
|
+
) -> np.ndarray:
|
|
488
|
+
"""EM impulse response with decay.
|
|
489
|
+
|
|
490
|
+
The continuous-time EPM response (without decay) is
|
|
491
|
+
``h(τ) = (1/mtt) * exp(-τ / mtt)``. We also apply an exponential
|
|
492
|
+
decay term ``exp(-λ τ)``.
|
|
493
|
+
|
|
494
|
+
Parameters
|
|
495
|
+
----------
|
|
496
|
+
tau : ndarray
|
|
497
|
+
Non-negative time axis (same spacing as simulation time grid).
|
|
498
|
+
dt : float
|
|
499
|
+
Time step size of the discretization.
|
|
500
|
+
lambda_ : float
|
|
501
|
+
Decay constant (1 / time units of ``tau``).
|
|
502
|
+
prod : bool, optional
|
|
503
|
+
If True, calculate production response (used to simulate 3He
|
|
504
|
+
production from 3H decay).
|
|
505
|
+
|
|
506
|
+
Returns
|
|
507
|
+
-------
|
|
508
|
+
ndarray
|
|
509
|
+
Impulse response evaluated at ``tau``.
|
|
510
|
+
"""
|
|
511
|
+
# check for edge cases
|
|
512
|
+
if self.mtt <= 0.0:
|
|
513
|
+
return np.zeros_like(tau)
|
|
514
|
+
|
|
515
|
+
# Note: a non-zero response at h[0] corresponds to a response at the
|
|
516
|
+
# same time step as the forcing. This is usually not what we want, so
|
|
517
|
+
# we need to make sure the first time bin is 0.
|
|
518
|
+
|
|
519
|
+
# Base EM shape
|
|
520
|
+
h = np.zeros_like(tau)
|
|
521
|
+
h = (1 / self.mtt) * np.exp(-tau / self.mtt)
|
|
522
|
+
|
|
523
|
+
# get response for constant-input block of length dt
|
|
524
|
+
h = self.get_block(h, tau, dt, lambda_, prod)
|
|
525
|
+
|
|
526
|
+
return h
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@dataclass
|
|
530
|
+
class PMUnit(Unit):
|
|
531
|
+
"""Piston-Flow Model (discrete delta at the mean travel time) with decay.
|
|
532
|
+
|
|
533
|
+
Parameters
|
|
534
|
+
----------
|
|
535
|
+
mtt : float
|
|
536
|
+
Mean travel time where all mass is transported as a plug flow.
|
|
537
|
+
PREFIX : str
|
|
538
|
+
Prefix for local parameter names. Helper for GUI.
|
|
539
|
+
PARAMS : List[Dict[str, Any]]
|
|
540
|
+
List of (default) parameter definitions. Helper for GUI.
|
|
541
|
+
|
|
542
|
+
Note: The parameter key for the mean travel time (``mtt``) is used in the
|
|
543
|
+
GUI explicitly. The GUI assumes the parameter is given in years and
|
|
544
|
+
internally converts it.
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
mtt: float
|
|
548
|
+
PREFIX = "pm"
|
|
549
|
+
# The parameter keys are used explicitly in the GUI! Changing them will lead
|
|
550
|
+
# to the GUI not displaying parameter units properly.
|
|
551
|
+
PARAMS = [
|
|
552
|
+
{"key": "mtt", "label": "Mean Transit Time", "default": 10.0, "bounds": (0.0, 10000.0)},
|
|
553
|
+
]
|
|
554
|
+
|
|
555
|
+
def param_values(self) -> Dict[str, float]:
|
|
556
|
+
"""Get parameter values.
|
|
557
|
+
|
|
558
|
+
Returns
|
|
559
|
+
-------
|
|
560
|
+
Dict[str, float]
|
|
561
|
+
Mapping from local parameter name to value.
|
|
562
|
+
"""
|
|
563
|
+
return {"mtt": float(self.mtt)}
|
|
564
|
+
|
|
565
|
+
def set_param_values(self, values: Dict[str, float]) -> None:
|
|
566
|
+
"""Set local parameter value.
|
|
567
|
+
|
|
568
|
+
Parameters
|
|
569
|
+
----------
|
|
570
|
+
values : Dict[str, float]
|
|
571
|
+
Mapping from local parameter name to new value. Keys not present
|
|
572
|
+
are ignored.
|
|
573
|
+
"""
|
|
574
|
+
if "mtt" in values:
|
|
575
|
+
self.mtt = float(values["mtt"])
|
|
576
|
+
|
|
577
|
+
def get_impulse_response(
|
|
578
|
+
self, tau: np.ndarray, dt: float, lambda_: float, prod: bool = False
|
|
579
|
+
) -> np.ndarray:
|
|
580
|
+
"""Discrete delta response on the grid with exponential decay.
|
|
581
|
+
|
|
582
|
+
The delta is represented by setting the bin at ``round(mtt/dt)`` to
|
|
583
|
+
``1/dt`` to preserve unit mass in the discrete sum.
|
|
584
|
+
|
|
585
|
+
Parameters
|
|
586
|
+
----------
|
|
587
|
+
tau : ndarray
|
|
588
|
+
Non-negative time axis (same spacing as simulation time grid).
|
|
589
|
+
dt : float
|
|
590
|
+
Time step size of the discretization.
|
|
591
|
+
lambda_ : float
|
|
592
|
+
Decay constant (1 / time units of ``tau``).
|
|
593
|
+
prod : bool, optional
|
|
594
|
+
If True, calculate production response (used to simulate 3He
|
|
595
|
+
production from 3H decay).
|
|
596
|
+
|
|
597
|
+
Returns
|
|
598
|
+
-------
|
|
599
|
+
ndarray
|
|
600
|
+
Impulse response evaluated at ``tau``.
|
|
601
|
+
"""
|
|
602
|
+
# Check for edge cases
|
|
603
|
+
if self.mtt <= 0.0:
|
|
604
|
+
return np.zeros_like(tau)
|
|
605
|
+
|
|
606
|
+
# Note: a non-zero response at h[0] corresponds to a response at the
|
|
607
|
+
# same time step as the forcing. This is usually not what we want, so
|
|
608
|
+
# we need to make sure the first time bin is 0.
|
|
609
|
+
|
|
610
|
+
# We don't need to shift the age grid here because we (almost surely)
|
|
611
|
+
# avoid having non-zero response at h[0] anyways for the PM.
|
|
612
|
+
|
|
613
|
+
h = np.zeros_like(tau)
|
|
614
|
+
idx = max(1, int(round(self.mtt / dt)))
|
|
615
|
+
|
|
616
|
+
# Mass is already preserved: before radioactive decay, we set a
|
|
617
|
+
# single time bin to 1/dt. Multiplying by dt and dividing by the
|
|
618
|
+
# sum just gives us 1/dt again.
|
|
619
|
+
if 0 <= idx < len(tau):
|
|
620
|
+
h[idx] = 1.0 / dt
|
|
621
|
+
# radioactive/first-order decay/production
|
|
622
|
+
if prod:
|
|
623
|
+
h[idx] *= 1 - np.exp(-lambda_ * self.mtt)
|
|
624
|
+
else:
|
|
625
|
+
h[idx] *= np.exp(-lambda_ * self.mtt)
|
|
626
|
+
return h
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyTracerLab
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Lumped parameter groundwater age simulations with a simple user interface - in Python
|
|
5
|
+
Author: Max G. Rudolph
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/iGW-TU-Dresden/PyTracerLab
|
|
8
|
+
Project-URL: Documentation, https://iGW-TU-Dresden.github.io/PyTracerLab/
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Hydrology
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy>=1.24
|
|
22
|
+
Requires-Dist: matplotlib>=3.7
|
|
23
|
+
Requires-Dist: scipy>=1.10
|
|
24
|
+
Requires-Dist: PyQt5>=5.15
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest; extra == "dev"
|
|
27
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
28
|
+
Requires-Dist: black; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff; extra == "dev"
|
|
30
|
+
Requires-Dist: pre-commit-hooks; extra == "dev"
|
|
31
|
+
Requires-Dist: mkdocs-material; extra == "dev"
|
|
32
|
+
Requires-Dist: mkdocstrings[python]; extra == "dev"
|
|
33
|
+
Requires-Dist: sphinx; extra == "dev"
|
|
34
|
+
Requires-Dist: furo; extra == "dev"
|
|
35
|
+
Requires-Dist: myst-parser; extra == "dev"
|
|
36
|
+
Requires-Dist: sphinx-autodoc-typehints; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
PyTracerLab/__init__.py,sha256=wqJZyPjhg3CkrN1Sr7-ALpdhCa5r4x9efZNbNg5zaqw,223
|
|
2
|
+
PyTracerLab/__main__.py,sha256=PfT1B9EcIvj6YbEkvqiIFoVngTdhFzWm-l4qFPUbfE8,173
|
|
3
|
+
PyTracerLab/gui/__init__.py,sha256=mdt-HHOLaQzO6rCgedujnRA-2Tbic3C7mwtZIt1ZD9Y,208
|
|
4
|
+
PyTracerLab/gui/app.py,sha256=6o016docAXtuvTbr9a2IdO4qlOq_Bp2lfkmdQ6E1TYA,315
|
|
5
|
+
PyTracerLab/gui/controller.py,sha256=8ILvwNblAMZce7uiiy7kbsxmPNva46L2n5-9eDWUJ1M,16691
|
|
6
|
+
PyTracerLab/gui/database.py,sha256=2WGXLtgB3U_qnwZqDCNb_PCCx1UApTmWaDq1NK1K4fQ,431
|
|
7
|
+
PyTracerLab/gui/main_window.py,sha256=Aro-bTPBqXdzgBKRhbDDdA9CjJO5-F2PpabxJNFDyWE,2834
|
|
8
|
+
PyTracerLab/gui/state.py,sha256=d_s9tR3gliKfNjLlGVX3wiyeb17wOpTRZTEMVXNftA0,4664
|
|
9
|
+
PyTracerLab/gui/tabs/file_input.py,sha256=AwV8GOHms2JwmeI75c4s9WYLI0qR5QJprvzMZFXVJoc,16488
|
|
10
|
+
PyTracerLab/gui/tabs/model_design.py,sha256=sl7fX4vtFYpzT8N2fOcofR-BlHa4z-tFg3a2bbvAWOE,11654
|
|
11
|
+
PyTracerLab/gui/tabs/parameters.py,sha256=KFSGEfj2ySup-NKIk9mK6tFFYH4ndfpgjQTVzmRRcQk,4647
|
|
12
|
+
PyTracerLab/gui/tabs/simulation.py,sha256=_eAAV0YHFSXAuqI-T2EsnZUz27S2-vkZMibH9meqia8,10974
|
|
13
|
+
PyTracerLab/gui/tabs/solver_params.py,sha256=VS4-EHK171CM5SIgrmdwiKr9rQzlbjOgOQu8sG8kWlM,26966
|
|
14
|
+
PyTracerLab/gui/tabs/tracer_tracer.py,sha256=BhZGsXJFMaHF492_HU-c0Uq2WYQHJdZdRe0JQc65ow4,11974
|
|
15
|
+
PyTracerLab/gui/tabs/widgets.py,sha256=xx9Jtkx59C_RQuXGVOWMi8_2hlPQHoZpgNxlLE7SACU,4019
|
|
16
|
+
PyTracerLab/model/__init__.py,sha256=Ook-7H4M-2q6uHlrYrTp68-4Hox2XiBvXQp_teCXGmI,1426
|
|
17
|
+
PyTracerLab/model/model.py,sha256=c3TY65vzTcdabCNZmNTaGAwP8zxVLBsy3ktVdXrBpJw,27743
|
|
18
|
+
PyTracerLab/model/registry.py,sha256=xUnnktqY4LFGLg8mwNS2bwZ79IhVwjObiNAutRS9tpw,828
|
|
19
|
+
PyTracerLab/model/solver.py,sha256=xKx8CsjbWpGrBq2Fnj2RdpNqzJYyi7xJOihz4qHdw-4,50819
|
|
20
|
+
PyTracerLab/model/units.py,sha256=UpGbbOFsoP1ooizKPFmSpahnnF6vGlq4kL5XoybfEk0,20949
|
|
21
|
+
pytracerlab-0.2.0.dist-info/licenses/LICENSE,sha256=yEpJ3xwftS_OOYaa_VkfGzZwP2Qxp7XWfI9zHwWXYjI,1080
|
|
22
|
+
pytracerlab-0.2.0.dist-info/METADATA,sha256=eY9VzFkMVKyLm0aaCtSK1__-ObtbhcF2tYx-dN-mK5E,1494
|
|
23
|
+
pytracerlab-0.2.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
24
|
+
pytracerlab-0.2.0.dist-info/entry_points.txt,sha256=RLc_5LOvpI6yYLmtr2WPwepke-dXdPZISFR9L40nzC8,115
|
|
25
|
+
pytracerlab-0.2.0.dist-info/top_level.txt,sha256=49vsBEZgWxSXH-u_dF2MX8qJPreibUp49FCCGEwqvF8,12
|
|
26
|
+
pytracerlab-0.2.0.dist-info/RECORD,,
|