weac 3.2.0__tar.gz → 3.3.0__tar.gz
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.
- {weac-3.2.0 → weac-3.3.0}/CITATION.cff +1 -1
- {weac-3.2.0/src/weac.egg-info → weac-3.3.0}/PKG-INFO +43 -3
- {weac-3.2.0 → weac-3.3.0}/README.md +40 -1
- {weac-3.2.0 → weac-3.3.0}/pyproject.toml +4 -3
- {weac-3.2.0 → weac-3.3.0}/src/weac/__init__.py +1 -1
- weac-3.3.0/src/weac/parser/__init__.py +13 -0
- weac-3.3.0/src/weac/parser/plotting/__init__.py +19 -0
- weac-3.3.0/src/weac/parser/plotting/force_penetration_layers.py +148 -0
- weac-3.3.0/src/weac/parser/plotting/force_penetration_plot.py +107 -0
- weac-3.3.0/src/weac/parser/plotting/smp_penetration_plot.py +34 -0
- weac-3.3.0/src/weac/parser/plotting/snowscope_penetration_plot.py +42 -0
- weac-3.3.0/src/weac/parser/smp_parser.py +204 -0
- {weac-3.2.0/src/weac/utils → weac-3.3.0/src/weac/parser}/snowpilot_parser.py +3 -25
- weac-3.3.0/src/weac/parser/snowscope_parser.py +307 -0
- weac-3.3.0/src/weac/parser/utils/__init__.py +11 -0
- weac-3.3.0/src/weac/parser/utils/layer_binning.py +120 -0
- weac-3.3.0/src/weac/parser/utils/layer_gradient.py +141 -0
- weac-3.3.0/src/weac/parser/utils/plump_to_slope_normal.py +21 -0
- weac-3.3.0/src/weac/utils/snowpilot_parser.py +16 -0
- {weac-3.2.0 → weac-3.3.0/src/weac.egg-info}/PKG-INFO +43 -3
- {weac-3.2.0 → weac-3.3.0}/src/weac.egg-info/SOURCES.txt +17 -1
- {weac-3.2.0 → weac-3.3.0}/src/weac.egg-info/requires.txt +2 -1
- weac-3.3.0/tests/parser/test_profile_layers.py +208 -0
- weac-3.3.0/tests/parser/test_smp_parser.py +114 -0
- {weac-3.2.0/tests/utils → weac-3.3.0/tests/parser}/test_snowpilot_parser.py +16 -7
- weac-3.3.0/tests/parser/test_snowscope_parser.py +103 -0
- {weac-3.2.0 → weac-3.3.0}/LICENSE +0 -0
- {weac-3.2.0 → weac-3.3.0}/MANIFEST.in +0 -0
- {weac-3.2.0 → weac-3.3.0}/img/bc.png +0 -0
- {weac-3.2.0 → weac-3.3.0}/img/layering.png +0 -0
- {weac-3.2.0 → weac-3.3.0}/img/logo.png +0 -0
- {weac-3.2.0 → weac-3.3.0}/img/model.png +0 -0
- {weac-3.2.0 → weac-3.3.0}/img/profiles.png +0 -0
- {weac-3.2.0 → weac-3.3.0}/img/systems.png +0 -0
- {weac-3.2.0 → weac-3.3.0}/setup.cfg +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/analysis/__init__.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/analysis/analyzer.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/analysis/coupled_criterion.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/analysis/criteria_evaluator.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/analysis/envelopes.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/analysis/plotter.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/analysis/steady_state.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/components/__init__.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/components/config.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/components/criteria_config.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/components/layer.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/components/model_input.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/components/presets.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/components/scenario_config.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/components/segment.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/components/weak_layer.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/constants.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/core/__init__.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/core/eigensystem.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/core/field_quantities.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/core/generalized_eigensystem.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/core/generalized_field_quantities.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/core/generalized_unknown_constants_solver.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/core/scenario.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/core/slab.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/core/slab_touchdown.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/core/system_model.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/core/unknown_constants_solver.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/logging_config.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/utils/__init__.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/utils/geldsetzer.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/utils/misc.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac/utils/snow_types.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac.egg-info/dependency_links.txt +0 -0
- {weac-3.2.0 → weac-3.3.0}/src/weac.egg-info/top_level.txt +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/__init__.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/analysis/__init__.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/analysis/test_analyzer.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/analysis/test_criteria_evaluator.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/analysis/test_pst_critical_cut_touchdown.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/analysis/test_slab_tensile_comparisons.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/analysis/test_steady_state_ease.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/components/__init__.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/components/test_configs.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/components/test_layer.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/conftest.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/core/__init__.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/core/test_eigensystem.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/core/test_field_quantities.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/core/test_scenario.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/core/test_slab.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/core/test_slab_touchdown.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/core/test_system_model.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/test_comparison_results.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/test_regression_simulation.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/utils/__init__.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/utils/json_helpers.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/utils/test_json_helpers.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/utils/test_misc.py +0 -0
- {weac-3.2.0 → weac-3.3.0}/tests/utils/weac_reference_runner.py +0 -0
|
@@ -8,7 +8,7 @@ authors:
|
|
|
8
8
|
- family-names: "Weissgraeber"
|
|
9
9
|
given-names: "Philipp"
|
|
10
10
|
orcid: "https://orcid.org/0000-0001-8320-8672"
|
|
11
|
-
version: 3.
|
|
11
|
+
version: 3.3.0
|
|
12
12
|
date-released: 2026-08-06
|
|
13
13
|
identifiers:
|
|
14
14
|
- description: Collection of archived snapshots of all versions of WEAC
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: weac
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Weak layer anticrack nucleation model
|
|
5
5
|
Author-email: 2phi GbR <mail@2phi.de>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,6 +21,7 @@ Requires-Dist: numpy<2.4.0,>=2.3.5
|
|
|
21
21
|
Requires-Dist: scipy>=1.14.0
|
|
22
22
|
Requires-Dist: pydantic>=2.11.7
|
|
23
23
|
Requires-Dist: snowpylot>=1.1.5
|
|
24
|
+
Requires-Dist: snowmicropyn>=1.4.0
|
|
24
25
|
Provides-Extra: interactive
|
|
25
26
|
Requires-Dist: jupyter; extra == "interactive"
|
|
26
27
|
Requires-Dist: ipython>=8.37.0; extra == "interactive"
|
|
@@ -53,7 +54,7 @@ Requires-Dist: pyzmq>=27.0.1; extra == "dev"
|
|
|
53
54
|
Requires-Dist: tornado>=6.5.2; extra == "dev"
|
|
54
55
|
Requires-Dist: traitlets>=5.14.3; extra == "dev"
|
|
55
56
|
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
56
|
-
Requires-Dist: pylint>=3.
|
|
57
|
+
Requires-Dist: pylint>=3.3.0; extra == "dev"
|
|
57
58
|
Requires-Dist: pycodestyle>=2.11.1; extra == "dev"
|
|
58
59
|
Requires-Dist: black>=24.4.0; extra == "dev"
|
|
59
60
|
Requires-Dist: isort>=5.13.0; extra == "dev"
|
|
@@ -193,7 +194,8 @@ Needs (runtime dependencies are declared in [pyproject.toml](https://github.com/
|
|
|
193
194
|
- [Scipy](https://www.scipy.org/) ≥ 1.14.0
|
|
194
195
|
- [Matplotlib](https://matplotlib.org/) ≥ 3.9.1
|
|
195
196
|
- [Pydantic](https://docs.pydantic.dev/latest/) ≥ 2.11.7
|
|
196
|
-
- [Snowpylot](https://github.com/connellymk/snowpylot) ≥ 1.1.
|
|
197
|
+
- [Snowpylot](https://github.com/connellymk/snowpylot) ≥ 1.1.5
|
|
198
|
+
- [snowmicropyn](https://github.com/slf-dot-ch/snowmicropyn) ≥ 1.4.0
|
|
197
199
|
|
|
198
200
|
<!-- DEVELOPMENT SETUP -->
|
|
199
201
|
## Development Setup
|
|
@@ -428,6 +430,44 @@ u_bot = skier_system.fq.u(Z=z_skier, h0=bot, unit='um')
|
|
|
428
430
|
psi = skier_system.fq.psi(Z=z_skier, unit='deg')
|
|
429
431
|
```
|
|
430
432
|
|
|
433
|
+
### Parsing field profiles into layers
|
|
434
|
+
|
|
435
|
+
Instead of hand-building `Layer` lists, WEAC ships parsers in `weac.parser` that convert common field-profile formats into slope-normal WEAC layers (ordered surface → ground). All parsers expose `extract_layers(...)` returning `(layers, density_methods)`, so the output plugs directly into `ModelInput`. See [`demo/parser_demo.ipynb`](https://github.com/2phi/weac/blob/main/demo/parser_demo.ipynb) for side-by-side plots of the three parsers on a co-located Weissfluhjoch pit.
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
from weac.parser import SMPParser, SnowScopeParser, SnowPilotParser
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**SnowMicroPen** (`.PNT`) — `SMPParser` derives density and SSA from the force signal via [snowmicropyn](https://github.com/slf-dot-ch/snowmicropyn)'s Löwe 2012 shot-noise model plus a parameterization (`P2015`, `CR2020`, `K2020a`, `K2020b`). Surface and ground are auto-detected at construction.
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
smp = SMPParser("profile.PNT", density_method="CR2020")
|
|
445
|
+
# "bin" groups at a fixed thickness; "gradient" cuts on density-gradient jumps
|
|
446
|
+
layers, density_methods = smp.extract_layers(method="bin", layer_thickness_mm=20.0)
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
**SnowScope** (`.csv`) — `SnowScopeParser` reads penetration hardness (kPa) and derives density from a semilog model `D = a·ln(F) + b` (default `HAGENMULLER2018`). It exposes the same `extract_layers` interface as the SMP parser.
|
|
450
|
+
|
|
451
|
+
```python
|
|
452
|
+
scope = SnowScopeParser("profile.csv", density_method="HAGENMULLER2018")
|
|
453
|
+
layers, density_methods = scope.extract_layers(method="gradient", gradient_threshold=12.0)
|
|
454
|
+
|
|
455
|
+
scope_custom = SnowScopeParser(
|
|
456
|
+
"profile.csv",
|
|
457
|
+
semilog_slope=50.0,
|
|
458
|
+
semilog_intercept=100.0,
|
|
459
|
+
)
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**SnowPilot / CAAML** (`.xml`) — `SnowPilotParser` uses [snowpylot](https://github.com/connellymk/snowpylot) to read CAAML snowpits, emitting hand-hardness / density-observation layers. Densities come from measured observations where available, falling back to the Geldsetzer estimate from grain type and hand hardness.
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
pilot = SnowPilotParser("profile.xml")
|
|
466
|
+
# pass a known slope angle to scale plumb depths to slope-normal, e.g.:
|
|
467
|
+
# pilot.extract_layers(slope_angle_deg=pilot.pit_slope_angle_deg() or 0.0)
|
|
468
|
+
layers, density_methods = pilot.extract_layers()
|
|
469
|
+
```
|
|
470
|
+
|
|
431
471
|
<!-- ROADMAP -->
|
|
432
472
|
## Roadmap
|
|
433
473
|
|
|
@@ -130,7 +130,8 @@ Needs (runtime dependencies are declared in [pyproject.toml](https://github.com/
|
|
|
130
130
|
- [Scipy](https://www.scipy.org/) ≥ 1.14.0
|
|
131
131
|
- [Matplotlib](https://matplotlib.org/) ≥ 3.9.1
|
|
132
132
|
- [Pydantic](https://docs.pydantic.dev/latest/) ≥ 2.11.7
|
|
133
|
-
- [Snowpylot](https://github.com/connellymk/snowpylot) ≥ 1.1.
|
|
133
|
+
- [Snowpylot](https://github.com/connellymk/snowpylot) ≥ 1.1.5
|
|
134
|
+
- [snowmicropyn](https://github.com/slf-dot-ch/snowmicropyn) ≥ 1.4.0
|
|
134
135
|
|
|
135
136
|
<!-- DEVELOPMENT SETUP -->
|
|
136
137
|
## Development Setup
|
|
@@ -365,6 +366,44 @@ u_bot = skier_system.fq.u(Z=z_skier, h0=bot, unit='um')
|
|
|
365
366
|
psi = skier_system.fq.psi(Z=z_skier, unit='deg')
|
|
366
367
|
```
|
|
367
368
|
|
|
369
|
+
### Parsing field profiles into layers
|
|
370
|
+
|
|
371
|
+
Instead of hand-building `Layer` lists, WEAC ships parsers in `weac.parser` that convert common field-profile formats into slope-normal WEAC layers (ordered surface → ground). All parsers expose `extract_layers(...)` returning `(layers, density_methods)`, so the output plugs directly into `ModelInput`. See [`demo/parser_demo.ipynb`](https://github.com/2phi/weac/blob/main/demo/parser_demo.ipynb) for side-by-side plots of the three parsers on a co-located Weissfluhjoch pit.
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
from weac.parser import SMPParser, SnowScopeParser, SnowPilotParser
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**SnowMicroPen** (`.PNT`) — `SMPParser` derives density and SSA from the force signal via [snowmicropyn](https://github.com/slf-dot-ch/snowmicropyn)'s Löwe 2012 shot-noise model plus a parameterization (`P2015`, `CR2020`, `K2020a`, `K2020b`). Surface and ground are auto-detected at construction.
|
|
378
|
+
|
|
379
|
+
```python
|
|
380
|
+
smp = SMPParser("profile.PNT", density_method="CR2020")
|
|
381
|
+
# "bin" groups at a fixed thickness; "gradient" cuts on density-gradient jumps
|
|
382
|
+
layers, density_methods = smp.extract_layers(method="bin", layer_thickness_mm=20.0)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**SnowScope** (`.csv`) — `SnowScopeParser` reads penetration hardness (kPa) and derives density from a semilog model `D = a·ln(F) + b` (default `HAGENMULLER2018`). It exposes the same `extract_layers` interface as the SMP parser.
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
scope = SnowScopeParser("profile.csv", density_method="HAGENMULLER2018")
|
|
389
|
+
layers, density_methods = scope.extract_layers(method="gradient", gradient_threshold=12.0)
|
|
390
|
+
|
|
391
|
+
scope_custom = SnowScopeParser(
|
|
392
|
+
"profile.csv",
|
|
393
|
+
semilog_slope=50.0,
|
|
394
|
+
semilog_intercept=100.0,
|
|
395
|
+
)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**SnowPilot / CAAML** (`.xml`) — `SnowPilotParser` uses [snowpylot](https://github.com/connellymk/snowpylot) to read CAAML snowpits, emitting hand-hardness / density-observation layers. Densities come from measured observations where available, falling back to the Geldsetzer estimate from grain type and hand hardness.
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
pilot = SnowPilotParser("profile.xml")
|
|
402
|
+
# pass a known slope angle to scale plumb depths to slope-normal, e.g.:
|
|
403
|
+
# pilot.extract_layers(slope_angle_deg=pilot.pit_slope_angle_deg() or 0.0)
|
|
404
|
+
layers, density_methods = pilot.extract_layers()
|
|
405
|
+
```
|
|
406
|
+
|
|
368
407
|
<!-- ROADMAP -->
|
|
369
408
|
## Roadmap
|
|
370
409
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "weac"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.3.0"
|
|
8
8
|
authors = [{ name = "2phi GbR", email = "mail@2phi.de" }]
|
|
9
9
|
description = "Weak layer anticrack nucleation model"
|
|
10
10
|
readme = "README.md"
|
|
@@ -24,6 +24,7 @@ dependencies = [
|
|
|
24
24
|
"scipy>=1.14.0",
|
|
25
25
|
"pydantic>=2.11.7",
|
|
26
26
|
"snowpylot>=1.1.5",
|
|
27
|
+
"snowmicropyn>=1.4.0",
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
[project.urls]
|
|
@@ -67,7 +68,7 @@ dev = [
|
|
|
67
68
|
|
|
68
69
|
# Linters/formatters aligned with configured tools
|
|
69
70
|
"ruff>=0.4.0",
|
|
70
|
-
"pylint>=3.
|
|
71
|
+
"pylint>=3.3.0",
|
|
71
72
|
"pycodestyle>=2.11.1",
|
|
72
73
|
"black>=24.4.0",
|
|
73
74
|
"isort>=5.13.0",
|
|
@@ -132,7 +133,7 @@ ignore = [
|
|
|
132
133
|
]
|
|
133
134
|
|
|
134
135
|
[tool.bumpversion]
|
|
135
|
-
current_version = "3.
|
|
136
|
+
current_version = "3.3.0"
|
|
136
137
|
|
|
137
138
|
[[tool.bumpversion.files]]
|
|
138
139
|
filename = "pyproject.toml"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Parsers that convert field-profile formats into WEAC layers."""
|
|
2
|
+
|
|
3
|
+
from weac.parser.smp_parser import SMPParser, SMPProfile
|
|
4
|
+
from weac.parser.snowpilot_parser import SnowPilotParser
|
|
5
|
+
from weac.parser.snowscope_parser import SnowScopeParser, SnowScopeProfile
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"SMPParser",
|
|
9
|
+
"SMPProfile",
|
|
10
|
+
"SnowPilotParser",
|
|
11
|
+
"SnowScopeParser",
|
|
12
|
+
"SnowScopeProfile",
|
|
13
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Debug plotting for penetration-resistance-first parsers (SMP, SnowScope)."""
|
|
2
|
+
|
|
3
|
+
from weac.parser.plotting.force_penetration_layers import ForcePenetrationProfile
|
|
4
|
+
from weac.parser.plotting.force_penetration_plot import (
|
|
5
|
+
ForcePenetrationParser,
|
|
6
|
+
plot_force_penetration_layers,
|
|
7
|
+
plot_force_penetration_parser,
|
|
8
|
+
plot_smp_penetration_parser,
|
|
9
|
+
plot_snowscope_penetration_parser,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ForcePenetrationParser",
|
|
14
|
+
"ForcePenetrationProfile",
|
|
15
|
+
"plot_force_penetration_layers",
|
|
16
|
+
"plot_force_penetration_parser",
|
|
17
|
+
"plot_smp_penetration_parser",
|
|
18
|
+
"plot_snowscope_penetration_parser",
|
|
19
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Shared four-panel penetration-resistance / density / layer plots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import numpy as np
|
|
10
|
+
from matplotlib.figure import Figure
|
|
11
|
+
|
|
12
|
+
from weac.components import Layer
|
|
13
|
+
from weac.parser.utils import plumb_to_slope_normal
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ForcePenetrationProfile(Protocol):
|
|
17
|
+
"""Profile from SMP or SnowScope (resistance–depth signal plus derived density)."""
|
|
18
|
+
|
|
19
|
+
depth_mm: np.ndarray
|
|
20
|
+
penetration_resistance_kPa: np.ndarray
|
|
21
|
+
density_kg_m3: np.ndarray
|
|
22
|
+
density_method: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _layer_staircase(
|
|
26
|
+
layers: list[Layer], surface_mm: float
|
|
27
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
28
|
+
"""Return (density, depth) staircase points for a top-down layer list.
|
|
29
|
+
|
|
30
|
+
Boundaries accumulate ``Layer.h`` from ``surface_mm`` (the profile's first
|
|
31
|
+
sample depth). Consecutive vertical segments share a boundary depth, so a
|
|
32
|
+
single ``plot`` call renders them as a density-vs-depth staircase. Because
|
|
33
|
+
``Layer.h`` is slope-normal, the staircase belongs on the slope-normal axis.
|
|
34
|
+
"""
|
|
35
|
+
if not layers:
|
|
36
|
+
return np.array([]), np.array([])
|
|
37
|
+
thicknesses = np.array([layer.h for layer in layers], dtype=np.float64)
|
|
38
|
+
densities = np.array([layer.rho for layer in layers], dtype=np.float64)
|
|
39
|
+
tops = surface_mm + np.concatenate(([0.0], np.cumsum(thicknesses)[:-1]))
|
|
40
|
+
bottoms = surface_mm + np.cumsum(thicknesses)
|
|
41
|
+
rho = np.repeat(densities, 2)
|
|
42
|
+
depth = np.empty(rho.size, dtype=np.float64)
|
|
43
|
+
depth[0::2] = tops
|
|
44
|
+
depth[1::2] = bottoms
|
|
45
|
+
return rho, depth
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def plot_force_penetration_layers(
|
|
49
|
+
profile: ForcePenetrationProfile,
|
|
50
|
+
layers: list[Layer],
|
|
51
|
+
*,
|
|
52
|
+
slope_angle_deg: float = 0.0,
|
|
53
|
+
title: str | None = None,
|
|
54
|
+
layer_label: str = "layers",
|
|
55
|
+
save_path: str | Path | None = None,
|
|
56
|
+
show: bool = True,
|
|
57
|
+
figsize: tuple[float, float] = (13.0, 6.5),
|
|
58
|
+
) -> Figure:
|
|
59
|
+
"""Plot penetration resistance, density, slope-normal density, and layers on a depth axis.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
profile: :class:`~weac.parser.smp_parser.SMPProfile` or
|
|
63
|
+
:class:`~weac.parser.snowscope_parser.SnowScopeProfile` (must expose
|
|
64
|
+
``depth_mm``, ``penetration_resistance_kPa``, ``density_kg_m3``,
|
|
65
|
+
``density_method``).
|
|
66
|
+
layers: WEAC layers ordered surface -> ground (from ``extract_layers``).
|
|
67
|
+
``Layer.h`` is slope-normal, so the reconstructed staircase is drawn
|
|
68
|
+
on the slope-normal axis (panels 3-4). Pass the same
|
|
69
|
+
``slope_angle_deg`` used to extract the layers so the slope-normal
|
|
70
|
+
density panel and the staircase share one axis. The staircase bottom
|
|
71
|
+
overshoots the last sample by one cell because the binning helpers
|
|
72
|
+
repeat the final sample spacing (an intentional part of their cell
|
|
73
|
+
model, see ``layer_binning``).
|
|
74
|
+
slope_angle_deg: Slope angle [deg] used when the layers were extracted;
|
|
75
|
+
sets the ``cos(phi)`` compression of the slope-normal depth axis.
|
|
76
|
+
title: Figure suptitle; defaults to the density method name.
|
|
77
|
+
layer_label: Legend/title label for the segmented panel.
|
|
78
|
+
save_path: If given, save the figure (PNG) to this path.
|
|
79
|
+
show: Call ``plt.show()`` before returning (default ``True``).
|
|
80
|
+
figsize: Figure size in inches.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The created :class:`~matplotlib.figure.Figure`.
|
|
84
|
+
"""
|
|
85
|
+
depth = np.asarray(profile.depth_mm, dtype=np.float64)
|
|
86
|
+
resistance = np.asarray(profile.penetration_resistance_kPa, dtype=np.float64)
|
|
87
|
+
density = np.asarray(profile.density_kg_m3, dtype=np.float64)
|
|
88
|
+
surface_mm = float(depth[0]) if depth.size else 0.0
|
|
89
|
+
|
|
90
|
+
phi = float(slope_angle_deg)
|
|
91
|
+
scale = plumb_to_slope_normal(phi) if phi != 0.0 else 1.0
|
|
92
|
+
# Anchor at the surface; compress only the below-surface span (matches how
|
|
93
|
+
# the layer staircase accumulates slope-normal thicknesses from surface_mm).
|
|
94
|
+
depth_sn = surface_mm + (depth - surface_mm) * scale
|
|
95
|
+
rho_step, depth_step = _layer_staircase(layers, surface_mm)
|
|
96
|
+
|
|
97
|
+
fig, (ax_resistance, ax_density, ax_density_sn, ax_layers) = plt.subplots(
|
|
98
|
+
1, 4, figsize=figsize, sharey=True
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
ax_resistance.plot(resistance, depth, color="#004E8A", lw=0.9)
|
|
102
|
+
ax_resistance.set_xlabel("Penetration resistance [kPa]")
|
|
103
|
+
ax_resistance.set_ylabel("Depth [mm]")
|
|
104
|
+
ax_resistance.set_title("Penetration resistance\n(plumb)")
|
|
105
|
+
|
|
106
|
+
ax_density.plot(density, depth, color="#00689D", lw=0.9)
|
|
107
|
+
ax_density.set_xlabel("Density [kg m$^{-3}$]")
|
|
108
|
+
ax_density.set_title("Sample density\n(plumb)")
|
|
109
|
+
|
|
110
|
+
ax_density_sn.plot(density, depth_sn, color="#009D81", lw=0.9)
|
|
111
|
+
ax_density_sn.set_xlabel("Density [kg m$^{-3}$]")
|
|
112
|
+
ax_density_sn.set_title(f"Sample density\n(slope-normal, φ={phi:g}°)")
|
|
113
|
+
|
|
114
|
+
# Segmented layers (slope-normal) with the slope-normal density overlaid.
|
|
115
|
+
ax_layers.plot(
|
|
116
|
+
density,
|
|
117
|
+
depth_sn,
|
|
118
|
+
color="#B5B5B5",
|
|
119
|
+
lw=0.8,
|
|
120
|
+
alpha=0.7,
|
|
121
|
+
label="sample density",
|
|
122
|
+
)
|
|
123
|
+
if rho_step.size:
|
|
124
|
+
ax_layers.plot(rho_step, depth_step, color="#EC6500", lw=1.6, label=layer_label)
|
|
125
|
+
# Boundary ticks (every other point is a shared top/bottom depth).
|
|
126
|
+
for boundary in depth_step[1::2]:
|
|
127
|
+
ax_layers.axhline(boundary, color="#EC6500", lw=0.3, alpha=0.3)
|
|
128
|
+
ax_layers.set_xlabel("Density [kg m$^{-3}$]")
|
|
129
|
+
ax_layers.set_title(f"Segmented {layer_label}\n(slope-normal, n={len(layers)})")
|
|
130
|
+
ax_layers.legend(loc="lower right", fontsize=8, frameon=False)
|
|
131
|
+
|
|
132
|
+
# Depth increases downward: surface at the top. Plumb depth is the deepest.
|
|
133
|
+
if depth.size:
|
|
134
|
+
max_depth = float(np.nanmax(depth))
|
|
135
|
+
if depth_step.size:
|
|
136
|
+
max_depth = max(max_depth, float(np.nanmax(depth_step)))
|
|
137
|
+
ax_resistance.set_ylim(max_depth, surface_mm)
|
|
138
|
+
for ax in (ax_resistance, ax_density, ax_density_sn, ax_layers):
|
|
139
|
+
ax.grid(True, alpha=0.25, lw=0.4)
|
|
140
|
+
|
|
141
|
+
fig.suptitle(title or f"Penetration resistance profile ({profile.density_method})")
|
|
142
|
+
fig.tight_layout()
|
|
143
|
+
|
|
144
|
+
if save_path is not None:
|
|
145
|
+
fig.savefig(save_path, dpi=200, bbox_inches="tight", facecolor="white")
|
|
146
|
+
if show:
|
|
147
|
+
plt.show()
|
|
148
|
+
return fig
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""SMP and SnowScope penetration-resistance debug plotting (unified entry points).
|
|
2
|
+
|
|
3
|
+
Penetration parsers record **penetration resistance first**, then derive sample
|
|
4
|
+
density (SnowMicroPen via snowmicropyn; SnowScope via a semilog hardness model).
|
|
5
|
+
SnowPilot pits are layer lists only — they do not expose this resistance–depth
|
|
6
|
+
signal, so this package does not apply to
|
|
7
|
+
:class:`~weac.parser.snowpilot_parser.SnowPilotParser`.
|
|
8
|
+
|
|
9
|
+
Four depth-aligned panels (see :func:`plot_force_penetration_layers`):
|
|
10
|
+
|
|
11
|
+
1. Penetration resistance vs plumb depth.
|
|
12
|
+
2. Sample density vs plumb depth.
|
|
13
|
+
3. Sample density vs slope-normal depth (plumb depth compressed by ``cos(phi)``).
|
|
14
|
+
4. Segmented WEAC layers (``bin`` or ``gradient``) vs slope-normal depth, with
|
|
15
|
+
the slope-normal density faintly overlaid.
|
|
16
|
+
|
|
17
|
+
Parser-specific extract-and-plot helpers live in
|
|
18
|
+
:mod:`weac.parser.plotting.smp_penetration_plot` and
|
|
19
|
+
:mod:`weac.parser.plotting.snowscope_penetration_plot`.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Literal, Protocol
|
|
25
|
+
|
|
26
|
+
from matplotlib.figure import Figure
|
|
27
|
+
|
|
28
|
+
from weac.components import Layer
|
|
29
|
+
from weac.parser.plotting.force_penetration_layers import (
|
|
30
|
+
ForcePenetrationProfile,
|
|
31
|
+
plot_force_penetration_layers,
|
|
32
|
+
)
|
|
33
|
+
from weac.parser.plotting.smp_penetration_plot import plot_smp_penetration_parser
|
|
34
|
+
from weac.parser.plotting.snowscope_penetration_plot import (
|
|
35
|
+
plot_snowscope_penetration_parser,
|
|
36
|
+
)
|
|
37
|
+
from weac.parser.smp_parser import SMPParser
|
|
38
|
+
from weac.parser.snowscope_parser import SnowScopeParser
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"ForcePenetrationParser",
|
|
42
|
+
"ForcePenetrationProfile",
|
|
43
|
+
"plot_force_penetration_layers",
|
|
44
|
+
"plot_force_penetration_parser",
|
|
45
|
+
"plot_smp_penetration_parser",
|
|
46
|
+
"plot_snowscope_penetration_parser",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ForcePenetrationParser(Protocol):
|
|
51
|
+
"""SMP or SnowScope parser with ``extract_profile`` / ``extract_layers``."""
|
|
52
|
+
|
|
53
|
+
def extract_profile(self, **kwargs) -> ForcePenetrationProfile: ...
|
|
54
|
+
|
|
55
|
+
def extract_layers(self, *args, **kwargs) -> tuple[list[Layer], list[str]]: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def plot_force_penetration_parser(
|
|
59
|
+
parser: ForcePenetrationParser,
|
|
60
|
+
*,
|
|
61
|
+
method: Literal["bin", "gradient"] = "bin",
|
|
62
|
+
slope_angle_deg: float = 0.0,
|
|
63
|
+
density_method: str | None = None,
|
|
64
|
+
semilog_slope: float | None = None,
|
|
65
|
+
semilog_intercept: float | None = None,
|
|
66
|
+
layer_thickness_mm: float | None = None,
|
|
67
|
+
gradient_threshold: float = 12.0,
|
|
68
|
+
**plot_kwargs,
|
|
69
|
+
) -> Figure:
|
|
70
|
+
"""Extract from an SMP or SnowScope parser and plot the debug view.
|
|
71
|
+
|
|
72
|
+
Dispatches to :func:`plot_smp_penetration_parser` or
|
|
73
|
+
:func:`plot_snowscope_penetration_parser`. ``semilog_slope`` /
|
|
74
|
+
``semilog_intercept`` apply only to SnowScope. ``**plot_kwargs`` (e.g.
|
|
75
|
+
``title``, ``save_path``, ``show``) pass through to
|
|
76
|
+
:func:`plot_force_penetration_layers`.
|
|
77
|
+
"""
|
|
78
|
+
if isinstance(parser, SnowScopeParser):
|
|
79
|
+
return plot_snowscope_penetration_parser(
|
|
80
|
+
parser,
|
|
81
|
+
method=method,
|
|
82
|
+
slope_angle_deg=slope_angle_deg,
|
|
83
|
+
density_method=density_method, # type: ignore[arg-type]
|
|
84
|
+
semilog_slope=semilog_slope,
|
|
85
|
+
semilog_intercept=semilog_intercept,
|
|
86
|
+
layer_thickness_mm=layer_thickness_mm,
|
|
87
|
+
gradient_threshold=gradient_threshold,
|
|
88
|
+
**plot_kwargs,
|
|
89
|
+
)
|
|
90
|
+
if isinstance(parser, SMPParser):
|
|
91
|
+
if semilog_slope is not None or semilog_intercept is not None:
|
|
92
|
+
raise TypeError(
|
|
93
|
+
"semilog_slope and semilog_intercept apply only to SnowScopeParser"
|
|
94
|
+
)
|
|
95
|
+
return plot_smp_penetration_parser(
|
|
96
|
+
parser,
|
|
97
|
+
method=method,
|
|
98
|
+
slope_angle_deg=slope_angle_deg,
|
|
99
|
+
density_method=density_method, # type: ignore[arg-type]
|
|
100
|
+
layer_thickness_mm=layer_thickness_mm,
|
|
101
|
+
gradient_threshold=gradient_threshold,
|
|
102
|
+
**plot_kwargs,
|
|
103
|
+
)
|
|
104
|
+
raise TypeError(
|
|
105
|
+
"plot_force_penetration_parser expects SMPParser or SnowScopeParser, "
|
|
106
|
+
f"got {type(parser).__name__}"
|
|
107
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Extract-and-plot helper for :class:`~weac.parser.smp_parser.SMPParser`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from matplotlib.figure import Figure
|
|
8
|
+
|
|
9
|
+
from weac.parser.plotting.force_penetration_layers import plot_force_penetration_layers
|
|
10
|
+
from weac.parser.smp_parser import DensityMethod, SMPParser
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def plot_smp_penetration_parser(
|
|
14
|
+
parser: SMPParser,
|
|
15
|
+
*,
|
|
16
|
+
method: Literal["bin", "gradient"] = "bin",
|
|
17
|
+
slope_angle_deg: float = 0.0,
|
|
18
|
+
density_method: DensityMethod | None = None,
|
|
19
|
+
layer_thickness_mm: float | None = None,
|
|
20
|
+
gradient_threshold: float = 12.0,
|
|
21
|
+
**plot_kwargs,
|
|
22
|
+
) -> Figure:
|
|
23
|
+
"""Extract from an SMP parser and plot the penetration-resistance debug view."""
|
|
24
|
+
profile = parser.extract_profile(density_method=density_method)
|
|
25
|
+
layers, _ = parser.extract_layers(
|
|
26
|
+
slope_angle_deg,
|
|
27
|
+
method=method,
|
|
28
|
+
density_method=density_method,
|
|
29
|
+
layer_thickness_mm=layer_thickness_mm,
|
|
30
|
+
gradient_threshold=gradient_threshold,
|
|
31
|
+
)
|
|
32
|
+
plot_kwargs.setdefault("layer_label", method)
|
|
33
|
+
plot_kwargs.setdefault("slope_angle_deg", slope_angle_deg)
|
|
34
|
+
return plot_force_penetration_layers(profile, layers, **plot_kwargs)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Extract-and-plot helper for :class:`~weac.parser.snowscope_parser.SnowScopeParser`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from matplotlib.figure import Figure
|
|
8
|
+
|
|
9
|
+
from weac.parser.plotting.force_penetration_layers import plot_force_penetration_layers
|
|
10
|
+
from weac.parser.snowscope_parser import DensityMethod, SnowScopeParser
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def plot_snowscope_penetration_parser(
|
|
14
|
+
parser: SnowScopeParser,
|
|
15
|
+
*,
|
|
16
|
+
method: Literal["bin", "gradient"] = "bin",
|
|
17
|
+
slope_angle_deg: float = 0.0,
|
|
18
|
+
density_method: DensityMethod | None = None,
|
|
19
|
+
semilog_slope: float | None = None,
|
|
20
|
+
semilog_intercept: float | None = None,
|
|
21
|
+
layer_thickness_mm: float | None = None,
|
|
22
|
+
gradient_threshold: float = 12.0,
|
|
23
|
+
**plot_kwargs,
|
|
24
|
+
) -> Figure:
|
|
25
|
+
"""Extract from a SnowScope parser and plot the penetration-resistance debug view."""
|
|
26
|
+
profile = parser.extract_profile(
|
|
27
|
+
density_method=density_method,
|
|
28
|
+
semilog_slope=semilog_slope,
|
|
29
|
+
semilog_intercept=semilog_intercept,
|
|
30
|
+
)
|
|
31
|
+
layers, _ = parser.extract_layers(
|
|
32
|
+
slope_angle_deg,
|
|
33
|
+
method=method,
|
|
34
|
+
density_method=density_method,
|
|
35
|
+
semilog_slope=semilog_slope,
|
|
36
|
+
semilog_intercept=semilog_intercept,
|
|
37
|
+
layer_thickness_mm=layer_thickness_mm,
|
|
38
|
+
gradient_threshold=gradient_threshold,
|
|
39
|
+
)
|
|
40
|
+
plot_kwargs.setdefault("layer_label", method)
|
|
41
|
+
plot_kwargs.setdefault("slope_angle_deg", slope_angle_deg)
|
|
42
|
+
return plot_force_penetration_layers(profile, layers, **plot_kwargs)
|