lsdviztools3 0.4.1__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.
- lsdviztools3/__init__.py +12 -0
- lsdviztools3/_gmt.py +48 -0
- lsdviztools3/charts/__init__.py +214 -0
- lsdviztools3/charts/burned_profiles.py +270 -0
- lsdviztools3/charts/channel_bearings.py +988 -0
- lsdviztools3/charts/channel_slopes.py +245 -0
- lsdviztools3/charts/chi.py +501 -0
- lsdviztools3/charts/circular.py +231 -0
- lsdviztools3/charts/cosmo_comparison.py +208 -0
- lsdviztools3/charts/cosmo_transient.py +437 -0
- lsdviztools3/charts/cosmo_transient_spatial.py +549 -0
- lsdviztools3/charts/cross_sections.py +1449 -0
- lsdviztools3/charts/distributions.py +120 -0
- lsdviztools3/charts/divide_migration.py +398 -0
- lsdviztools3/charts/erodibility_calibration.py +220 -0
- lsdviztools3/charts/estar_rstar.py +250 -0
- lsdviztools3/charts/floodplain_qq.py +91 -0
- lsdviztools3/charts/gradient_curvature.py +253 -0
- lsdviztools3/charts/hand.py +939 -0
- lsdviztools3/charts/hillslope_profiles.py +187 -0
- lsdviztools3/charts/hillslope_residuals.py +102 -0
- lsdviztools3/charts/hillslope_transient_residuals.py +87 -0
- lsdviztools3/charts/hilltop_coupling.py +691 -0
- lsdviztools3/charts/hydraulic_breaks.py +555 -0
- lsdviztools3/charts/knickpoints.py +395 -0
- lsdviztools3/charts/ladder_band_map.py +108 -0
- lsdviztools3/charts/ladder_density.py +149 -0
- lsdviztools3/charts/ladder_windows.py +187 -0
- lsdviztools3/charts/landslides.py +197 -0
- lsdviztools3/charts/lithology_ksn.py +683 -0
- lsdviztools3/charts/network_geometry.py +471 -0
- lsdviztools3/charts/network_maps.py +255 -0
- lsdviztools3/charts/perimeter_sources.py +379 -0
- lsdviztools3/charts/ridgeline.py +736 -0
- lsdviztools3/charts/rose.py +148 -0
- lsdviztools3/charts/slope_area.py +306 -0
- lsdviztools3/charts/snapping.py +223 -0
- lsdviztools3/charts/spectral.py +419 -0
- lsdviztools3/charts/surface_methods.py +124 -0
- lsdviztools3/charts/swath.py +99 -0
- lsdviztools3/charts/training_polygons.py +108 -0
- lsdviztools3/charts/valley_section_map.py +151 -0
- lsdviztools3/charts/valley_sections.py +309 -0
- lsdviztools3/charts/valley_spacing.py +302 -0
- lsdviztools3/charts/valley_width_network.py +610 -0
- lsdviztools3/cli/__init__.py +26 -0
- lsdviztools3/cli/_chi_common.py +74 -0
- lsdviztools3/cli/_common.py +154 -0
- lsdviztools3/cli/_ridgeline.py +199 -0
- lsdviztools3/cli/grab_dem.py +78 -0
- lsdviztools3/cli/plot_animation.py +82 -0
- lsdviztools3/cli/plot_basemap.py +86 -0
- lsdviztools3/cli/plot_basins.py +136 -0
- lsdviztools3/cli/plot_burned_profiles.py +77 -0
- lsdviztools3/cli/plot_channel_bearings.py +299 -0
- lsdviztools3/cli/plot_channel_slopes.py +91 -0
- lsdviztools3/cli/plot_channels.py +69 -0
- lsdviztools3/cli/plot_chi.py +112 -0
- lsdviztools3/cli/plot_chi_cosmo.py +109 -0
- lsdviztools3/cli/plot_cosmo_basins.py +134 -0
- lsdviztools3/cli/plot_cosmo_compare.py +81 -0
- lsdviztools3/cli/plot_cosmo_published.py +64 -0
- lsdviztools3/cli/plot_cosmo_transient.py +88 -0
- lsdviztools3/cli/plot_divide_migration.py +137 -0
- lsdviztools3/cli/plot_drape.py +171 -0
- lsdviztools3/cli/plot_erodibility_calibration.py +150 -0
- lsdviztools3/cli/plot_estar_rstar.py +83 -0
- lsdviztools3/cli/plot_floodplain.py +140 -0
- lsdviztools3/cli/plot_hillshade.py +69 -0
- lsdviztools3/cli/plot_hillslope_comparison.py +42 -0
- lsdviztools3/cli/plot_hilltop_coupling.py +148 -0
- lsdviztools3/cli/plot_knickpoints.py +140 -0
- lsdviztools3/cli/plot_ksn_by_lithology.py +117 -0
- lsdviztools3/cli/plot_ladder_band_map.py +50 -0
- lsdviztools3/cli/plot_ladder_density.py +64 -0
- lsdviztools3/cli/plot_ladder_windows.py +67 -0
- lsdviztools3/cli/plot_lem_ci_animation.py +90 -0
- lsdviztools3/cli/plot_lem_pulse.py +150 -0
- lsdviztools3/cli/plot_lithology_map.py +235 -0
- lsdviztools3/cli/plot_mdd_ridgeline.py +69 -0
- lsdviztools3/cli/plot_perimeter_sources.py +74 -0
- lsdviztools3/cli/plot_ridgeline_curvature.py +74 -0
- lsdviztools3/cli/plot_ridgeline_spacing.py +147 -0
- lsdviztools3/cli/plot_ridgeline_summary.py +115 -0
- lsdviztools3/cli/plot_sti.py +237 -0
- lsdviztools3/cli/plot_surface_methods.py +83 -0
- lsdviztools3/cli/plot_valley_section_map.py +66 -0
- lsdviztools3/cli/plot_valley_sections.py +95 -0
- lsdviztools3/cli/plot_valley_widths.py +131 -0
- lsdviztools3/cli/run_lsdtt.py +88 -0
- lsdviztools3/colour_limits.py +64 -0
- lsdviztools3/download/__init__.py +1 -0
- lsdviztools3/download/apikey.py +69 -0
- lsdviztools3/download/ot_scraper.py +167 -0
- lsdviztools3/driver/__init__.py +19 -0
- lsdviztools3/driver/config.py +226 -0
- lsdviztools3/driver/lsdtt_driver.py +123 -0
- lsdviztools3/frames.py +122 -0
- lsdviztools3/io/__init__.py +6 -0
- lsdviztools3/io/channel_bearings.py +442 -0
- lsdviztools3/io/chi.py +336 -0
- lsdviztools3/io/coverage.py +118 -0
- lsdviztools3/io/divide_migration.py +368 -0
- lsdviztools3/io/erodibility_calibration.py +204 -0
- lsdviztools3/io/grids.py +40 -0
- lsdviztools3/io/hilltop_coupling.py +272 -0
- lsdviztools3/io/lithology.py +696 -0
- lsdviztools3/io/network_geometry.py +428 -0
- lsdviztools3/io/raster.py +196 -0
- lsdviztools3/io/ridgeline.py +528 -0
- lsdviztools3/io/snapping.py +434 -0
- lsdviztools3/io/spectral.py +347 -0
- lsdviztools3/io/swath.py +92 -0
- lsdviztools3/io/table.py +54 -0
- lsdviztools3/io/valley_spacing.py +171 -0
- lsdviztools3/io/vector.py +91 -0
- lsdviztools3/paths.py +53 -0
- lsdviztools3/regions.py +155 -0
- lsdviztools3/render/__init__.py +15 -0
- lsdviztools3/render/animation.py +349 -0
- lsdviztools3/render/base.py +199 -0
- lsdviztools3/render/basemap.py +165 -0
- lsdviztools3/render/basins.py +112 -0
- lsdviztools3/render/categorical.py +490 -0
- lsdviztools3/render/channels.py +360 -0
- lsdviztools3/render/colormaps.py +144 -0
- lsdviztools3/render/decorations.py +505 -0
- lsdviztools3/render/divide_migration.py +188 -0
- lsdviztools3/render/erodibility_calibration_map.py +193 -0
- lsdviztools3/render/floodplain.py +176 -0
- lsdviztools3/render/graticule.py +187 -0
- lsdviztools3/render/hillshade.py +115 -0
- lsdviztools3/render/hillslope_comparison_animation.py +352 -0
- lsdviztools3/render/lem_ci_animation.py +342 -0
- lsdviztools3/render/lem_pulse.py +522 -0
- lsdviztools3/render/lithology_colours.py +124 -0
- lsdviztools3/render/lithology_map.py +668 -0
- lsdviztools3/render/points.py +149 -0
- lsdviztools3/render/relief3d.py +152 -0
- lsdviztools3/render/ridgeline.py +225 -0
- lsdviztools3/render/sti.py +54 -0
- lsdviztools3/render/style.py +74 -0
- lsdviztools3/render/surface_methods.py +184 -0
- lsdviztools3/workflows.py +2713 -0
- lsdviztools3-0.4.1.dist-info/METADATA +367 -0
- lsdviztools3-0.4.1.dist-info/RECORD +149 -0
- lsdviztools3-0.4.1.dist-info/WHEEL +4 -0
- lsdviztools3-0.4.1.dist-info/entry_points.txt +42 -0
- lsdviztools3-0.4.1.dist-info/licenses/LICENSE +21 -0
lsdviztools3/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""lsdviztools3 — PyGMT-based visualisation tools for LSDTopoTools v3 output.
|
|
2
|
+
|
|
3
|
+
Version 1 provides a backend-agnostic IO/data core (``lsdviztools3.io``) and a
|
|
4
|
+
thin PyGMT render layer (``lsdviztools3.render``). Statistics (x-y) plotting is
|
|
5
|
+
deferred (see ``lsdviztools3.charts``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.4.1"
|
|
9
|
+
|
|
10
|
+
# TODO: curate top-level re-exports once the render API stabilises, e.g.
|
|
11
|
+
# from .render.hillshade import render_hillshade
|
|
12
|
+
# from .render.style import MapStyle
|
lsdviztools3/_gmt.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Import PyGMT with a clear message when GMT itself is missing.
|
|
2
|
+
|
|
3
|
+
PyGMT is a Python wrapper around the GMT C library. pip installs PyGMT (it is a
|
|
4
|
+
dependency of lsdviztools3) but cannot install GMT, so a plain ``pip install``
|
|
5
|
+
gives a PyGMT that fails on import with ``GMTCLibNotFoundError``. Every map
|
|
6
|
+
renderer imports PyGMT through :func:`require_pygmt`, which turns that, and a
|
|
7
|
+
missing PyGMT, into :class:`GMTNotAvailableError` saying how to install GMT.
|
|
8
|
+
|
|
9
|
+
The import happens inside each renderer, not when lsdviztools3 is imported, so
|
|
10
|
+
the matplotlib charts keep working without GMT.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
INSTALL_HELP = (
|
|
16
|
+
"lsdviztools3 draws maps with PyGMT, which needs the GMT library. pip cannot "
|
|
17
|
+
"install GMT: install it from conda-forge (for example "
|
|
18
|
+
"`conda install -c conda-forge gmt pygmt`, or use the pixi environment in "
|
|
19
|
+
"the lsdviztools3 repository), Homebrew (`brew install gmt`) or your system "
|
|
20
|
+
"package manager (GMT 6.5 or later). The matplotlib charts work without it. "
|
|
21
|
+
"See https://lsdtopotools.github.io/lsdviztools3/ for installation."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GMTNotAvailableError(ImportError):
|
|
26
|
+
"""PyGMT, or the GMT library it wraps, cannot be loaded.
|
|
27
|
+
|
|
28
|
+
A subclass of ImportError, so ``pytest.importorskip`` and code that already
|
|
29
|
+
catches ImportError treat a missing GMT like a missing package.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def require_pygmt():
|
|
34
|
+
"""Return the ``pygmt`` module, or raise :class:`GMTNotAvailableError`."""
|
|
35
|
+
try:
|
|
36
|
+
import pygmt
|
|
37
|
+
except ImportError as err: # PyGMT itself is not installed
|
|
38
|
+
raise GMTNotAvailableError(f"PyGMT is not installed. {INSTALL_HELP}") from err
|
|
39
|
+
except Exception as err: # noqa: BLE001 - PyGMT raises its own types on load
|
|
40
|
+
# GMTCLibNotFoundError / GMTCLibError: PyGMT is there but cannot load
|
|
41
|
+
# libgmt (missing, or too old). Match on the name: pygmt.exceptions
|
|
42
|
+
# cannot be imported when importing pygmt itself has just failed.
|
|
43
|
+
if type(err).__name__.startswith("GMTCLib"):
|
|
44
|
+
raise GMTNotAvailableError(
|
|
45
|
+
f"PyGMT is installed but cannot load the GMT library ({err}). {INSTALL_HELP}"
|
|
46
|
+
) from err
|
|
47
|
+
raise
|
|
48
|
+
return pygmt
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Statistics (x-y) plotting.
|
|
2
|
+
|
|
3
|
+
All x-y charts are implemented on a matplotlib engine (DESIGN decision #1),
|
|
4
|
+
imported lazily so the core maps install stays lean. Only the channel
|
|
5
|
+
longitudinal profile (:func:`plot_profile`) is still deferred and raises
|
|
6
|
+
``NotImplementedError``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .chi import plot_chi_profile, plot_chi_stacked, plot_ksn_vs_erate
|
|
12
|
+
from .circular import (
|
|
13
|
+
circular_wasserstein, kuiper_two_sample, plot_circular_comparison,
|
|
14
|
+
)
|
|
15
|
+
from .cross_sections import (
|
|
16
|
+
plot_valley_cross_section_map, plot_valley_cross_sections,
|
|
17
|
+
plot_valley_curved_cross_section_map,
|
|
18
|
+
plot_valley_cross_section_features,
|
|
19
|
+
plot_valley_cross_section_features_map,
|
|
20
|
+
plot_valley_cross_section_hydraulics,
|
|
21
|
+
cross_section_hydraulics, detect_hydraulic_flats,
|
|
22
|
+
combine_width_steps, compute_flat_surface_edges,
|
|
23
|
+
)
|
|
24
|
+
from .cosmo_comparison import (
|
|
25
|
+
load_cosmo_table, plot_cosmo_sweep_panels, plot_cosmo_sample_residuals,
|
|
26
|
+
)
|
|
27
|
+
from .cosmo_transient import (
|
|
28
|
+
cosmo_transient_title, load_cosmo_transient_table,
|
|
29
|
+
plot_cosmo_transient_concentration, plot_cosmo_transient_erate_ratio,
|
|
30
|
+
plot_nuclide_removal_response,
|
|
31
|
+
)
|
|
32
|
+
from .cosmo_transient_spatial import (
|
|
33
|
+
load_joined_samples, plot_ratio_overlay_1d, plot_ratio_vs_mean_erate,
|
|
34
|
+
plot_scenario_delta,
|
|
35
|
+
plot_ratio_vs_steptime,
|
|
36
|
+
)
|
|
37
|
+
from .distributions import plot_distributions
|
|
38
|
+
from .lithology_ksn import (
|
|
39
|
+
plot_ksn_box_by_lithology, plot_ksn_by_lithology_and_basin,
|
|
40
|
+
plot_ksn_histogram_by_lithology, plot_reach_ksn_by_lithology,
|
|
41
|
+
)
|
|
42
|
+
from .channel_bearings import (
|
|
43
|
+
plot_binned_rose, plot_binned_rose_grid, plot_comparison_table,
|
|
44
|
+
plot_junction_angle_distributions, plot_junction_angle_summary,
|
|
45
|
+
plot_rose_comparison,
|
|
46
|
+
)
|
|
47
|
+
from .estar_rstar import plot_estar_rstar_model_scan
|
|
48
|
+
from .divide_migration import (
|
|
49
|
+
aggressor_annotation, plot_head_metric_vs_distance, plot_pair_comparison,
|
|
50
|
+
)
|
|
51
|
+
from .hand import (
|
|
52
|
+
plot_hand_distance, plot_hand_window_populations, plot_valley_break_metric,
|
|
53
|
+
plot_valley_break_fraction, plot_valley_floor_map, plot_valley_reach_map,
|
|
54
|
+
)
|
|
55
|
+
from .hydraulic_breaks import (
|
|
56
|
+
plot_hydraulic_breaks_downstream, hydraulic_break_clusters,
|
|
57
|
+
plot_hydraulic_break_clusters, plot_cross_section_informed_valley_mask,
|
|
58
|
+
)
|
|
59
|
+
from .landslides import (
|
|
60
|
+
load_landslide_inventory,
|
|
61
|
+
plot_landslide_area_volume_larsen,
|
|
62
|
+
plot_sample_effect_apparent_vs_true,
|
|
63
|
+
plot_sample_effect_ratio,
|
|
64
|
+
)
|
|
65
|
+
from .network_geometry import (
|
|
66
|
+
plot_delta_L_cdf, plot_edge_basin_cdf, plot_junction_angle_cdf,
|
|
67
|
+
plot_metric_panels, plot_metric_vs_temperature,
|
|
68
|
+
)
|
|
69
|
+
from .rose import rose_diagram
|
|
70
|
+
from .swath import plot_swath
|
|
71
|
+
from .hillslope_profiles import (
|
|
72
|
+
load_profile_raster, profile_derivatives, profile_residuals,
|
|
73
|
+
plot_hillslope_profiles, plot_hillslope_profile, plot_profile_residual_panels,
|
|
74
|
+
)
|
|
75
|
+
from .hillslope_residuals import (
|
|
76
|
+
residual_statistics, summarise_residuals, plot_residual_density,
|
|
77
|
+
plot_hillslope_residual_density, plot_hillslope_residuals,
|
|
78
|
+
summarize_residuals, plot_residual_raster,
|
|
79
|
+
)
|
|
80
|
+
from .hillslope_transient_residuals import (
|
|
81
|
+
load_transient_residuals, plot_transient_residuals,
|
|
82
|
+
plot_hillslope_transient_residuals, plot_strip_transient_metrics,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
__all__ = [
|
|
86
|
+
"plot_chi_profile", "plot_chi_stacked", "plot_ksn_vs_erate", "plot_swath",
|
|
87
|
+
"plot_hand_distance", "plot_hand_window_populations",
|
|
88
|
+
"plot_valley_break_metric", "plot_valley_break_fraction",
|
|
89
|
+
"plot_valley_cross_sections",
|
|
90
|
+
"plot_valley_cross_section_map",
|
|
91
|
+
"plot_valley_curved_cross_section_map",
|
|
92
|
+
"plot_valley_cross_section_features",
|
|
93
|
+
"plot_valley_cross_section_features_map",
|
|
94
|
+
"plot_valley_cross_section_hydraulics",
|
|
95
|
+
"cross_section_hydraulics", "detect_hydraulic_flats",
|
|
96
|
+
"combine_width_steps", "compute_flat_surface_edges",
|
|
97
|
+
"plot_valley_reach_map",
|
|
98
|
+
"plot_valley_floor_map",
|
|
99
|
+
"plot_hydraulic_breaks_downstream", "hydraulic_break_clusters",
|
|
100
|
+
"plot_hydraulic_break_clusters",
|
|
101
|
+
"plot_cross_section_informed_valley_mask",
|
|
102
|
+
"plot_distributions", "rose_diagram",
|
|
103
|
+
"plot_ksn_histogram_by_lithology", "plot_ksn_box_by_lithology",
|
|
104
|
+
"plot_reach_ksn_by_lithology", "plot_ksn_by_lithology_and_basin",
|
|
105
|
+
"plot_binned_rose", "plot_binned_rose_grid", "plot_junction_angle_summary",
|
|
106
|
+
"plot_junction_angle_distributions", "plot_rose_comparison", "plot_comparison_table",
|
|
107
|
+
"plot_estar_rstar_model_scan",
|
|
108
|
+
"load_landslide_inventory",
|
|
109
|
+
"plot_landslide_area_volume_larsen",
|
|
110
|
+
"plot_sample_effect_apparent_vs_true",
|
|
111
|
+
"plot_sample_effect_ratio",
|
|
112
|
+
"circular_wasserstein", "kuiper_two_sample", "plot_circular_comparison",
|
|
113
|
+
"load_cosmo_table", "plot_cosmo_sweep_panels", "plot_cosmo_sample_residuals",
|
|
114
|
+
"load_cosmo_transient_table", "cosmo_transient_title",
|
|
115
|
+
"plot_cosmo_transient_concentration", "plot_cosmo_transient_erate_ratio",
|
|
116
|
+
"plot_nuclide_removal_response",
|
|
117
|
+
"load_joined_samples", "plot_ratio_vs_steptime", "plot_ratio_vs_mean_erate",
|
|
118
|
+
"plot_ratio_overlay_1d", "plot_scenario_delta",
|
|
119
|
+
"plot_pair_comparison", "plot_head_metric_vs_distance",
|
|
120
|
+
"aggressor_annotation",
|
|
121
|
+
"plot_junction_angle_cdf", "plot_delta_L_cdf", "plot_edge_basin_cdf",
|
|
122
|
+
"plot_metric_panels", "plot_metric_vs_temperature",
|
|
123
|
+
"load_profile_raster", "profile_derivatives", "profile_residuals",
|
|
124
|
+
"plot_hillslope_profiles", "plot_hillslope_profile", "plot_profile_residual_panels",
|
|
125
|
+
"residual_statistics",
|
|
126
|
+
"summarise_residuals", "plot_residual_density", "plot_hillslope_residual_density",
|
|
127
|
+
"plot_hillslope_residuals", "summarize_residuals", "plot_residual_raster",
|
|
128
|
+
"load_transient_residuals", "plot_transient_residuals",
|
|
129
|
+
"plot_hillslope_transient_residuals", "plot_strip_transient_metrics",
|
|
130
|
+
"plot_profile", "plot_slope_area", "plot_gradient_curvature",
|
|
131
|
+
"plot_valley_spectrum", "plot_spacing_vs_ci", "plot_landscape_panel",
|
|
132
|
+
"plot_significant_frequencies_polar", "plot_spectral_tile_summary",
|
|
133
|
+
"plot_normalised_spectrum_2d", "plot_radial_spectrum",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
_DEFERRED_MSG = (
|
|
137
|
+
"This lsdviztools3.charts x-y plot is deferred to a later release and is "
|
|
138
|
+
"not implemented yet."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def plot_profile(*args, **kwargs):
|
|
143
|
+
"""Longitudinal channel profile. Deferred to a later release."""
|
|
144
|
+
raise NotImplementedError(_DEFERRED_MSG)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def plot_slope_area(*args, **kwargs):
|
|
148
|
+
"""Slope-area "boomerang" -- see :mod:`lsdviztools3.charts.slope_area`."""
|
|
149
|
+
from .slope_area import plot_slope_area as _impl
|
|
150
|
+
return _impl(*args, **kwargs)
|
|
151
|
+
|
|
152
|
+
def plot_valley_spectrum(*args, **kwargs):
|
|
153
|
+
"""One landscape's spectrum with both spacing measures marked.
|
|
154
|
+
|
|
155
|
+
See :mod:`lsdviztools3.charts.valley_spacing`.
|
|
156
|
+
"""
|
|
157
|
+
from .valley_spacing import plot_valley_spectrum as _impl
|
|
158
|
+
return _impl(*args, **kwargs)
|
|
159
|
+
|
|
160
|
+
def plot_spacing_vs_ci(*args, **kwargs):
|
|
161
|
+
"""Valley spacing against the Bonetti channelization index, for a sweep.
|
|
162
|
+
|
|
163
|
+
See :mod:`lsdviztools3.charts.valley_spacing`.
|
|
164
|
+
"""
|
|
165
|
+
from .valley_spacing import plot_spacing_vs_ci as _impl
|
|
166
|
+
return _impl(*args, **kwargs)
|
|
167
|
+
|
|
168
|
+
def plot_landscape_panel(*args, **kwargs):
|
|
169
|
+
"""Hillshade a set of landscapes side by side, for a visual sniff test.
|
|
170
|
+
|
|
171
|
+
See :mod:`lsdviztools3.charts.valley_spacing`.
|
|
172
|
+
"""
|
|
173
|
+
from .valley_spacing import plot_landscape_panel as _impl
|
|
174
|
+
return _impl(*args, **kwargs)
|
|
175
|
+
|
|
176
|
+
def plot_significant_frequencies_polar(*args, **kwargs):
|
|
177
|
+
"""Significant 2D spectrum cells on a North-up polar axis (bearing x frequency).
|
|
178
|
+
|
|
179
|
+
See :mod:`lsdviztools3.charts.spectral`.
|
|
180
|
+
"""
|
|
181
|
+
from .spectral import plot_significant_frequencies_polar as _impl
|
|
182
|
+
return _impl(*args, **kwargs)
|
|
183
|
+
|
|
184
|
+
def plot_spectral_tile_summary(*args, **kwargs):
|
|
185
|
+
"""One lsdtt-spectral-analysis run on a 2 x 2 figure (input, 2D, radial, polar).
|
|
186
|
+
|
|
187
|
+
See :mod:`lsdviztools3.charts.spectral`.
|
|
188
|
+
"""
|
|
189
|
+
from .spectral import plot_spectral_tile_summary as _impl
|
|
190
|
+
return _impl(*args, **kwargs)
|
|
191
|
+
|
|
192
|
+
def plot_normalised_spectrum_2d(*args, **kwargs):
|
|
193
|
+
"""Normalised 2D spectrum, north up, significant cells outlined.
|
|
194
|
+
|
|
195
|
+
See :mod:`lsdviztools3.charts.spectral`.
|
|
196
|
+
"""
|
|
197
|
+
from .spectral import plot_normalised_spectrum_2d as _impl
|
|
198
|
+
return _impl(*args, **kwargs)
|
|
199
|
+
|
|
200
|
+
def plot_radial_spectrum(*args, **kwargs):
|
|
201
|
+
"""Binned radial spectrum with background, normalised PSD and rollover.
|
|
202
|
+
|
|
203
|
+
See :mod:`lsdviztools3.charts.spectral`.
|
|
204
|
+
"""
|
|
205
|
+
from .spectral import plot_radial_spectrum as _impl
|
|
206
|
+
return _impl(*args, **kwargs)
|
|
207
|
+
|
|
208
|
+
def plot_gradient_curvature(*args, **kwargs):
|
|
209
|
+
"""Gradient-curvature diagnostic after Roering (2008).
|
|
210
|
+
|
|
211
|
+
See :mod:`lsdviztools3.charts.gradient_curvature`.
|
|
212
|
+
"""
|
|
213
|
+
from .gradient_curvature import plot_gradient_curvature as _impl
|
|
214
|
+
return _impl(*args, **kwargs)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Long profiles of channels burned by lsdtt3 ``lsdtt-stream-burning``.
|
|
2
|
+
|
|
3
|
+
Reads ``<write_fname>_burned_profiles.csv`` from a run with
|
|
4
|
+
``stream_burn_mode: burn_to_corridor``: one row
|
|
5
|
+
per channel cell, downstream order within each path (a chain of reaches from
|
|
6
|
+
its head to where it joins another path or leaves the network), with
|
|
7
|
+
|
|
8
|
+
path_id, path_rank, reach_id, name, point_index, distance_m, flow_distance_m,
|
|
9
|
+
x, y, z_original, z_cleaned, z_profile, z_final, pit_filled, pit_id,
|
|
10
|
+
is_record_low, is_rejected_low, drainage_area_m2, chi, joins_path_id,
|
|
11
|
+
join_distance_m, starts_on_path_id
|
|
12
|
+
|
|
13
|
+
Everything here is drawing: which cells form a course (a path and the paths it
|
|
14
|
+
joins, down to the outlet) is read from ``joins_path_id`` / ``join_distance_m``;
|
|
15
|
+
nothing is recomputed. The one derived quantity is the chi axis of a course,
|
|
16
|
+
where each joined path's chi (counted from that path's head in the CSV) is
|
|
17
|
+
shifted so the course's chi is continuous at the joins.
|
|
18
|
+
|
|
19
|
+
``course_rows`` the rows of a course, ordered downstream, with
|
|
20
|
+
``course_distance_m`` and ``course_chi``
|
|
21
|
+
``select_course`` a course by path id, path rank or river name
|
|
22
|
+
``draw_burned_profile`` one course on one axis
|
|
23
|
+
``plot_burned_profiles`` one panel per CSV (e.g. X = 0 / 60 / 150 m, or a
|
|
24
|
+
linear against a chi fit), shared x axis
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
import matplotlib
|
|
32
|
+
matplotlib.use("Agg")
|
|
33
|
+
import matplotlib.pyplot as plt
|
|
34
|
+
import numpy as np
|
|
35
|
+
import pandas as pd
|
|
36
|
+
|
|
37
|
+
# Fixed roles (validated together, light surface; green is below 3:1 contrast,
|
|
38
|
+
# so every band carries a legend label).
|
|
39
|
+
FINAL_COLOUR = "#2a78d6" # the burned channel in the output DEM
|
|
40
|
+
CUT_COLOUR = "#eb6834" # cut below the cleaned DEM
|
|
41
|
+
PIT_COLOUR = "#1baf7a" # small pits filled before the profile was sampled
|
|
42
|
+
RAISE_COLOUR = "#9a5bd6" # raised above the cleaned DEM (junction pins, rejected minima)
|
|
43
|
+
ORIGINAL_COLOUR = "#8a8a85" # the input DEM under the channel
|
|
44
|
+
INK = "#3d3d3a"
|
|
45
|
+
|
|
46
|
+
REQUIRED = ["path_id", "path_rank", "name", "point_index", "distance_m", "flow_distance_m",
|
|
47
|
+
"x", "y", "z_original", "z_cleaned", "z_profile", "z_final", "pit_filled",
|
|
48
|
+
"pit_id", "is_record_low", "is_rejected_low", "chi", "joins_path_id",
|
|
49
|
+
"join_distance_m"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def read_burned_profiles(csv) -> pd.DataFrame:
|
|
53
|
+
"""Read a ``_burned_profiles.csv`` written by burn_to_corridor."""
|
|
54
|
+
p = Path(csv)
|
|
55
|
+
if not p.exists():
|
|
56
|
+
raise FileNotFoundError(f"{p} (run lsdtt-stream-burning with stream_burn_mode: "
|
|
57
|
+
"burn_to_corridor and write_burned_profiles_csv: true).")
|
|
58
|
+
df = pd.read_csv(p, keep_default_na=False, na_values={"chi": [""], "drainage_area_m2": [""],
|
|
59
|
+
"join_distance_m": [""]})
|
|
60
|
+
missing = [c for c in REQUIRED if c not in df.columns]
|
|
61
|
+
if missing:
|
|
62
|
+
raise ValueError(f"{p}: not a burn_to_corridor profiles file (missing {missing}); "
|
|
63
|
+
"older runs (z_filled / z_burned) are not supported.")
|
|
64
|
+
df["name"] = df["name"].astype(str)
|
|
65
|
+
return df
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def course_rows(df: pd.DataFrame, path_id: int, start_index: int = 0) -> pd.DataFrame:
|
|
69
|
+
"""Rows of the course from ``path_id`` (from its cell ``start_index``) down
|
|
70
|
+
to the outlet, following ``joins_path_id``. Adds ``course_distance_m``
|
|
71
|
+
(downstream from the course's first cell) and ``course_chi`` (continuous
|
|
72
|
+
at the joins; NaN where the run has no chi)."""
|
|
73
|
+
parts = []
|
|
74
|
+
pid, lo = int(path_id), int(start_index)
|
|
75
|
+
offset = 0.0 # added to the chi of the current path
|
|
76
|
+
seen = set()
|
|
77
|
+
while pid >= 0 and pid not in seen:
|
|
78
|
+
seen.add(pid)
|
|
79
|
+
g = df[df.path_id == pid].sort_values("point_index")
|
|
80
|
+
if g.empty:
|
|
81
|
+
raise ValueError(f"no path {pid} in the profiles file")
|
|
82
|
+
g = g[g.point_index >= lo].copy()
|
|
83
|
+
g["course_chi"] = g.chi.astype(float) + offset
|
|
84
|
+
nxt = int(g.joins_path_id.iloc[0])
|
|
85
|
+
if nxt >= 0:
|
|
86
|
+
# the joined path continues from the join cell (this path's last
|
|
87
|
+
# cell); its chi is shifted to match this path's value there
|
|
88
|
+
h = df[df.path_id == nxt].sort_values("point_index")
|
|
89
|
+
jd = float(g.join_distance_m.iloc[0])
|
|
90
|
+
at = h[np.isclose(h.distance_m, jd, atol=1e-6)]
|
|
91
|
+
if at.empty:
|
|
92
|
+
raise ValueError(f"path {pid} joins path {nxt} at {jd} m, which has no such cell")
|
|
93
|
+
lo = int(at.point_index.min())
|
|
94
|
+
offset = float(g.course_chi.iloc[-1]) - float(at.chi.iloc[0])
|
|
95
|
+
g = g.iloc[:-1]
|
|
96
|
+
parts.append(g)
|
|
97
|
+
pid = nxt
|
|
98
|
+
c = pd.concat(parts, ignore_index=True)
|
|
99
|
+
c["course_distance_m"] = c.flow_distance_m.iloc[0] - c.flow_distance_m
|
|
100
|
+
return c
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def select_course(df: pd.DataFrame, path_id=None, path_rank=None, name=None):
|
|
104
|
+
"""A course by path id, path rank (0 = the first, longest path) or river
|
|
105
|
+
name (from the most upstream cell with that name). Returns (rows, label)."""
|
|
106
|
+
given = [v is not None for v in (path_id, path_rank, name)]
|
|
107
|
+
if sum(given) > 1:
|
|
108
|
+
raise ValueError("give one of path_id, path_rank, name")
|
|
109
|
+
if name is not None:
|
|
110
|
+
hit = df[df.name == name]
|
|
111
|
+
if hit.empty:
|
|
112
|
+
raise ValueError(f"no cells named {name!r} (was vector_name_field set?)")
|
|
113
|
+
top = hit.loc[hit.flow_distance_m.idxmax()]
|
|
114
|
+
return course_rows(df, int(top.path_id), int(top.point_index)), name
|
|
115
|
+
if path_rank is None and path_id is None:
|
|
116
|
+
path_rank = 0
|
|
117
|
+
if path_rank is not None:
|
|
118
|
+
hit = df[df.path_rank == int(path_rank)]
|
|
119
|
+
if hit.empty:
|
|
120
|
+
raise ValueError(f"no path of rank {path_rank}")
|
|
121
|
+
path_id = int(hit.path_id.iloc[0])
|
|
122
|
+
rows = course_rows(df, int(path_id))
|
|
123
|
+
names = list(dict.fromkeys(n for n in rows.name if n))
|
|
124
|
+
label = (" / ".join(names[:3]) + (" ..." if len(names) > 3 else "")) if names else f"path {path_id}"
|
|
125
|
+
return rows, label
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _joins_on_course(df, course, max_labels):
|
|
129
|
+
"""(course x position, name, length) of paths joining the course."""
|
|
130
|
+
on = {(round(a, 3), round(b, 3)): i for i, (a, b) in enumerate(zip(course.x, course.y))}
|
|
131
|
+
course_paths = set(course.path_id)
|
|
132
|
+
out = []
|
|
133
|
+
for pid, g in df.groupby("path_id"):
|
|
134
|
+
if pid in course_paths:
|
|
135
|
+
continue
|
|
136
|
+
g = g.sort_values("point_index")
|
|
137
|
+
last = g.iloc[-1]
|
|
138
|
+
k = on.get((round(last.x, 3), round(last.y, 3)))
|
|
139
|
+
if k is None or int(last.joins_path_id) < 0:
|
|
140
|
+
continue
|
|
141
|
+
nm = next((n for n in g.name[::-1] if n), "")
|
|
142
|
+
out.append((k, nm, float(g.distance_m.max())))
|
|
143
|
+
out.sort(key=lambda t: -t[2])
|
|
144
|
+
return out, out[:max_labels]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def draw_burned_profile(ax, df: pd.DataFrame, course: pd.DataFrame, x: str = "distance",
|
|
148
|
+
mark=None, max_join_labels: int = 6, legend: bool = True):
|
|
149
|
+
"""Draw one course on ``ax``. ``x``: "distance" (km downstream) or "chi"."""
|
|
150
|
+
if x == "chi":
|
|
151
|
+
if not np.isfinite(course.course_chi).all():
|
|
152
|
+
raise ValueError("this run has no chi (burn_profile_fit_algorithm_choice: chi)")
|
|
153
|
+
xs = course.course_chi.to_numpy()
|
|
154
|
+
ax.set_xlabel("chi (m)")
|
|
155
|
+
else:
|
|
156
|
+
xs = course.course_distance_m.to_numpy() / 1000.0
|
|
157
|
+
ax.set_xlabel("distance downstream (km)")
|
|
158
|
+
zo = course.z_original.to_numpy()
|
|
159
|
+
zc = course.z_cleaned.to_numpy()
|
|
160
|
+
zf = course.z_final.to_numpy()
|
|
161
|
+
zp = course.z_profile.to_numpy()
|
|
162
|
+
|
|
163
|
+
# Bands first, lines over them. Each cell's change against the input DEM is
|
|
164
|
+
# split without overlap: cut (final below the DEM); pit fill (from the DEM
|
|
165
|
+
# up to the lower of the cleaned and final values: the part of a small pit
|
|
166
|
+
# the channel still sits in); other raise (above both: junction pins,
|
|
167
|
+
# rejected minima).
|
|
168
|
+
pit = zc > zo
|
|
169
|
+
n_pits = int(course.loc[pit, "pit_id"].nunique())
|
|
170
|
+
|
|
171
|
+
def widen(m):
|
|
172
|
+
# each run widened by one cell either side (the band is zero there),
|
|
173
|
+
# so a one-cell run still draws
|
|
174
|
+
w = m.copy()
|
|
175
|
+
w[1:] |= m[:-1]
|
|
176
|
+
w[:-1] |= m[1:]
|
|
177
|
+
return w
|
|
178
|
+
|
|
179
|
+
cut = zf < zo
|
|
180
|
+
pit_top = np.minimum(zc, zf)
|
|
181
|
+
pit_band = pit & (zf > zo)
|
|
182
|
+
raise_lo = np.maximum(zo, zc)
|
|
183
|
+
raise_band = zf > raise_lo + 1e-6
|
|
184
|
+
ax.fill_between(xs, zf, zo, where=widen(cut), color=CUT_COLOUR, alpha=0.35, lw=0)
|
|
185
|
+
ax.fill_between(xs, zo, np.where(pit_band, pit_top, zo), where=widen(pit_band),
|
|
186
|
+
color=PIT_COLOUR, alpha=0.6, lw=0)
|
|
187
|
+
ax.fill_between(xs, raise_lo, np.where(raise_band, zf, raise_lo), where=widen(raise_band),
|
|
188
|
+
color=RAISE_COLOUR, alpha=0.5, lw=0)
|
|
189
|
+
ax.plot(xs, zo, color=ORIGINAL_COLOUR, lw=0.8, zorder=3)
|
|
190
|
+
if np.nanmax(np.abs(zp - zf)) > 0.01:
|
|
191
|
+
ax.plot(xs, zp, color=INK, lw=0.8, ls="--", zorder=4)
|
|
192
|
+
ax.plot(xs, zf, color=FINAL_COLOUR, lw=1.6, zorder=5)
|
|
193
|
+
lows = course.is_record_low.to_numpy() == 1
|
|
194
|
+
ax.plot(xs[lows], zf[lows], "o", ms=3.0, color=INK, mec="white", mew=0.5, zorder=6, ls="none")
|
|
195
|
+
rej = course.is_rejected_low.to_numpy() == 1
|
|
196
|
+
if rej.any():
|
|
197
|
+
ax.plot(xs[rej], zo[rej], "x", ms=4.5, color=INK, zorder=6, ls="none")
|
|
198
|
+
|
|
199
|
+
joins, labelled = _joins_on_course(df, course, max_join_labels)
|
|
200
|
+
for k, nm, _ in joins:
|
|
201
|
+
ax.axvline(xs[k], color=ORIGINAL_COLOUR, lw=0.5, alpha=0.6, zorder=1)
|
|
202
|
+
ytop = ax.get_ylim()[1]
|
|
203
|
+
span = float(np.nanmax(xs) - np.nanmin(xs)) or 1.0
|
|
204
|
+
placed = []
|
|
205
|
+
for k, nm, _ in labelled:
|
|
206
|
+
if nm and all(abs(xs[k] - q) > 0.03 * span for q in placed):
|
|
207
|
+
ax.text(xs[k], ytop, " " + nm, rotation=90, va="top", ha="right", fontsize=7, color=INK)
|
|
208
|
+
placed.append(xs[k])
|
|
209
|
+
if mark is not None:
|
|
210
|
+
mx, my, mlabel = mark
|
|
211
|
+
k = int(np.argmin(np.hypot(course.x - mx, course.y - my)))
|
|
212
|
+
ax.axvline(xs[k], color=INK, lw=0.8, ls=":", zorder=2)
|
|
213
|
+
ax.text(xs[k], ytop, " " + mlabel, va="top", ha="left", fontsize=8, color=INK)
|
|
214
|
+
ax.set_ylabel("elevation (m)")
|
|
215
|
+
ax.grid(alpha=0.25, lw=0.5)
|
|
216
|
+
for s in ("top", "right"):
|
|
217
|
+
ax.spines[s].set_visible(False)
|
|
218
|
+
|
|
219
|
+
rises = int((np.diff(zf) > 0).sum())
|
|
220
|
+
stats = (f"{n_pits} pits filled ({int(pit.sum())} cells), {int(lows.sum())} minima used, "
|
|
221
|
+
f"{int(rej.sum())} rejected; final rises at {rises} of {len(zf) - 1} steps")
|
|
222
|
+
from matplotlib.lines import Line2D
|
|
223
|
+
from matplotlib.patches import Patch
|
|
224
|
+
handles = [Line2D([], [], color=ORIGINAL_COLOUR, lw=0.8, label="input DEM under the channel"),
|
|
225
|
+
Line2D([], [], color=FINAL_COLOUR, lw=1.6, label="burned channel (output DEM)"),
|
|
226
|
+
Line2D([], [], color=INK, marker="o", ms=3, mec="white", ls="none",
|
|
227
|
+
label="minima the profile joins"),
|
|
228
|
+
Patch(color=CUT_COLOUR, alpha=0.35, label="cut below the input DEM"),
|
|
229
|
+
Patch(color=PIT_COLOUR, alpha=0.6, label="small pit filled before sampling (channel in it)"),
|
|
230
|
+
Patch(color=RAISE_COLOUR, alpha=0.5, label="raised (junction, rejected minimum)")]
|
|
231
|
+
if rej.any():
|
|
232
|
+
handles.append(Line2D([], [], color=INK, marker="x", ls="none", label="minima rejected by the cut cap"))
|
|
233
|
+
if np.nanmax(np.abs(zp - zf)) > 0.01:
|
|
234
|
+
handles.append(Line2D([], [], color=INK, lw=0.8, ls="--", label="fitted profile"))
|
|
235
|
+
if legend:
|
|
236
|
+
ax.legend(handles=handles, fontsize=7, loc="upper right", frameon=False)
|
|
237
|
+
draw_burned_profile.last_handles = handles
|
|
238
|
+
return stats
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def plot_burned_profiles(csvs, out, labels=None, path_id=None, path_rank=None, name=None,
|
|
242
|
+
x: str = "distance", mark=None, title=None, dpi: int = 200,
|
|
243
|
+
panel_height: float = 3.4, max_join_labels: int = 6):
|
|
244
|
+
"""One panel per CSV for the same course (chosen in the first CSV, by path
|
|
245
|
+
id, path rank or name; by name or rank in the others, whose path ids may
|
|
246
|
+
differ), shared x axis. Writes ``out`` (PNG)."""
|
|
247
|
+
csvs = [csvs] if isinstance(csvs, (str, Path)) else list(csvs)
|
|
248
|
+
labels = list(labels) if labels else [Path(c).name.replace("_burned_profiles.csv", "") for c in csvs]
|
|
249
|
+
if len(labels) != len(csvs):
|
|
250
|
+
raise ValueError("give one label per CSV")
|
|
251
|
+
fig, axes = plt.subplots(len(csvs), 1, figsize=(11, panel_height * len(csvs)), sharex=True, squeeze=False)
|
|
252
|
+
course_label = None
|
|
253
|
+
for i, (csv, lab) in enumerate(zip(csvs, labels)):
|
|
254
|
+
ax = axes[i, 0]
|
|
255
|
+
df = read_burned_profiles(csv)
|
|
256
|
+
if i > 0 and path_id is not None and name is None and path_rank is None:
|
|
257
|
+
raise ValueError("compare several runs by name or path rank, not path id")
|
|
258
|
+
course, course_label = select_course(df, path_id=path_id, path_rank=path_rank, name=name)
|
|
259
|
+
stats = draw_burned_profile(ax, df, course, x=x, mark=mark, max_join_labels=max_join_labels,
|
|
260
|
+
legend=False)
|
|
261
|
+
ax.set_title(f"{lab}: {stats}", fontsize=9, loc="left")
|
|
262
|
+
if i < len(csvs) - 1:
|
|
263
|
+
ax.set_xlabel("")
|
|
264
|
+
fig.suptitle(title or course_label, fontsize=11)
|
|
265
|
+
handles = draw_burned_profile.last_handles
|
|
266
|
+
fig.legend(handles=handles, loc="lower center", ncol=min(4, len(handles)), fontsize=8, frameon=False)
|
|
267
|
+
fig.tight_layout(rect=(0, 0.06 if len(csvs) > 1 else 0.14, 1, 1))
|
|
268
|
+
fig.savefig(out, dpi=dpi)
|
|
269
|
+
plt.close(fig)
|
|
270
|
+
return out
|