shamash-orogen 0.2.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.
- shamash_orogen-0.2.0/PKG-INFO +101 -0
- shamash_orogen-0.2.0/README.md +91 -0
- shamash_orogen-0.2.0/pyproject.toml +21 -0
- shamash_orogen-0.2.0/setup.cfg +4 -0
- shamash_orogen-0.2.0/src/orogen/__init__.py +1 -0
- shamash_orogen-0.2.0/src/orogen/__main__.py +5 -0
- shamash_orogen-0.2.0/src/orogen/boundaries.py +251 -0
- shamash_orogen-0.2.0/src/orogen/cli.py +155 -0
- shamash_orogen-0.2.0/src/orogen/climate.py +722 -0
- shamash_orogen-0.2.0/src/orogen/dem.py +120 -0
- shamash_orogen-0.2.0/src/orogen/grid.py +150 -0
- shamash_orogen-0.2.0/src/orogen/history.py +25 -0
- shamash_orogen-0.2.0/src/orogen/koppen.py +107 -0
- shamash_orogen-0.2.0/src/orogen/noise.py +99 -0
- shamash_orogen-0.2.0/src/orogen/ocean.py +161 -0
- shamash_orogen-0.2.0/src/orogen/orogeny.py +124 -0
- shamash_orogen-0.2.0/src/orogen/phase.py +51 -0
- shamash_orogen-0.2.0/src/orogen/reconstruction.py +188 -0
- shamash_orogen-0.2.0/src/orogen/ridges.py +106 -0
- shamash_orogen-0.2.0/src/orogen/rotation.py +166 -0
- shamash_orogen-0.2.0/src/orogen/seafloor.py +75 -0
- shamash_orogen-0.2.0/src/orogen/tiling.py +160 -0
- shamash_orogen-0.2.0/src/shamash_orogen.egg-info/PKG-INFO +101 -0
- shamash_orogen-0.2.0/src/shamash_orogen.egg-info/SOURCES.txt +26 -0
- shamash_orogen-0.2.0/src/shamash_orogen.egg-info/dependency_links.txt +1 -0
- shamash_orogen-0.2.0/src/shamash_orogen.egg-info/entry_points.txt +2 -0
- shamash_orogen-0.2.0/src/shamash_orogen.egg-info/requires.txt +2 -0
- shamash_orogen-0.2.0/src/shamash_orogen.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shamash-orogen
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Algorithms that turn a plate-tectonic history into terrain: seafloor age and depth, collision history, and from there a global DEM.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: shapely>=2
|
|
10
|
+
|
|
11
|
+
# shamash-orogen
|
|
12
|
+
|
|
13
|
+
Algorithms that turn a plate-tectonic history into terrain: the
|
|
14
|
+
reconstruction of the plates themselves, and from it global lon/lat grids that
|
|
15
|
+
GDAL and QGIS read directly. Its input is one tectonics phase as
|
|
16
|
+
[shamash-core](https://codeberg.org/schobernoise/shamash-core) lays it out
|
|
17
|
+
(`<body>/tectonics/phase-N/`: the plates and continents as drawn and every
|
|
18
|
+
plate's Euler pole; then a snapshot of them every few Myr, and the present with
|
|
19
|
+
new seafloor shared along mid-ocean ridges). shamash-core depends on it and
|
|
20
|
+
calls it in-process.
|
|
21
|
+
|
|
22
|
+
Plain Python with numpy and shapely; it never imports GDAL, QGIS or pygplates:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv pip install --python ~/Dev/projects/shamash/shamash-core/venv/bin/python -e ~/Dev/projects/shamash-orogen
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Algorithms
|
|
29
|
+
|
|
30
|
+
One verb each, all `python -m orogen VERB PHASE OUT [--resolution DEG] [--crs WKTFILE]`
|
|
31
|
+
but `reconstruct`, which writes into the phase:
|
|
32
|
+
|
|
33
|
+
- **`reconstruct`**: the plates drawn at `duration` Ma turned rigidly about
|
|
34
|
+
their Euler poles at a constant rate to 0 Ma (Rodrigues' rotation, Cox &
|
|
35
|
+
Hart 1986), cut at the antimeridian and closed through a pole a plate comes
|
|
36
|
+
to cover; continents split along the plates and carried by them; slivers of
|
|
37
|
+
imperfect snapping resolved; the overlap and gap reported. Writes
|
|
38
|
+
`reconstructed/{plates,continents}_<t>Ma.geojson` every `step` Myr and
|
|
39
|
+
`next/`, the present made to tile the globe again (overlaps to the plate
|
|
40
|
+
with more continent, gaps shared along mid-ocean ridges). ea's 19 plates,
|
|
41
|
+
21 snapshots: 4 s.
|
|
42
|
+
- **`seafloor`**: seafloor age and depth at the present. Within each plate's
|
|
43
|
+
half of an opened ocean, age runs from 0 at the ridge to the phase's duration
|
|
44
|
+
at the plate's edge; ocean floor older than the phase is assumed `duration +
|
|
45
|
+
--initial-age` (60 Myr). Depth from age by GDH1 (Stein & Stein 1992).
|
|
46
|
+
Writes `seafloor_age.asc`, `seafloor_depth.asc`.
|
|
47
|
+
- **`history`**: where the action was. On a fixed grid, the Myr each cell lay
|
|
48
|
+
under two or more plates (convergence) and under none (divergence) over the
|
|
49
|
+
phase. Writes `overlap_myr.asc`, `gap_myr.asc`.
|
|
50
|
+
- **`boundaries`**: the present plate boundaries every 0.5°, classified from
|
|
51
|
+
the two plates' Euler-pole velocities and the crust on each side:
|
|
52
|
+
collision, subduction, island arc, rift, ridge, transform; with the
|
|
53
|
+
shortening each has seen (closing rate × duration). Writes
|
|
54
|
+
`boundaries.geojson`.
|
|
55
|
+
- **`orogeny`**: the relief the boundaries raise or cut, Earth-shaped
|
|
56
|
+
heuristic profiles across each kind (flat-topped collision ranges, trench and
|
|
57
|
+
cordillera, trench and island arc, rift valley). Writes `relief.asc`.
|
|
58
|
+
- **`dem`**: everything composed: continental lowlands rising inland, shelf
|
|
59
|
+
and slope down to the age-depth ocean floor, the boundaries' relief, and
|
|
60
|
+
fractal noise on the sphere, rougher where the ground was raised; relief and
|
|
61
|
+
noise scaled by Earth's gravity over `--gravity`, clipped to `--range`.
|
|
62
|
+
Writes `dem.png` (16-bit, `--range` spans black to white), `dem_8bit.png`
|
|
63
|
+
(the same rounded away from sea level, for a drawing program to reproduce
|
|
64
|
+
exactly) and `dem.json`,
|
|
65
|
+
and with `--asc` the float grid `dem.asc`. ea's phase 1 at 0.1°: 2 minutes.
|
|
66
|
+
|
|
67
|
+
- **`climate`** (`orogen climate DEM_PNG OUT --planet planet.json --range LOW
|
|
68
|
+
HIGH`): a body's climate from its heights and the parameters shamash-core
|
|
69
|
+
writes to `climate/planet.json`. A seasonal energy balance model on the
|
|
70
|
+
lon/lat grid (outgoing radiation linearised about the body's own
|
|
71
|
+
equilibrium, moist and anisotropic heat transport, ice that brightens the
|
|
72
|
+
albedo while its global mean stays the body's Bond albedo), Hadley cells
|
|
73
|
+
from Held & Hou scaled to Earth's 30 deg, an ITCZ that follows the warmest
|
|
74
|
+
air, rain at the ITCZ and along storm tracks, carried upwind over land by a
|
|
75
|
+
semi-Lagrangian sweep, and the sea's heat carried by wind-driven gyres
|
|
76
|
+
(`ocean.py`, the Stommel model: western boundary currents, coastal
|
|
77
|
+
upwelling). Writes monthly-derived temperatures, precipitation,
|
|
78
|
+
Köppen-Geiger classes (`koppen.py`, Kottek et al. 2006, coloured as Beck et
|
|
79
|
+
al. 2018), solstice winds, ocean currents and `climate.json`. Earth at
|
|
80
|
+
1 deg: 23 s, 64% of land cells in the right main class.
|
|
81
|
+
|
|
82
|
+
`--crs` takes the body's geographic CRS as WKT (shamash-core's
|
|
83
|
+
`qgis/project.json` holds it) and writes it beside each grid as a `.prj`.
|
|
84
|
+
|
|
85
|
+
## Roadmap
|
|
86
|
+
|
|
87
|
+
- [x] Seafloor age and depth, convergence footprint (0.5° in seconds)
|
|
88
|
+
- [x] Plate boundaries classified from relative Euler velocities: collision,
|
|
89
|
+
subduction, island arc, rift, transform
|
|
90
|
+
- [x] Mountain, trench and arc profiles from them
|
|
91
|
+
- [x] One global DEM at 0.1°, as a 16-bit PNG
|
|
92
|
+
- [ ] Erosion: the ranges are smooth heuristic profiles plus noise, with no
|
|
93
|
+
valleys cut by water
|
|
94
|
+
- [ ] More than one phase: ocean floor and mountains carried from phase to
|
|
95
|
+
phase instead of each phase starting fresh
|
|
96
|
+
- [x] `shamash map terrain`, writing the inputs and running this
|
|
97
|
+
- [x] The reconstruction itself, replacing pygplates (2026-10-16)
|
|
98
|
+
- [x] A climate model with Köppen-Geiger classes (2026-10-16)
|
|
99
|
+
- [x] Wind-driven ocean currents and coastal upwelling (2026-10-16)
|
|
100
|
+
- [ ] Climate: monsoons, a North Atlantic Drift that reaches Norway, cloud-albedo
|
|
101
|
+
- [ ] Release to PyPI, since shamash-core depends on it
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# shamash-orogen
|
|
2
|
+
|
|
3
|
+
Algorithms that turn a plate-tectonic history into terrain: the
|
|
4
|
+
reconstruction of the plates themselves, and from it global lon/lat grids that
|
|
5
|
+
GDAL and QGIS read directly. Its input is one tectonics phase as
|
|
6
|
+
[shamash-core](https://codeberg.org/schobernoise/shamash-core) lays it out
|
|
7
|
+
(`<body>/tectonics/phase-N/`: the plates and continents as drawn and every
|
|
8
|
+
plate's Euler pole; then a snapshot of them every few Myr, and the present with
|
|
9
|
+
new seafloor shared along mid-ocean ridges). shamash-core depends on it and
|
|
10
|
+
calls it in-process.
|
|
11
|
+
|
|
12
|
+
Plain Python with numpy and shapely; it never imports GDAL, QGIS or pygplates:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv pip install --python ~/Dev/projects/shamash/shamash-core/venv/bin/python -e ~/Dev/projects/shamash-orogen
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Algorithms
|
|
19
|
+
|
|
20
|
+
One verb each, all `python -m orogen VERB PHASE OUT [--resolution DEG] [--crs WKTFILE]`
|
|
21
|
+
but `reconstruct`, which writes into the phase:
|
|
22
|
+
|
|
23
|
+
- **`reconstruct`**: the plates drawn at `duration` Ma turned rigidly about
|
|
24
|
+
their Euler poles at a constant rate to 0 Ma (Rodrigues' rotation, Cox &
|
|
25
|
+
Hart 1986), cut at the antimeridian and closed through a pole a plate comes
|
|
26
|
+
to cover; continents split along the plates and carried by them; slivers of
|
|
27
|
+
imperfect snapping resolved; the overlap and gap reported. Writes
|
|
28
|
+
`reconstructed/{plates,continents}_<t>Ma.geojson` every `step` Myr and
|
|
29
|
+
`next/`, the present made to tile the globe again (overlaps to the plate
|
|
30
|
+
with more continent, gaps shared along mid-ocean ridges). ea's 19 plates,
|
|
31
|
+
21 snapshots: 4 s.
|
|
32
|
+
- **`seafloor`**: seafloor age and depth at the present. Within each plate's
|
|
33
|
+
half of an opened ocean, age runs from 0 at the ridge to the phase's duration
|
|
34
|
+
at the plate's edge; ocean floor older than the phase is assumed `duration +
|
|
35
|
+
--initial-age` (60 Myr). Depth from age by GDH1 (Stein & Stein 1992).
|
|
36
|
+
Writes `seafloor_age.asc`, `seafloor_depth.asc`.
|
|
37
|
+
- **`history`**: where the action was. On a fixed grid, the Myr each cell lay
|
|
38
|
+
under two or more plates (convergence) and under none (divergence) over the
|
|
39
|
+
phase. Writes `overlap_myr.asc`, `gap_myr.asc`.
|
|
40
|
+
- **`boundaries`**: the present plate boundaries every 0.5°, classified from
|
|
41
|
+
the two plates' Euler-pole velocities and the crust on each side:
|
|
42
|
+
collision, subduction, island arc, rift, ridge, transform; with the
|
|
43
|
+
shortening each has seen (closing rate × duration). Writes
|
|
44
|
+
`boundaries.geojson`.
|
|
45
|
+
- **`orogeny`**: the relief the boundaries raise or cut, Earth-shaped
|
|
46
|
+
heuristic profiles across each kind (flat-topped collision ranges, trench and
|
|
47
|
+
cordillera, trench and island arc, rift valley). Writes `relief.asc`.
|
|
48
|
+
- **`dem`**: everything composed: continental lowlands rising inland, shelf
|
|
49
|
+
and slope down to the age-depth ocean floor, the boundaries' relief, and
|
|
50
|
+
fractal noise on the sphere, rougher where the ground was raised; relief and
|
|
51
|
+
noise scaled by Earth's gravity over `--gravity`, clipped to `--range`.
|
|
52
|
+
Writes `dem.png` (16-bit, `--range` spans black to white), `dem_8bit.png`
|
|
53
|
+
(the same rounded away from sea level, for a drawing program to reproduce
|
|
54
|
+
exactly) and `dem.json`,
|
|
55
|
+
and with `--asc` the float grid `dem.asc`. ea's phase 1 at 0.1°: 2 minutes.
|
|
56
|
+
|
|
57
|
+
- **`climate`** (`orogen climate DEM_PNG OUT --planet planet.json --range LOW
|
|
58
|
+
HIGH`): a body's climate from its heights and the parameters shamash-core
|
|
59
|
+
writes to `climate/planet.json`. A seasonal energy balance model on the
|
|
60
|
+
lon/lat grid (outgoing radiation linearised about the body's own
|
|
61
|
+
equilibrium, moist and anisotropic heat transport, ice that brightens the
|
|
62
|
+
albedo while its global mean stays the body's Bond albedo), Hadley cells
|
|
63
|
+
from Held & Hou scaled to Earth's 30 deg, an ITCZ that follows the warmest
|
|
64
|
+
air, rain at the ITCZ and along storm tracks, carried upwind over land by a
|
|
65
|
+
semi-Lagrangian sweep, and the sea's heat carried by wind-driven gyres
|
|
66
|
+
(`ocean.py`, the Stommel model: western boundary currents, coastal
|
|
67
|
+
upwelling). Writes monthly-derived temperatures, precipitation,
|
|
68
|
+
Köppen-Geiger classes (`koppen.py`, Kottek et al. 2006, coloured as Beck et
|
|
69
|
+
al. 2018), solstice winds, ocean currents and `climate.json`. Earth at
|
|
70
|
+
1 deg: 23 s, 64% of land cells in the right main class.
|
|
71
|
+
|
|
72
|
+
`--crs` takes the body's geographic CRS as WKT (shamash-core's
|
|
73
|
+
`qgis/project.json` holds it) and writes it beside each grid as a `.prj`.
|
|
74
|
+
|
|
75
|
+
## Roadmap
|
|
76
|
+
|
|
77
|
+
- [x] Seafloor age and depth, convergence footprint (0.5° in seconds)
|
|
78
|
+
- [x] Plate boundaries classified from relative Euler velocities: collision,
|
|
79
|
+
subduction, island arc, rift, transform
|
|
80
|
+
- [x] Mountain, trench and arc profiles from them
|
|
81
|
+
- [x] One global DEM at 0.1°, as a 16-bit PNG
|
|
82
|
+
- [ ] Erosion: the ranges are smooth heuristic profiles plus noise, with no
|
|
83
|
+
valleys cut by water
|
|
84
|
+
- [ ] More than one phase: ocean floor and mountains carried from phase to
|
|
85
|
+
phase instead of each phase starting fresh
|
|
86
|
+
- [x] `shamash map terrain`, writing the inputs and running this
|
|
87
|
+
- [x] The reconstruction itself, replacing pygplates (2026-10-16)
|
|
88
|
+
- [x] A climate model with Köppen-Geiger classes (2026-10-16)
|
|
89
|
+
- [x] Wind-driven ocean currents and coastal upwelling (2026-10-16)
|
|
90
|
+
- [ ] Climate: monsoons, a North Atlantic Drift that reaches Norway, cloud-albedo
|
|
91
|
+
- [ ] Release to PyPI, since shamash-core depends on it
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shamash-orogen"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Algorithms that turn a plate-tectonic history into terrain: seafloor age and depth, collision history, and from there a global DEM."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"numpy",
|
|
14
|
+
"shapely>=2",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
orogen = "orogen.cli:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
package-dir = {"" = "src"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Algorithms that turn a plate-tectonic history into terrain."""
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""The present plate boundaries of a phase, sampled and classified.
|
|
2
|
+
|
|
3
|
+
Boundaries are where the plates of next/ meet -- the present with overlaps
|
|
4
|
+
resolved and new seafloor shared along ridges, so the plates tile the globe.
|
|
5
|
+
Every SPACING degrees along each, the two plates' velocities come from their
|
|
6
|
+
Euler poles (v = omega x r, the phase's constant rotations): the part across
|
|
7
|
+
the boundary says whether they close or open, the part along it marks a
|
|
8
|
+
transform. The crust a few hundred km to each side then names the kind:
|
|
9
|
+
|
|
10
|
+
closing continent | continent -> collision
|
|
11
|
+
ocean | continent -> subduction, the ocean going under
|
|
12
|
+
ocean | ocean -> island arc, the older floor going under
|
|
13
|
+
opening continent on either side -> rift; ocean only -> ridge
|
|
14
|
+
sliding -> transform
|
|
15
|
+
|
|
16
|
+
Since the rotations are constant, a boundary point has closed at the same
|
|
17
|
+
rate all phase long: shortening = closing rate x duration (mm/yr x Myr = km).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import dataclasses
|
|
21
|
+
import json
|
|
22
|
+
import math
|
|
23
|
+
from collections import Counter
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
|
|
26
|
+
import numpy
|
|
27
|
+
import shapely
|
|
28
|
+
|
|
29
|
+
from orogen import phase
|
|
30
|
+
|
|
31
|
+
#: Degrees between boundary samples.
|
|
32
|
+
SPACING = 0.5
|
|
33
|
+
|
|
34
|
+
#: Degrees to each side of a boundary at which its crust is looked up.
|
|
35
|
+
SIDE = 1.5
|
|
36
|
+
|
|
37
|
+
#: mm/yr below which a boundary is not moving at all.
|
|
38
|
+
STILL = 1.0
|
|
39
|
+
|
|
40
|
+
#: A boundary closes or opens when that part of the motion is at least this
|
|
41
|
+
#: share of the sliding part; otherwise it is a transform.
|
|
42
|
+
DOMINANT = 0.5
|
|
43
|
+
|
|
44
|
+
#: Samples to each side over which a boundary's kind, and its shortening, are
|
|
45
|
+
#: smoothed along it. Near a threshold the kind flickered from sample to
|
|
46
|
+
#: sample (dashed trenches), and shortening jumping between neighbours cut
|
|
47
|
+
#: straight steps into the ranges.
|
|
48
|
+
KIND_WINDOW = 3
|
|
49
|
+
SHORTENING_WINDOW = 5
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class Sample:
|
|
54
|
+
lon: float
|
|
55
|
+
lat: float
|
|
56
|
+
normal: tuple # (east, north) unit vector into the overriding side
|
|
57
|
+
kind: str
|
|
58
|
+
closing: float # mm/yr, negative when opening
|
|
59
|
+
sliding: float # mm/yr
|
|
60
|
+
shortening: float # km over the phase, 0 unless closing
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def angular_velocities(settings: dict) -> dict:
|
|
64
|
+
"""{plate_id: omega, rad/Myr as an xyz vector}."""
|
|
65
|
+
return {
|
|
66
|
+
plate["plate_id"]: math.radians(plate["rate"])
|
|
67
|
+
* unit(plate["pole"]["lon"], plate["pole"]["lat"])
|
|
68
|
+
for plate in settings["plates"]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def unit(lon, lat) -> numpy.ndarray:
|
|
73
|
+
lon, lat = math.radians(lon), math.radians(lat)
|
|
74
|
+
return numpy.array(
|
|
75
|
+
[math.cos(lat) * math.cos(lon), math.cos(lat) * math.sin(lon), math.sin(lat)]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def local_velocity(omega, lon, lat, radius_km) -> numpy.ndarray:
|
|
80
|
+
"""(east, north) velocity in km/Myr, i.e. mm/yr, of a point on a plate."""
|
|
81
|
+
v = radius_km * numpy.cross(omega, unit(lon, lat))
|
|
82
|
+
east = numpy.array([-math.sin(math.radians(lon)), math.cos(math.radians(lon)), 0])
|
|
83
|
+
north = numpy.cross(unit(lon, lat), east)
|
|
84
|
+
return numpy.array([v @ east, v @ north])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def classify(directory, age_at) -> list:
|
|
88
|
+
"""Every boundary sample of the phase's present. age_at(lon, lat) gives
|
|
89
|
+
the seafloor age there, to tell which ocean floor is older."""
|
|
90
|
+
settings = phase.settings(directory)
|
|
91
|
+
omega = angular_velocities(settings)
|
|
92
|
+
radius = settings["radius"] / 1000
|
|
93
|
+
duration = float(settings["duration"])
|
|
94
|
+
plates = phase.plates(directory / "next" / "plates.geojson")
|
|
95
|
+
land = phase.land(phase.snapshot(directory, "continents", 0.0))
|
|
96
|
+
shapely.prepare(land)
|
|
97
|
+
samples = []
|
|
98
|
+
ids = sorted(plates)
|
|
99
|
+
for i, a in enumerate(ids):
|
|
100
|
+
for b in ids[i + 1 :]:
|
|
101
|
+
shared = plates[a].boundary.intersection(plates[b].buffer(1e-6))
|
|
102
|
+
lines = [
|
|
103
|
+
part
|
|
104
|
+
for part in shapely.get_parts(shared)
|
|
105
|
+
if part.geom_type == "LineString"
|
|
106
|
+
]
|
|
107
|
+
for line in shapely.get_parts(
|
|
108
|
+
shapely.line_merge(shapely.multilinestrings(lines))
|
|
109
|
+
):
|
|
110
|
+
if line.length < SPACING / 2:
|
|
111
|
+
continue
|
|
112
|
+
described = []
|
|
113
|
+
for point, tangent in along(line):
|
|
114
|
+
lon, lat, normal, closing, sliding = sample(
|
|
115
|
+
point, tangent, a, b, plates, omega, radius
|
|
116
|
+
)
|
|
117
|
+
described.append(
|
|
118
|
+
describe(
|
|
119
|
+
lon, lat, normal, closing, sliding, land, duration, age_at
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
samples += smoothed(described)
|
|
123
|
+
return samples
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def smoothed(line: list) -> list:
|
|
127
|
+
"""The samples of one boundary line, in order, each taking the commonest
|
|
128
|
+
kind among its neighbours (with the overriding side of the nearest
|
|
129
|
+
neighbour of that kind), the side most of its neighbours' normals point
|
|
130
|
+
to, and the mean shortening around it. Near-equal seafloor ages on both
|
|
131
|
+
sides of an arc flipped its side from sample to sample, drawing the
|
|
132
|
+
trench as bars across the boundary."""
|
|
133
|
+
retyped = []
|
|
134
|
+
for i, current in enumerate(line):
|
|
135
|
+
near = range(max(0, i - KIND_WINDOW), min(len(line), i + KIND_WINDOW + 1))
|
|
136
|
+
kind = Counter(line[j].kind for j in near).most_common(1)[0][0]
|
|
137
|
+
model = line[
|
|
138
|
+
min((j for j in near if line[j].kind == kind), key=lambda j: abs(j - i))
|
|
139
|
+
]
|
|
140
|
+
retyped.append(dataclasses.replace(current, kind=kind, normal=model.normal))
|
|
141
|
+
out = []
|
|
142
|
+
for i, current in enumerate(retyped):
|
|
143
|
+
span = retyped[max(0, i - SHORTENING_WINDOW) : i + SHORTENING_WINDOW + 1]
|
|
144
|
+
east = sum(s.normal[0] for s in span)
|
|
145
|
+
north = sum(s.normal[1] for s in span)
|
|
146
|
+
flip = current.normal[0] * east + current.normal[1] * north < 0
|
|
147
|
+
out.append(
|
|
148
|
+
dataclasses.replace(
|
|
149
|
+
current,
|
|
150
|
+
normal=(
|
|
151
|
+
(-current.normal[0], -current.normal[1]) if flip else current.normal
|
|
152
|
+
),
|
|
153
|
+
shortening=sum(s.shortening for s in span) / len(span),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def along(line):
|
|
160
|
+
"""(point, unit tangent in east/north) every SPACING degrees of a line."""
|
|
161
|
+
count = max(1, round(line.length / SPACING))
|
|
162
|
+
for k in range(count):
|
|
163
|
+
at = (k + 0.5) / count * line.length
|
|
164
|
+
p = line.interpolate(at)
|
|
165
|
+
ahead = line.interpolate(min(at + SPACING / 4, line.length))
|
|
166
|
+
behind = line.interpolate(max(at - SPACING / 4, 0))
|
|
167
|
+
east = (ahead.x - behind.x) * math.cos(math.radians(p.y))
|
|
168
|
+
north = ahead.y - behind.y
|
|
169
|
+
norm = math.hypot(east, north)
|
|
170
|
+
if norm > 0:
|
|
171
|
+
yield p, (east / norm, north / norm)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def sample(point, tangent, a, b, plates, omega, radius):
|
|
175
|
+
"""(lon, lat, normal from a into b, closing and sliding mm/yr)."""
|
|
176
|
+
normal = (tangent[1], -tangent[0])
|
|
177
|
+
if not plates[b].contains(offset(point, normal, SIDE / 3)):
|
|
178
|
+
normal = (-normal[0], -normal[1])
|
|
179
|
+
relative = local_velocity(omega[a], point.x, point.y, radius) - local_velocity(
|
|
180
|
+
omega[b], point.x, point.y, radius
|
|
181
|
+
)
|
|
182
|
+
closing = float(relative @ numpy.array(normal))
|
|
183
|
+
sliding = float(relative @ numpy.array(tangent))
|
|
184
|
+
return (point.x, point.y, normal, closing, sliding)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def offset(point, direction, degrees):
|
|
188
|
+
"""The point `degrees` of arc away along an (east, north) direction."""
|
|
189
|
+
lat = point.y + direction[1] * degrees
|
|
190
|
+
lon = point.x + direction[0] * degrees / max(math.cos(math.radians(point.y)), 0.05)
|
|
191
|
+
return shapely.Point((lon + 180) % 360 - 180, max(-89.9, min(89.9, lat)))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def describe(lon, lat, normal, closing, sliding, land, duration, age_at) -> Sample:
|
|
195
|
+
"""Name the boundary and turn its normal towards the overriding side
|
|
196
|
+
(for a collision, a ridge or a transform the side does not matter)."""
|
|
197
|
+
p = shapely.Point(lon, lat)
|
|
198
|
+
into_b = land.contains(offset(p, normal, SIDE))
|
|
199
|
+
into_a = land.contains(offset(p, (-normal[0], -normal[1]), SIDE))
|
|
200
|
+
if abs(closing) < STILL and abs(sliding) < STILL:
|
|
201
|
+
kind = "still"
|
|
202
|
+
elif closing >= DOMINANT * abs(sliding):
|
|
203
|
+
if into_a and into_b:
|
|
204
|
+
kind = "collision"
|
|
205
|
+
elif into_a or into_b:
|
|
206
|
+
kind = "subduction"
|
|
207
|
+
if into_a:
|
|
208
|
+
normal = (-normal[0], -normal[1])
|
|
209
|
+
else:
|
|
210
|
+
kind = "arc"
|
|
211
|
+
age_a = age_at(offset(p, (-normal[0], -normal[1]), SIDE))
|
|
212
|
+
age_b = age_at(offset(p, normal, SIDE))
|
|
213
|
+
if age_b > age_a:
|
|
214
|
+
normal = (-normal[0], -normal[1])
|
|
215
|
+
elif -closing >= DOMINANT * abs(sliding):
|
|
216
|
+
kind = "rift" if into_a or into_b else "ridge"
|
|
217
|
+
else:
|
|
218
|
+
kind = "transform"
|
|
219
|
+
return Sample(
|
|
220
|
+
lon,
|
|
221
|
+
lat,
|
|
222
|
+
normal,
|
|
223
|
+
kind,
|
|
224
|
+
closing,
|
|
225
|
+
sliding,
|
|
226
|
+
closing * duration if closing > 0 else 0.0,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def write(path, samples):
|
|
231
|
+
"""The samples as GeoJSON points, to look at in QGIS."""
|
|
232
|
+
features = [
|
|
233
|
+
{
|
|
234
|
+
"type": "Feature",
|
|
235
|
+
"properties": {
|
|
236
|
+
"kind": s.kind,
|
|
237
|
+
"closing": round(s.closing, 2),
|
|
238
|
+
"sliding": round(s.sliding, 2),
|
|
239
|
+
"shortening_km": round(s.shortening),
|
|
240
|
+
"normal_east": round(s.normal[0], 3),
|
|
241
|
+
"normal_north": round(s.normal[1], 3),
|
|
242
|
+
},
|
|
243
|
+
"geometry": {"type": "Point", "coordinates": [s.lon, s.lat]},
|
|
244
|
+
}
|
|
245
|
+
for s in samples
|
|
246
|
+
]
|
|
247
|
+
path.write_text(
|
|
248
|
+
json.dumps({"type": "FeatureCollection", "features": features}),
|
|
249
|
+
encoding="utf-8",
|
|
250
|
+
)
|
|
251
|
+
print("wrote", path)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""`orogen VERB PHASE OUT`: one verb per algorithm, each reading a shamash-core
|
|
2
|
+
tectonics phase directory and writing ESRI ASCII grids into OUT; `orogen
|
|
3
|
+
reconstruct PHASE` writes the phase's own snapshots instead, so takes no OUT,
|
|
4
|
+
and `orogen climate DEM OUT --planet planet.json` reads a dem.png instead."""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import time
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from orogen import (
|
|
14
|
+
boundaries,
|
|
15
|
+
climate,
|
|
16
|
+
dem,
|
|
17
|
+
history,
|
|
18
|
+
orogeny,
|
|
19
|
+
phase,
|
|
20
|
+
reconstruction,
|
|
21
|
+
seafloor,
|
|
22
|
+
)
|
|
23
|
+
from orogen.grid import Grid, read_png16
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run_seafloor(args, grid, crs):
|
|
27
|
+
ages = seafloor.age(args.phase, grid, args.initial_age)
|
|
28
|
+
grid.write(args.out / "seafloor_age.asc", ages, crs)
|
|
29
|
+
grid.write(args.out / "seafloor_depth.asc", -seafloor.depth(ages), crs)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_history(args, grid, crs):
|
|
33
|
+
overlap, gap = history.footprint(args.phase, grid)
|
|
34
|
+
grid.write(args.out / "overlap_myr.asc", overlap, crs)
|
|
35
|
+
grid.write(args.out / "gap_myr.asc", gap, crs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def run_boundaries(args, grid, crs):
|
|
39
|
+
ages = seafloor.age(args.phase, grid, args.initial_age)
|
|
40
|
+
samples = boundaries.classify(args.phase, lambda p: grid.at(ages, p.x, p.y))
|
|
41
|
+
boundaries.write(args.out / "boundaries.geojson", samples)
|
|
42
|
+
for kind, count in Counter(s.kind for s in samples).most_common():
|
|
43
|
+
print(" {:<10} {:>5} samples".format(kind, count))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_orogeny(args, grid, crs):
|
|
47
|
+
ages = seafloor.age(args.phase, grid, args.initial_age)
|
|
48
|
+
samples = boundaries.classify(args.phase, lambda p: grid.at(ages, p.x, p.y))
|
|
49
|
+
radius_km = phase.settings(args.phase)["radius"] / 1000
|
|
50
|
+
grid.write(args.out / "relief.asc", orogeny.relief(samples, grid, radius_km), crs)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_dem(args, grid, crs):
|
|
54
|
+
heights = dem.build(
|
|
55
|
+
args.phase,
|
|
56
|
+
args.out,
|
|
57
|
+
grid,
|
|
58
|
+
args.gravity,
|
|
59
|
+
args.range,
|
|
60
|
+
args.seed,
|
|
61
|
+
args.initial_age,
|
|
62
|
+
args.asc,
|
|
63
|
+
crs,
|
|
64
|
+
)
|
|
65
|
+
print(
|
|
66
|
+
"elevation {:.0f}..{:.0f} m, land {:.1%} of cells".format(
|
|
67
|
+
heights.min(), heights.max(), (heights > 0).mean()
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def run_climate(args, grid, crs):
|
|
73
|
+
if args.planet is None:
|
|
74
|
+
raise SystemExit("climate needs --planet, the planet.json shamash-core writes")
|
|
75
|
+
planet = json.loads(args.planet.read_text(encoding="utf-8"))
|
|
76
|
+
summary = climate.build(
|
|
77
|
+
read_png16(args.phase, *args.range), planet, grid, args.out, crs
|
|
78
|
+
)
|
|
79
|
+
print(json.dumps(summary))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def run_reconstruct(args, grid, crs):
|
|
83
|
+
reconstruction.build(args.phase, overwrite=args.overwrite)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
VERBS = {
|
|
87
|
+
"reconstruct": run_reconstruct,
|
|
88
|
+
"climate": run_climate,
|
|
89
|
+
"seafloor": run_seafloor,
|
|
90
|
+
"history": run_history,
|
|
91
|
+
"boundaries": run_boundaries,
|
|
92
|
+
"orogeny": run_orogeny,
|
|
93
|
+
"dem": run_dem,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_args(argv=None) -> argparse.Namespace:
|
|
98
|
+
parser = argparse.ArgumentParser(prog="orogen", description=__doc__)
|
|
99
|
+
parser.add_argument("verb", choices=VERBS)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"phase",
|
|
102
|
+
type=Path,
|
|
103
|
+
help="<body>/tectonics/phase-N; for climate, the dem.png to read",
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument("out", type=Path, nargs="?", help="every verb but reconstruct")
|
|
106
|
+
parser.add_argument("--resolution", type=float, default=0.5, help="degrees")
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--crs", type=Path, help="a file holding the body's geographic CRS as WKT"
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--initial-age",
|
|
112
|
+
type=float,
|
|
113
|
+
default=seafloor.INITIAL_AGE,
|
|
114
|
+
help="Myr, the age of the ocean floor a phase starts with",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--gravity", type=float, default=dem.EARTH_GRAVITY, help="m/s2 of the body"
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--range",
|
|
121
|
+
type=float,
|
|
122
|
+
nargs=2,
|
|
123
|
+
default=(-11200.0, 9200.0),
|
|
124
|
+
metavar=("LOW", "HIGH"),
|
|
125
|
+
help="m, the elevation range of the map project (dem.png spans it)",
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument("--seed", type=int, default=0)
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--asc", action="store_true", help="dem also writes the float grid, dem.asc"
|
|
130
|
+
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--planet",
|
|
133
|
+
type=Path,
|
|
134
|
+
help="climate: the body's parameters, climate/planet.json from shamash-core",
|
|
135
|
+
)
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"--overwrite",
|
|
138
|
+
action="store_true",
|
|
139
|
+
help="reconstruct replaces the phase's snapshots",
|
|
140
|
+
)
|
|
141
|
+
args = parser.parse_args(argv)
|
|
142
|
+
if args.out is None and args.verb != "reconstruct":
|
|
143
|
+
parser.error("{} writes into OUT; give one".format(args.verb))
|
|
144
|
+
return args
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main(argv=None) -> int:
|
|
148
|
+
args = parse_args(argv)
|
|
149
|
+
if args.out:
|
|
150
|
+
args.out.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
crs = args.crs.read_text(encoding="utf-8") if args.crs else None
|
|
152
|
+
started = time.perf_counter()
|
|
153
|
+
VERBS[args.verb](args, Grid(args.resolution), crs)
|
|
154
|
+
print("{} took {:.1f} s".format(args.verb, time.perf_counter() - started))
|
|
155
|
+
return 0
|