paravis 2.0.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.
- paravis/__init__.py +25 -0
- paravis/__version__.py +4 -0
- paravis/api/__init__.py +18 -0
- paravis/api/indices.py +116 -0
- paravis/api/raoq.py +105 -0
- paravis/api/visualization.py +95 -0
- paravis/core/__init__.py +8 -0
- paravis/core/indices/__init__.py +28 -0
- paravis/core/indices/constants.py +52 -0
- paravis/core/indices/engine.py +334 -0
- paravis/core/indices/models.py +53 -0
- paravis/core/indices/registry.py +73 -0
- paravis/core/models.py +26 -0
- paravis/core/raoq/__init__.py +11 -0
- paravis/core/raoq/engine.py +485 -0
- paravis/core/raoq/gpu.py +639 -0
- paravis/core/raoq/models.py +35 -0
- paravis/core/raster/__init__.py +14 -0
- paravis/core/raster/reader.py +126 -0
- paravis/core/raster/utils.py +56 -0
- paravis/core/raster/writer.py +61 -0
- paravis/gui/__init__.py +10 -0
- paravis/gui/app.py +60 -0
- paravis/gui/components/__init__.py +1 -0
- paravis/gui/components/mpl_canvas.py +28 -0
- paravis/gui/components/recent_files.py +67 -0
- paravis/gui/components/splash.py +108 -0
- paravis/gui/components/workers.py +126 -0
- paravis/gui/dialogs/__init__.py +1 -0
- paravis/gui/dialogs/about.py +100 -0
- paravis/gui/dialogs/band_mapping.py +178 -0
- paravis/gui/dialogs/constants_editor.py +122 -0
- paravis/gui/dialogs/index_table.py +228 -0
- paravis/gui/dialogs/settings.py +136 -0
- paravis/gui/main_window.py +430 -0
- paravis/gui/models/__init__.py +1 -0
- paravis/gui/models/delegates.py +34 -0
- paravis/gui/models/index_table_model.py +129 -0
- paravis/gui/models/proxy_model.py +37 -0
- paravis/gui/theme/dark_teal.qss +437 -0
- paravis/gui/theme/light_teal.qss +474 -0
- paravis/gui/theme/logo.png +0 -0
- paravis/gui/widgets/__init__.py +1 -0
- paravis/gui/widgets/indices_panel.py +774 -0
- paravis/gui/widgets/raoq_panel.py +946 -0
- paravis/gui/widgets/viz_panel.py +1646 -0
- paravis/utils/__init__.py +14 -0
- paravis/utils/logging.py +67 -0
- paravis/utils/settings.py +100 -0
- paravis/utils/system.py +247 -0
- paravis/workers/__init__.py +7 -0
- paravis/workers/base_worker.py +57 -0
- paravis/workers/index_worker.py +525 -0
- paravis/workers/raoq_worker.py +218 -0
- paravis-2.0.0.dist-info/METADATA +251 -0
- paravis-2.0.0.dist-info/RECORD +60 -0
- paravis-2.0.0.dist-info/WHEEL +5 -0
- paravis-2.0.0.dist-info/entry_points.txt +2 -0
- paravis-2.0.0.dist-info/licenses/LICENSE +21 -0
- paravis-2.0.0.dist-info/top_level.txt +1 -0
paravis/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PaRaVis — Parallel Rao's Q Visualization and Analysis
|
|
3
|
+
======================================================
|
|
4
|
+
|
|
5
|
+
A library for spectral index computation, Rao's Q diversity analysis,
|
|
6
|
+
and raster data visualization.
|
|
7
|
+
|
|
8
|
+
Main components:
|
|
9
|
+
paravis.api — Public API for headless/script usage
|
|
10
|
+
paravis.core — Core computation (no Qt dependency)
|
|
11
|
+
paravis.gui — PySide6 desktop application
|
|
12
|
+
paravis.workers — Background processing threads
|
|
13
|
+
paravis.utils — Shared utilities (settings, system profiler)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .__version__ import __version__, __version_info__ # noqa: F401
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def __getattr__(name):
|
|
20
|
+
"""Lazy import of public API to avoid circular imports at package level."""
|
|
21
|
+
if name in ("compute_indices", "compute_rao_q", "plot_raster", "plot_comparison"):
|
|
22
|
+
import paravis.api as _api
|
|
23
|
+
return getattr(_api, name)
|
|
24
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
25
|
+
|
paravis/__version__.py
ADDED
paravis/api/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
paravis.api — Public API for headless/script usage.
|
|
3
|
+
|
|
4
|
+
Convenience wrappers around paravis.core that provide sensible defaults
|
|
5
|
+
and return NumPy arrays or xarray-compatible results.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .indices import compute_indices, list_available_indices
|
|
9
|
+
from .raoq import compute_rao_q
|
|
10
|
+
from .visualization import plot_raster, plot_comparison
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"compute_indices",
|
|
14
|
+
"list_available_indices",
|
|
15
|
+
"compute_rao_q",
|
|
16
|
+
"plot_raster",
|
|
17
|
+
"plot_comparison",
|
|
18
|
+
]
|
paravis/api/indices.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public API for spectral index computation.
|
|
3
|
+
|
|
4
|
+
Convenience wrappers around paravis.core.indices with sensible defaults.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from paravis.core.raster import read_raster
|
|
11
|
+
from paravis.core.indices import (
|
|
12
|
+
compute_index as _compute_index,
|
|
13
|
+
compute_indices as _compute_indices,
|
|
14
|
+
get_available_indices,
|
|
15
|
+
is_index_computable,
|
|
16
|
+
get_default_band_mapping,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def list_available_indices() -> List[str]:
|
|
21
|
+
"""List all available spectral index names.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
List[str]
|
|
26
|
+
"""
|
|
27
|
+
return [idx.name for idx in get_available_indices()]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compute_indices(
|
|
31
|
+
raster_path: str,
|
|
32
|
+
indices: Optional[List[str]] = None,
|
|
33
|
+
band_mapping: Optional[Dict[int, str]] = None,
|
|
34
|
+
constants: Optional[Dict[str, float]] = None,
|
|
35
|
+
max_pixels: int = 1_000_000,
|
|
36
|
+
) -> Dict[str, np.ndarray]:
|
|
37
|
+
"""Compute spectral indices from a raster file.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
raster_path : str
|
|
42
|
+
Path to a multi-band GeoTIFF.
|
|
43
|
+
indices : List[str], optional
|
|
44
|
+
Names of indices to compute. If None, computes all computable ones.
|
|
45
|
+
band_mapping : Dict[int, str], optional
|
|
46
|
+
Mapping from band number to spectral code.
|
|
47
|
+
Defaults to Landsat 8/9 mapping.
|
|
48
|
+
constants : Dict[str, float], optional
|
|
49
|
+
Override constants for index computation.
|
|
50
|
+
max_pixels : int
|
|
51
|
+
Maximum pixels to read (auto-downsamples beyond this).
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
Dict[str, np.ndarray]
|
|
56
|
+
Mapping from index name to 2D result array.
|
|
57
|
+
"""
|
|
58
|
+
if band_mapping is None:
|
|
59
|
+
band_mapping = get_default_band_mapping()
|
|
60
|
+
|
|
61
|
+
# Read raster
|
|
62
|
+
data_3d, transform, crs = read_raster(raster_path, max_pixels=max_pixels)
|
|
63
|
+
|
|
64
|
+
# Ensure 3D
|
|
65
|
+
if data_3d.ndim == 2:
|
|
66
|
+
data_3d = data_3d[np.newaxis, :, :]
|
|
67
|
+
|
|
68
|
+
# Determine which indices to compute
|
|
69
|
+
if indices is None:
|
|
70
|
+
all_indices = get_available_indices()
|
|
71
|
+
idx_constants = constants or {}
|
|
72
|
+
indices = [
|
|
73
|
+
idx.name for idx in all_indices
|
|
74
|
+
if is_index_computable(idx.name, idx_constants, band_mapping)
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# Compute
|
|
78
|
+
results = _compute_indices(data_3d, band_mapping, indices, constants)
|
|
79
|
+
return results
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def compute_index(
|
|
83
|
+
raster_path: str,
|
|
84
|
+
index_name: str,
|
|
85
|
+
band_mapping: Optional[Dict[int, str]] = None,
|
|
86
|
+
constants: Optional[Dict[str, float]] = None,
|
|
87
|
+
max_pixels: int = 1_000_000,
|
|
88
|
+
) -> np.ndarray:
|
|
89
|
+
"""Compute a single spectral index from a raster file.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
raster_path : str
|
|
94
|
+
Path to a multi-band GeoTIFF.
|
|
95
|
+
index_name : str
|
|
96
|
+
Name of the index (e.g. 'NDVI').
|
|
97
|
+
band_mapping : Dict[int, str], optional
|
|
98
|
+
Band mapping. Defaults to Landsat 8/9.
|
|
99
|
+
constants : Dict[str, float], optional
|
|
100
|
+
Override constants.
|
|
101
|
+
max_pixels : int
|
|
102
|
+
Maximum pixels to read.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
np.ndarray
|
|
107
|
+
2D array of the computed index.
|
|
108
|
+
"""
|
|
109
|
+
if band_mapping is None:
|
|
110
|
+
band_mapping = get_default_band_mapping()
|
|
111
|
+
|
|
112
|
+
data_3d, _, _ = read_raster(raster_path, max_pixels=max_pixels)
|
|
113
|
+
if data_3d.ndim == 2:
|
|
114
|
+
data_3d = data_3d[np.newaxis, :, :]
|
|
115
|
+
|
|
116
|
+
return _compute_index(data_3d, band_mapping, index_name, constants)
|
paravis/api/raoq.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public API for Rao's Q diversity computation.
|
|
3
|
+
|
|
4
|
+
Implements the ecological Rao's quadratic entropy formula:
|
|
5
|
+
|
|
6
|
+
Q = Σᵢ Σⱼ dᵢⱼ × pᵢ × pⱼ
|
|
7
|
+
|
|
8
|
+
where each unique spectral profile in a window is treated as a
|
|
9
|
+
"species" (i), dᵢⱼ is the spectral distance between species i and j,
|
|
10
|
+
and pᵢ, pⱼ are their relative abundances (proportion of pixels
|
|
11
|
+
belonging to each unique profile).
|
|
12
|
+
"""
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from paravis.core.raster import read_raster
|
|
18
|
+
from paravis.core.raoq import (
|
|
19
|
+
compute_rao_q as _compute_rao_q_cpu,
|
|
20
|
+
RaoQConfig,
|
|
21
|
+
RaoQResult,
|
|
22
|
+
)
|
|
23
|
+
from paravis.core.raoq.gpu import compute_rao_q_gpu, is_gpu_available
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def compute_rao_q(
|
|
27
|
+
raster_path: str,
|
|
28
|
+
window_size: int = 15,
|
|
29
|
+
step_size: int = 1,
|
|
30
|
+
na_tolerance: float = 0.3,
|
|
31
|
+
backend: str = "auto",
|
|
32
|
+
n_workers: int = 4,
|
|
33
|
+
max_pixels: int = 500_000,
|
|
34
|
+
simplify: int = 2,
|
|
35
|
+
distance_metric: str = "euclidean",
|
|
36
|
+
p_minkowski: int = 2,
|
|
37
|
+
) -> np.ndarray:
|
|
38
|
+
"""Compute Rao's Q diversity from a raster file.
|
|
39
|
+
|
|
40
|
+
For each moving window, unique spectral profiles are identified
|
|
41
|
+
as "species", and Rao's Q is computed as:
|
|
42
|
+
|
|
43
|
+
Q = Σᵢ Σⱼ dᵢⱼ × pᵢ × pⱼ
|
|
44
|
+
|
|
45
|
+
where pᵢ is the relative abundance (frequency) of each unique
|
|
46
|
+
spectral profile in the window.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
raster_path : str
|
|
51
|
+
Path to a multi-band GeoTIFF.
|
|
52
|
+
window_size : int
|
|
53
|
+
Size of the moving window (odd number).
|
|
54
|
+
step_size : int
|
|
55
|
+
Step between windows (default 1).
|
|
56
|
+
na_tolerance : float
|
|
57
|
+
Maximum allowed fraction of NA pixels (0.0 to 1.0).
|
|
58
|
+
backend : str
|
|
59
|
+
'auto', 'cpu', 'gpu', or 'parallel'. If 'auto', tries GPU first.
|
|
60
|
+
n_workers : int
|
|
61
|
+
Number of CPU workers for 'parallel' backend.
|
|
62
|
+
max_pixels : int
|
|
63
|
+
Maximum pixels to read (auto-downsamples beyond this).
|
|
64
|
+
simplify : int
|
|
65
|
+
Number of decimal places to round the output to (0 = integers, default 2).
|
|
66
|
+
distance_metric : str
|
|
67
|
+
Distance metric: 'euclidean', 'manhattan', 'chebyshev', or 'minkowski'.
|
|
68
|
+
p_minkowski : int
|
|
69
|
+
Exponent for Minkowski distance (ignored for other metrics).
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
np.ndarray
|
|
74
|
+
2D array of Rao's Q diversity values.
|
|
75
|
+
"""
|
|
76
|
+
config = RaoQConfig(
|
|
77
|
+
window_size=window_size,
|
|
78
|
+
step_size=step_size,
|
|
79
|
+
na_tolerance=na_tolerance,
|
|
80
|
+
n_workers=n_workers,
|
|
81
|
+
simplify=simplify,
|
|
82
|
+
distance_metric=distance_metric,
|
|
83
|
+
p_minkowski=p_minkowski,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Read raster
|
|
87
|
+
data_3d, _, _ = read_raster(raster_path, max_pixels=max_pixels)
|
|
88
|
+
if data_3d.ndim == 2:
|
|
89
|
+
data_3d = data_3d[np.newaxis, :, :]
|
|
90
|
+
|
|
91
|
+
# Choose backend
|
|
92
|
+
if backend == "auto":
|
|
93
|
+
if is_gpu_available():
|
|
94
|
+
result = compute_rao_q_gpu(data_3d, config)
|
|
95
|
+
else:
|
|
96
|
+
result = _compute_rao_q_cpu(data_3d, config)
|
|
97
|
+
elif backend == "gpu":
|
|
98
|
+
result = compute_rao_q_gpu(data_3d, config)
|
|
99
|
+
elif backend == "parallel":
|
|
100
|
+
from paravis.core.raoq import compute_rao_q_parallel
|
|
101
|
+
result = compute_rao_q_parallel(data_3d, config)
|
|
102
|
+
else:
|
|
103
|
+
result = _compute_rao_q_cpu(data_3d, config)
|
|
104
|
+
|
|
105
|
+
return result
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public API for raster visualization.
|
|
3
|
+
|
|
4
|
+
Provides quick-plot functions for use in scripts and notebooks.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Optional, List, Tuple
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from paravis.core.raster import read_raster, normalize_data
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def plot_raster(
|
|
13
|
+
raster_path: str,
|
|
14
|
+
title: Optional[str] = None,
|
|
15
|
+
cmap: str = "viridis",
|
|
16
|
+
figsize: Tuple[int, int] = (10, 8),
|
|
17
|
+
max_pixels: int = 1_000_000,
|
|
18
|
+
show_colorbar: bool = True,
|
|
19
|
+
):
|
|
20
|
+
"""Quick-plot a raster file.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
raster_path : str
|
|
25
|
+
Path to a raster file.
|
|
26
|
+
title : str, optional
|
|
27
|
+
Plot title.
|
|
28
|
+
cmap : str
|
|
29
|
+
Matplotlib colormap name.
|
|
30
|
+
figsize : Tuple[int, int]
|
|
31
|
+
Figure size in inches.
|
|
32
|
+
max_pixels : int
|
|
33
|
+
Maximum pixels to read.
|
|
34
|
+
show_colorbar : bool
|
|
35
|
+
Whether to show a colorbar.
|
|
36
|
+
"""
|
|
37
|
+
import matplotlib.pyplot as plt
|
|
38
|
+
|
|
39
|
+
data, transform, _ = read_raster(raster_path, max_pixels=max_pixels)
|
|
40
|
+
if data.ndim == 3:
|
|
41
|
+
data = data[0]
|
|
42
|
+
|
|
43
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
44
|
+
im = ax.imshow(data, cmap=cmap, interpolation="nearest")
|
|
45
|
+
if show_colorbar:
|
|
46
|
+
plt.colorbar(im, ax=ax, shrink=0.75)
|
|
47
|
+
ax.set_title(title or raster_path.split("/")[-1])
|
|
48
|
+
ax.set_xlabel("Column")
|
|
49
|
+
ax.set_ylabel("Row")
|
|
50
|
+
plt.tight_layout()
|
|
51
|
+
return fig, ax
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def plot_comparison(
|
|
55
|
+
file_paths: List[str],
|
|
56
|
+
labels: Optional[List[str]] = None,
|
|
57
|
+
cmap: str = "viridis",
|
|
58
|
+
figsize: Optional[Tuple[int, int]] = None,
|
|
59
|
+
max_pixels: int = 500_000,
|
|
60
|
+
):
|
|
61
|
+
"""Plot multiple rasters side by side for comparison.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
file_paths : List[str]
|
|
66
|
+
Paths to raster files.
|
|
67
|
+
labels : List[str], optional
|
|
68
|
+
Labels for each subplot.
|
|
69
|
+
cmap : str
|
|
70
|
+
Colormap name.
|
|
71
|
+
figsize : Tuple[int, int], optional
|
|
72
|
+
Figure size.
|
|
73
|
+
max_pixels : int
|
|
74
|
+
Maximum pixels to read.
|
|
75
|
+
"""
|
|
76
|
+
import matplotlib.pyplot as plt
|
|
77
|
+
|
|
78
|
+
n = len(file_paths)
|
|
79
|
+
if figsize is None:
|
|
80
|
+
figsize = (5 * n, 5)
|
|
81
|
+
|
|
82
|
+
fig, axes = plt.subplots(1, n, figsize=figsize, squeeze=False)
|
|
83
|
+
if labels is None:
|
|
84
|
+
labels = [p.split("/")[-1] for p in file_paths]
|
|
85
|
+
|
|
86
|
+
for i, (path, label) in enumerate(zip(file_paths, labels)):
|
|
87
|
+
data, _, _ = read_raster(path, max_pixels=max_pixels)
|
|
88
|
+
if data.ndim == 3:
|
|
89
|
+
data = data[0]
|
|
90
|
+
im = axes[0, i].imshow(data, cmap=cmap, interpolation="nearest")
|
|
91
|
+
axes[0, i].set_title(label)
|
|
92
|
+
plt.colorbar(im, ax=axes[0, i], shrink=0.75)
|
|
93
|
+
|
|
94
|
+
plt.tight_layout()
|
|
95
|
+
return fig, axes
|
paravis/core/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Spectral index computation engine."""
|
|
2
|
+
|
|
3
|
+
from .engine import (
|
|
4
|
+
compute_index,
|
|
5
|
+
compute_indices,
|
|
6
|
+
compute_indices_dask,
|
|
7
|
+
is_index_computable,
|
|
8
|
+
get_available_indices,
|
|
9
|
+
get_default_band_mapping,
|
|
10
|
+
)
|
|
11
|
+
from .registry import register_index, list_custom_indices
|
|
12
|
+
from .constants import get_default_constants, merge_constants
|
|
13
|
+
from .models import SpectralIndex, BandMapping
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"compute_index",
|
|
17
|
+
"compute_indices",
|
|
18
|
+
"compute_indices_dask",
|
|
19
|
+
"is_index_computable",
|
|
20
|
+
"get_available_indices",
|
|
21
|
+
"get_default_band_mapping",
|
|
22
|
+
"register_index",
|
|
23
|
+
"list_custom_indices",
|
|
24
|
+
"get_default_constants",
|
|
25
|
+
"merge_constants",
|
|
26
|
+
"SpectralIndex",
|
|
27
|
+
"BandMapping",
|
|
28
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants management for spectral index computation.
|
|
3
|
+
|
|
4
|
+
Handles default values from spyndex and user overrides.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
|
|
8
|
+
import spyndex
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_default_constants() -> Dict[str, float]:
|
|
12
|
+
"""Return all default constant values from spyndex.
|
|
13
|
+
|
|
14
|
+
Returns
|
|
15
|
+
-------
|
|
16
|
+
Dict[str, float]
|
|
17
|
+
Mapping from constant name to default value.
|
|
18
|
+
"""
|
|
19
|
+
constants: Dict[str, float] = {}
|
|
20
|
+
for name, const in spyndex.constants.items():
|
|
21
|
+
try:
|
|
22
|
+
constants[name] = float(getattr(const, "default", 0.0))
|
|
23
|
+
except (ValueError, TypeError):
|
|
24
|
+
constants[name] = 0.0
|
|
25
|
+
return constants
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def merge_constants(
|
|
29
|
+
defaults: Dict[str, float],
|
|
30
|
+
overrides: Dict[str, Optional[float]],
|
|
31
|
+
) -> Dict[str, float]:
|
|
32
|
+
"""Merge user overrides into default constants.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
defaults : Dict[str, float]
|
|
37
|
+
Default constant values.
|
|
38
|
+
overrides : Dict[str, Optional[float]]
|
|
39
|
+
User overrides. A value of None removes the override (fallback to default).
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
Dict[str, float]
|
|
44
|
+
Merged constants dictionary.
|
|
45
|
+
"""
|
|
46
|
+
merged = dict(defaults)
|
|
47
|
+
for key, value in overrides.items():
|
|
48
|
+
if value is not None:
|
|
49
|
+
merged[key] = value
|
|
50
|
+
elif key in merged:
|
|
51
|
+
del merged[key]
|
|
52
|
+
return merged
|