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.
Files changed (28) hide show
  1. shamash_orogen-0.2.0/PKG-INFO +101 -0
  2. shamash_orogen-0.2.0/README.md +91 -0
  3. shamash_orogen-0.2.0/pyproject.toml +21 -0
  4. shamash_orogen-0.2.0/setup.cfg +4 -0
  5. shamash_orogen-0.2.0/src/orogen/__init__.py +1 -0
  6. shamash_orogen-0.2.0/src/orogen/__main__.py +5 -0
  7. shamash_orogen-0.2.0/src/orogen/boundaries.py +251 -0
  8. shamash_orogen-0.2.0/src/orogen/cli.py +155 -0
  9. shamash_orogen-0.2.0/src/orogen/climate.py +722 -0
  10. shamash_orogen-0.2.0/src/orogen/dem.py +120 -0
  11. shamash_orogen-0.2.0/src/orogen/grid.py +150 -0
  12. shamash_orogen-0.2.0/src/orogen/history.py +25 -0
  13. shamash_orogen-0.2.0/src/orogen/koppen.py +107 -0
  14. shamash_orogen-0.2.0/src/orogen/noise.py +99 -0
  15. shamash_orogen-0.2.0/src/orogen/ocean.py +161 -0
  16. shamash_orogen-0.2.0/src/orogen/orogeny.py +124 -0
  17. shamash_orogen-0.2.0/src/orogen/phase.py +51 -0
  18. shamash_orogen-0.2.0/src/orogen/reconstruction.py +188 -0
  19. shamash_orogen-0.2.0/src/orogen/ridges.py +106 -0
  20. shamash_orogen-0.2.0/src/orogen/rotation.py +166 -0
  21. shamash_orogen-0.2.0/src/orogen/seafloor.py +75 -0
  22. shamash_orogen-0.2.0/src/orogen/tiling.py +160 -0
  23. shamash_orogen-0.2.0/src/shamash_orogen.egg-info/PKG-INFO +101 -0
  24. shamash_orogen-0.2.0/src/shamash_orogen.egg-info/SOURCES.txt +26 -0
  25. shamash_orogen-0.2.0/src/shamash_orogen.egg-info/dependency_links.txt +1 -0
  26. shamash_orogen-0.2.0/src/shamash_orogen.egg-info/entry_points.txt +2 -0
  27. shamash_orogen-0.2.0/src/shamash_orogen.egg-info/requires.txt +2 -0
  28. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """Algorithms that turn a plate-tectonic history into terrain."""
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from orogen.cli import main
4
+
5
+ sys.exit(main())
@@ -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