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.
Files changed (60) hide show
  1. paravis/__init__.py +25 -0
  2. paravis/__version__.py +4 -0
  3. paravis/api/__init__.py +18 -0
  4. paravis/api/indices.py +116 -0
  5. paravis/api/raoq.py +105 -0
  6. paravis/api/visualization.py +95 -0
  7. paravis/core/__init__.py +8 -0
  8. paravis/core/indices/__init__.py +28 -0
  9. paravis/core/indices/constants.py +52 -0
  10. paravis/core/indices/engine.py +334 -0
  11. paravis/core/indices/models.py +53 -0
  12. paravis/core/indices/registry.py +73 -0
  13. paravis/core/models.py +26 -0
  14. paravis/core/raoq/__init__.py +11 -0
  15. paravis/core/raoq/engine.py +485 -0
  16. paravis/core/raoq/gpu.py +639 -0
  17. paravis/core/raoq/models.py +35 -0
  18. paravis/core/raster/__init__.py +14 -0
  19. paravis/core/raster/reader.py +126 -0
  20. paravis/core/raster/utils.py +56 -0
  21. paravis/core/raster/writer.py +61 -0
  22. paravis/gui/__init__.py +10 -0
  23. paravis/gui/app.py +60 -0
  24. paravis/gui/components/__init__.py +1 -0
  25. paravis/gui/components/mpl_canvas.py +28 -0
  26. paravis/gui/components/recent_files.py +67 -0
  27. paravis/gui/components/splash.py +108 -0
  28. paravis/gui/components/workers.py +126 -0
  29. paravis/gui/dialogs/__init__.py +1 -0
  30. paravis/gui/dialogs/about.py +100 -0
  31. paravis/gui/dialogs/band_mapping.py +178 -0
  32. paravis/gui/dialogs/constants_editor.py +122 -0
  33. paravis/gui/dialogs/index_table.py +228 -0
  34. paravis/gui/dialogs/settings.py +136 -0
  35. paravis/gui/main_window.py +430 -0
  36. paravis/gui/models/__init__.py +1 -0
  37. paravis/gui/models/delegates.py +34 -0
  38. paravis/gui/models/index_table_model.py +129 -0
  39. paravis/gui/models/proxy_model.py +37 -0
  40. paravis/gui/theme/dark_teal.qss +437 -0
  41. paravis/gui/theme/light_teal.qss +474 -0
  42. paravis/gui/theme/logo.png +0 -0
  43. paravis/gui/widgets/__init__.py +1 -0
  44. paravis/gui/widgets/indices_panel.py +774 -0
  45. paravis/gui/widgets/raoq_panel.py +946 -0
  46. paravis/gui/widgets/viz_panel.py +1646 -0
  47. paravis/utils/__init__.py +14 -0
  48. paravis/utils/logging.py +67 -0
  49. paravis/utils/settings.py +100 -0
  50. paravis/utils/system.py +247 -0
  51. paravis/workers/__init__.py +7 -0
  52. paravis/workers/base_worker.py +57 -0
  53. paravis/workers/index_worker.py +525 -0
  54. paravis/workers/raoq_worker.py +218 -0
  55. paravis-2.0.0.dist-info/METADATA +251 -0
  56. paravis-2.0.0.dist-info/RECORD +60 -0
  57. paravis-2.0.0.dist-info/WHEEL +5 -0
  58. paravis-2.0.0.dist-info/entry_points.txt +2 -0
  59. paravis-2.0.0.dist-info/licenses/LICENSE +21 -0
  60. 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
@@ -0,0 +1,4 @@
1
+ """Version information for PaRaVis."""
2
+
3
+ __version__ = "2.0.0"
4
+ __version_info__ = (2, 0, 0)
@@ -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
@@ -0,0 +1,8 @@
1
+ """
2
+ paravis.core — Pure computation modules (zero Qt dependency).
3
+
4
+ Sub-packages:
5
+ core.indices — Spectral index computation engine
6
+ core.raoq — Rao's Q diversity computation (CPU + GPU)
7
+ core.raster — Raster I/O utilities
8
+ """
@@ -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