corelle.engine 3.0.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.
- corelle_engine-3.0.0/PKG-INFO +31 -0
- corelle_engine-3.0.0/corelle/engine/__init__.py +1 -0
- corelle_engine-3.0.0/corelle/engine/cache.py +78 -0
- corelle_engine-3.0.0/corelle/engine/cli.py +188 -0
- corelle_engine-3.0.0/corelle/engine/database.py +35 -0
- corelle_engine-3.0.0/corelle/engine/load_data.py +350 -0
- corelle_engine-3.0.0/corelle/engine/query/__init__.py +7 -0
- corelle_engine-3.0.0/corelle/engine/query/active-plates-at-time.sql +11 -0
- corelle_engine-3.0.0/corelle/engine/query/cache-feature-dataset.sql +37 -0
- corelle_engine-3.0.0/corelle/engine/query/delete-extraneous-cache.sql +7 -0
- corelle_engine-3.0.0/corelle/engine/query/feature-dataset-cached.sql +4 -0
- corelle_engine-3.0.0/corelle/engine/query/feature-dataset.sql +21 -0
- corelle_engine-3.0.0/corelle/engine/query/modern-plate-polygons-cached.sql +3 -0
- corelle_engine-3.0.0/corelle/engine/query/modern-plate-polygons.sql +14 -0
- corelle_engine-3.0.0/corelle/engine/query/plate-for-point.sql +7 -0
- corelle_engine-3.0.0/corelle/engine/query/plate-time-ranges.sql +9 -0
- corelle_engine-3.0.0/corelle/engine/query/plates-for-model.sql +6 -0
- corelle_engine-3.0.0/corelle/engine/query/rotation-pairs-for-model.sql +64 -0
- corelle_engine-3.0.0/corelle/engine/query/rotation-pairs-for-time.sql +63 -0
- corelle_engine-3.0.0/corelle/engine/query/rotation-pairs.sql +64 -0
- corelle_engine-3.0.0/corelle/engine/query/update-cache.sql +9 -0
- corelle_engine-3.0.0/corelle/engine/rotate.py +265 -0
- corelle_engine-3.0.0/corelle/engine/schema/00-init.sql +2 -0
- corelle_engine-3.0.0/corelle/engine/schema/01-schema.sql +88 -0
- corelle_engine-3.0.0/corelle/engine/schema/02-functions.sql +233 -0
- corelle_engine-3.0.0/corelle/engine/schema/03-testing-functions.sql +186 -0
- corelle_engine-3.0.0/corelle/engine/schema/04-views.sql +47 -0
- corelle_engine-3.0.0/corelle/engine/storage.py +11 -0
- corelle_engine-3.0.0/corelle/tests/__init__.py +0 -0
- corelle_engine-3.0.0/corelle/tests/conftest.py +32 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/Seton2012-relative-rotations-2Ma.csv +692 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/Seton2012-rotations-100Ma.csv +579 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/Seton2012-rotations-10Ma.csv +689 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-request-africa.geojson +36 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-request.geojson +56 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-0.geojson +56 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-1.geojson +1 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-10.geojson +56 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-120.geojson +56 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-140.geojson +1 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-200.geojson +1 -0
- corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-africa-140.geojson +36 -0
- corelle_engine-3.0.0/corelle/tests/rotation_testing_functions.py +232 -0
- corelle_engine-3.0.0/corelle/tests/test_database_rotations.py +310 -0
- corelle_engine-3.0.0/corelle/tests/test_gplates.py +57 -0
- corelle_engine-3.0.0/corelle/tests/test_imports.py +33 -0
- corelle_engine-3.0.0/corelle/tests/test_model.py +154 -0
- corelle_engine-3.0.0/corelle/tests/test_rotate.py +91 -0
- corelle_engine-3.0.0/corelle/tests/utils.py +21 -0
- corelle_engine-3.0.0/pyproject.toml +59 -0
- corelle_engine-3.0.0/pyproject.toml.orig +55 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: corelle.engine
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Plate-rotation models and rotation engine for the Corelle system
|
|
5
|
+
Author: Daven Quinn
|
|
6
|
+
Author-email: Daven Quinn <dev@davenquinn.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: click>=8.1,<9
|
|
15
|
+
Requires-Dist: corelle-client>=2.2,<3
|
|
16
|
+
Requires-Dist: macrostrat-database>=4.7,<5
|
|
17
|
+
Requires-Dist: macrostrat-utils>=1.0.1,<2
|
|
18
|
+
Requires-Dist: numba>=0.63.1,<0.69
|
|
19
|
+
Requires-Dist: numpy>=2.0.0,<3
|
|
20
|
+
Requires-Dist: numpy-quaternion>=2024.0.13,<2025.0.0
|
|
21
|
+
Requires-Dist: rich>12.6.0,<16
|
|
22
|
+
Requires-Dist: typer>0.12.0,<0.30
|
|
23
|
+
Requires-Dist: geoalchemy2>0.14.0,<0.22
|
|
24
|
+
Requires-Dist: sqlalchemy>=2.0.25,<2.1
|
|
25
|
+
Requires-Dist: fiona>=1.10.1,<2 ; extra == 'import'
|
|
26
|
+
Requires-Dist: pyyaml>=6.0,<7 ; extra == 'import'
|
|
27
|
+
Requires-Dist: wget>=3.2,<4 ; extra == 'import'
|
|
28
|
+
Requires-Dist: ipython>=8.0,<10 ; extra == 'shell'
|
|
29
|
+
Requires-Python: >=3.10, <4
|
|
30
|
+
Provides-Extra: import
|
|
31
|
+
Provides-Extra: shell
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .cli import cli
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Precomputed rotations, stored in `corelle.rotation_cache`."""
|
|
2
|
+
|
|
3
|
+
import numpy as N
|
|
4
|
+
from sqlalchemy.sql import text
|
|
5
|
+
|
|
6
|
+
from .query import get_sql
|
|
7
|
+
from .rotate import get_rotation_series
|
|
8
|
+
|
|
9
|
+
update_derived = get_sql("update-cache")
|
|
10
|
+
|
|
11
|
+
_cached_rotation = text(
|
|
12
|
+
"""
|
|
13
|
+
SELECT c.rotation
|
|
14
|
+
FROM corelle.rotation_cache c
|
|
15
|
+
JOIN corelle.model m ON m.id = c.model_id
|
|
16
|
+
WHERE m.name = :model_name AND c.plate_id = :plate_id AND c.t_step = :time
|
|
17
|
+
"""
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_insert_rotation = text(
|
|
21
|
+
"""
|
|
22
|
+
INSERT INTO corelle.rotation_cache (model_id, plate_id, t_step, rotation)
|
|
23
|
+
VALUES (:model_id, :plate_id, :t_step, :rotation)
|
|
24
|
+
"""
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_from_cache(conn, cache_args):
|
|
29
|
+
(model_name, plate_id, time) = cache_args
|
|
30
|
+
params = dict(model_name=model_name, plate_id=plate_id, time=time)
|
|
31
|
+
res = conn.execute(_cached_rotation, params).scalar()
|
|
32
|
+
if res is not None:
|
|
33
|
+
return N.quaternion(*res)
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_rotation_caches(conn):
|
|
38
|
+
conn.execute(text("DELETE FROM corelle.rotation_cache"))
|
|
39
|
+
models = conn.execute(text("SELECT * FROM corelle.model")).fetchall()
|
|
40
|
+
for model in models:
|
|
41
|
+
build_rotation_cache(conn, model)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_rotation_cache(conn, model, time_step=1):
|
|
45
|
+
min_age = model.min_age or 0
|
|
46
|
+
max_age = model.max_age or 1000
|
|
47
|
+
|
|
48
|
+
# Get model steps every 1 Myr
|
|
49
|
+
t_steps = list(range(int(min_age), int(max_age) + 1, time_step))
|
|
50
|
+
|
|
51
|
+
conn.execute(
|
|
52
|
+
text("DELETE FROM corelle.rotation_cache WHERE model_id = :model_id"),
|
|
53
|
+
dict(model_id=model.id),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
for tstep in get_rotation_series(conn, model.name, *t_steps):
|
|
57
|
+
time = tstep["time"]
|
|
58
|
+
print(model.name, " - ", time, " Ma")
|
|
59
|
+
rows = [
|
|
60
|
+
build_cache_row(model.id, plate_id, time, q)
|
|
61
|
+
for plate_id, q in tstep["rotations"]
|
|
62
|
+
if q is not None
|
|
63
|
+
]
|
|
64
|
+
add_to_cache(conn, rows)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def build_cache_row(model_id, plate_id, t_step, q):
|
|
68
|
+
return dict(
|
|
69
|
+
model_id=int(model_id),
|
|
70
|
+
plate_id=int(plate_id),
|
|
71
|
+
t_step=float(t_step),
|
|
72
|
+
rotation=[float(v) for v in (q.w, q.x, q.y, q.z)],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def add_to_cache(conn, data):
|
|
77
|
+
if data:
|
|
78
|
+
conn.execute(_insert_rotation, data)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from click import group, argument, option, echo
|
|
3
|
+
from macrostrat.utils import working_directory
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@contextmanager
|
|
9
|
+
def transaction():
|
|
10
|
+
from .database import get_database
|
|
11
|
+
|
|
12
|
+
with get_database().engine.begin() as conn:
|
|
13
|
+
yield conn
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@group()
|
|
17
|
+
def cli():
|
|
18
|
+
"""Register and pre-calculate plate-rotation models"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cli.command(name="init")
|
|
23
|
+
@option("--drop", is_flag=True, default=False)
|
|
24
|
+
def init(drop=False):
|
|
25
|
+
"""Create database fixtures"""
|
|
26
|
+
from .database import initialize
|
|
27
|
+
|
|
28
|
+
initialize(drop=drop)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
file = click.Path(exists=True, dir_okay=False)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@cli.command(name="import")
|
|
35
|
+
@argument("model_name")
|
|
36
|
+
@argument("plates", type=file)
|
|
37
|
+
@argument("rotations", type=file)
|
|
38
|
+
@option("--fields", type=file)
|
|
39
|
+
@option("--overwrite", is_flag=True, default=False)
|
|
40
|
+
@option("--min-age", type=float, default=None)
|
|
41
|
+
@option("--max-age", type=float, default=None)
|
|
42
|
+
def _import(
|
|
43
|
+
model_name,
|
|
44
|
+
plates,
|
|
45
|
+
rotations,
|
|
46
|
+
fields=None,
|
|
47
|
+
overwrite=False,
|
|
48
|
+
min_age=None,
|
|
49
|
+
max_age=None,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Import a plate-rotation model
|
|
53
|
+
"""
|
|
54
|
+
from .load_data import import_model
|
|
55
|
+
|
|
56
|
+
with transaction() as conn:
|
|
57
|
+
import_model(
|
|
58
|
+
conn,
|
|
59
|
+
model_name,
|
|
60
|
+
plates,
|
|
61
|
+
rotations,
|
|
62
|
+
fields=fields,
|
|
63
|
+
overwrite=overwrite,
|
|
64
|
+
min_age=min_age,
|
|
65
|
+
max_age=max_age,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@cli.command(name="import-features")
|
|
70
|
+
@argument("name")
|
|
71
|
+
@argument("file", type=file)
|
|
72
|
+
@option("--overwrite", is_flag=True, default=False)
|
|
73
|
+
def _import_features(name, file, overwrite=False):
|
|
74
|
+
"""
|
|
75
|
+
Import features that can be associated with the models
|
|
76
|
+
"""
|
|
77
|
+
from .load_data import import_features
|
|
78
|
+
|
|
79
|
+
with transaction() as conn:
|
|
80
|
+
import_features(conn, name, file, overwrite=overwrite)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@cli.command(name="import-starter-data")
|
|
84
|
+
def import_basic():
|
|
85
|
+
"""Import basic models and data"""
|
|
86
|
+
from .load_data import load_basic_data
|
|
87
|
+
|
|
88
|
+
with transaction() as conn:
|
|
89
|
+
load_basic_data(conn)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@cli.command(name="reset-cache")
|
|
93
|
+
def cache():
|
|
94
|
+
"""
|
|
95
|
+
Compute and cache rotations
|
|
96
|
+
"""
|
|
97
|
+
from .rotate import reset_cache
|
|
98
|
+
|
|
99
|
+
reset_cache()
|
|
100
|
+
echo("Cache was reset!")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@cli.command(name="rotate")
|
|
104
|
+
@argument("model", type=str)
|
|
105
|
+
@argument("plate", type=int)
|
|
106
|
+
@argument("time", type=float)
|
|
107
|
+
@option("--verbose", "-v", is_flag=True, default=False)
|
|
108
|
+
def rotate(model, plate, time, verbose=False):
|
|
109
|
+
"""
|
|
110
|
+
Rotate a plate to a time
|
|
111
|
+
"""
|
|
112
|
+
import numpy as N
|
|
113
|
+
from corelle.math import quaternion_to_euler
|
|
114
|
+
from .rotate import get_rotation
|
|
115
|
+
|
|
116
|
+
with transaction() as conn:
|
|
117
|
+
q = get_rotation(conn, model, plate, time, verbose=verbose)
|
|
118
|
+
if q is None:
|
|
119
|
+
echo(f"No rotation for plate {plate} at {time} Ma.")
|
|
120
|
+
return
|
|
121
|
+
angle = N.degrees(q.angle())
|
|
122
|
+
echo(f"Rotate {angle:.2f}° around {q.vec}")
|
|
123
|
+
lat, lon, angle = quaternion_to_euler(q)
|
|
124
|
+
echo(f"Pole: {lat:.2f}°, {lon:.2f}°; Angle: {angle:.2f}°")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@cli.command(name="rotate-all")
|
|
128
|
+
@argument("model", type=str)
|
|
129
|
+
@argument("time", type=float)
|
|
130
|
+
@option("--verbose", "-v", is_flag=True, default=False)
|
|
131
|
+
def rotate_all(model, time, verbose=False):
|
|
132
|
+
"""
|
|
133
|
+
Rotate all plates in a model to a time
|
|
134
|
+
"""
|
|
135
|
+
import numpy as N
|
|
136
|
+
from .rotate import get_all_rotations
|
|
137
|
+
|
|
138
|
+
with transaction() as conn:
|
|
139
|
+
for plate_id, q in get_all_rotations(conn, model, time, verbose=verbose):
|
|
140
|
+
angle = N.degrees(q.angle())
|
|
141
|
+
echo(f"{plate_id}: rotate {angle:.2f}° around {q.vec}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@cli.command(name="serve")
|
|
145
|
+
@option("-p", "--port", type=int, default=5000)
|
|
146
|
+
@option("--debug", is_flag=True, default=False)
|
|
147
|
+
def serve(**kwargs):
|
|
148
|
+
"""Run the application server"""
|
|
149
|
+
from corelle.server import app
|
|
150
|
+
|
|
151
|
+
app.run(host="0.0.0.0", **kwargs)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@cli.command(name="shell")
|
|
155
|
+
def shell():
|
|
156
|
+
"""Get a shell in the application context"""
|
|
157
|
+
from IPython import embed
|
|
158
|
+
|
|
159
|
+
embed()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@cli.command(name="cache-rotations")
|
|
163
|
+
def build_cache():
|
|
164
|
+
"""Cache rotations for all models"""
|
|
165
|
+
from .cache import build_rotation_caches
|
|
166
|
+
|
|
167
|
+
with transaction() as conn:
|
|
168
|
+
build_rotation_caches(conn)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def parent_dir(_start: str, name: str) -> Path:
|
|
172
|
+
start = Path(_start)
|
|
173
|
+
while start.parent is not None:
|
|
174
|
+
start = start.parent
|
|
175
|
+
if start.name == name:
|
|
176
|
+
return start
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@cli.command(name="test", context_settings=dict(ignore_unknown_options=True))
|
|
181
|
+
def test(*args):
|
|
182
|
+
"""Run tests"""
|
|
183
|
+
basedir = parent_dir(__file__, "py-packages")
|
|
184
|
+
|
|
185
|
+
with working_directory(basedir):
|
|
186
|
+
from pytest import main
|
|
187
|
+
|
|
188
|
+
main(list(args))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from functools import cache
|
|
2
|
+
from os import environ
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from macrostrat.database import Database
|
|
6
|
+
from macrostrat.utils import relative_path
|
|
7
|
+
from sqlalchemy.exc import ProgrammingError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def database_url():
|
|
11
|
+
return environ.get("CORELLE_DB", "postgresql:///plate-rotations")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_database(url=None) -> Database:
|
|
15
|
+
"""The database for the CLI and the server; engine functions take a connection."""
|
|
16
|
+
return _database(url or database_url())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@cache
|
|
20
|
+
def _database(url):
|
|
21
|
+
# Pre-ping replaces pooled connections the server has dropped.
|
|
22
|
+
return Database(url, pool_pre_ping=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def initialize(db=None, drop=False):
|
|
26
|
+
db = db or get_database()
|
|
27
|
+
dn = Path(relative_path(__file__, "schema"))
|
|
28
|
+
if drop:
|
|
29
|
+
try:
|
|
30
|
+
db.run_sql("DROP SCHEMA corelle CASCADE")
|
|
31
|
+
except ProgrammingError:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
for file in sorted(dn.glob("*.sql")):
|
|
35
|
+
db.run_sql(file, has_server_binds=False)
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
from sqlalchemy import func, text
|
|
2
|
+
from sqlalchemy.exc import IntegrityError
|
|
3
|
+
from sqlalchemy.dialects.postgresql import insert
|
|
4
|
+
from time import perf_counter
|
|
5
|
+
from json import dumps
|
|
6
|
+
import fiona
|
|
7
|
+
from click import echo, style
|
|
8
|
+
import json
|
|
9
|
+
import yaml
|
|
10
|
+
from os.path import splitext
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from tempfile import TemporaryDirectory
|
|
13
|
+
from macrostrat.utils import working_directory
|
|
14
|
+
from wget import download
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from geoalchemy2 import functions as gfunc
|
|
17
|
+
from geoalchemy2.shape import from_shape
|
|
18
|
+
from shapely.geometry import shape, MultiPolygon
|
|
19
|
+
|
|
20
|
+
from .query import get_sql
|
|
21
|
+
from .storage import reflect_table
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_model(conn, name, **kwargs):
|
|
25
|
+
model = reflect_table(conn, "model")
|
|
26
|
+
try:
|
|
27
|
+
# A savepoint, so a conflict doesn't abort the caller's transaction.
|
|
28
|
+
with conn.begin_nested():
|
|
29
|
+
conn.execute(
|
|
30
|
+
insert(model)
|
|
31
|
+
.values(name=name, **kwargs)
|
|
32
|
+
.on_conflict_do_update(index_elements=(model.c.id,), set_=kwargs)
|
|
33
|
+
)
|
|
34
|
+
except IntegrityError:
|
|
35
|
+
pass
|
|
36
|
+
return conn.execute(model.select().where(model.c.name == name)).first()[0]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def pg_geometry(feature):
|
|
40
|
+
"""Convert a GeoJSON feature to a PostGIS geometry"""
|
|
41
|
+
geom = shape(feature.geometry)
|
|
42
|
+
# ensure the geometry is a valid multipolygon
|
|
43
|
+
return gfunc.ST_Multi(gfunc.ST_MakeValid(from_shape(geom, srid=4326)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def insert_rows(conn, statement, rows, chunk_size=1000):
|
|
47
|
+
"""Insert in chunks: one VALUES list overflows psycopg's 65,535 parameters.
|
|
48
|
+
|
|
49
|
+
Not executemany, since geometries are SQL expressions rather than parameters.
|
|
50
|
+
"""
|
|
51
|
+
rows = list(rows)
|
|
52
|
+
keys = set().union(*rows) if rows else set()
|
|
53
|
+
rows = [{k: row.get(k) for k in keys} for row in rows]
|
|
54
|
+
for i in range(0, len(rows), chunk_size):
|
|
55
|
+
conn.execute(statement.values(rows[i : i + chunk_size]))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def import_plate(model_id, feature, fields=None):
|
|
59
|
+
def field(field_id):
|
|
60
|
+
# If we have provided a mapping of fields for the input
|
|
61
|
+
# geojson file, we should use these fields for
|
|
62
|
+
if fields and field_id in fields:
|
|
63
|
+
field_id = fields[field_id]
|
|
64
|
+
return feature["properties"].get(field_id, None)
|
|
65
|
+
|
|
66
|
+
plate_id = field("id") or feature.get("id", None)
|
|
67
|
+
if plate_id is None:
|
|
68
|
+
raise Exception(f"Invalid plate ID: {feature}")
|
|
69
|
+
|
|
70
|
+
plate = dict(
|
|
71
|
+
id=plate_id,
|
|
72
|
+
model_id=model_id,
|
|
73
|
+
parent_id=field("parent_id"),
|
|
74
|
+
name=field("name"),
|
|
75
|
+
cotid=field("cotid"),
|
|
76
|
+
coid=field("coid"),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
young_lim = field("young_lim")
|
|
80
|
+
if young_lim == -999:
|
|
81
|
+
young_lim = None
|
|
82
|
+
|
|
83
|
+
plate_polygon = dict(
|
|
84
|
+
plate_id=plate_id,
|
|
85
|
+
model_id=model_id,
|
|
86
|
+
young_lim=young_lim,
|
|
87
|
+
old_lim=field("old_lim"),
|
|
88
|
+
geometry=pg_geometry(feature),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return plate, plate_polygon
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def import_plates(conn, model_id, datafile, fields={}):
|
|
95
|
+
"""Import all plates for a model"""
|
|
96
|
+
if fields is None:
|
|
97
|
+
fields = {}
|
|
98
|
+
|
|
99
|
+
# A plate can have several polygons (PalaeoPlates has 390 such plates); keep
|
|
100
|
+
# one row per plate ID, the first seen.
|
|
101
|
+
plates = {}
|
|
102
|
+
plate_polygons = []
|
|
103
|
+
with fiona.open(datafile, "r") as src:
|
|
104
|
+
mrf = fields.get("mantle_reference_frame", None)
|
|
105
|
+
if mrf is not None:
|
|
106
|
+
plate = dict(id=mrf, model_id=model_id, name="Mantle reference frame")
|
|
107
|
+
plates.setdefault(mrf, plate)
|
|
108
|
+
|
|
109
|
+
# The spin axis is assumed to have plate ID 0 by default
|
|
110
|
+
spin_axis = fields.get("spin_axis", 0)
|
|
111
|
+
spin_axis_plate = dict(id=spin_axis, model_id=model_id, name="Spin axis")
|
|
112
|
+
plates.setdefault(spin_axis, spin_axis_plate)
|
|
113
|
+
|
|
114
|
+
for feature in src:
|
|
115
|
+
plate, plate_polygon = import_plate(model_id, feature, fields=fields)
|
|
116
|
+
plates.setdefault(plate["id"], plate)
|
|
117
|
+
plate_polygons.append(plate_polygon)
|
|
118
|
+
|
|
119
|
+
insert_rows(conn, insert(reflect_table(conn, "plate")), plates.values())
|
|
120
|
+
insert_rows(conn, insert(reflect_table(conn, "plate_polygon")), plate_polygons)
|
|
121
|
+
|
|
122
|
+
# For faster updates, this materialized view could become an actual table
|
|
123
|
+
conn.execute(text("REFRESH MATERIALIZED VIEW corelle.plate_polygon_cache"))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def create_feature(dataset, feature):
|
|
127
|
+
props = feature["properties"]
|
|
128
|
+
if props is not None:
|
|
129
|
+
props = dict(props)
|
|
130
|
+
|
|
131
|
+
return dict(
|
|
132
|
+
dataset_id=dataset,
|
|
133
|
+
properties=(props or None),
|
|
134
|
+
geometry=pg_geometry(feature),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def import_features(conn, name, features, overwrite=False):
|
|
139
|
+
start = perf_counter()
|
|
140
|
+
table = reflect_table(conn, "feature")
|
|
141
|
+
with fiona.open(features, "r") as src:
|
|
142
|
+
if overwrite:
|
|
143
|
+
conn.execute(table.delete().where(table.c.dataset_id == name))
|
|
144
|
+
|
|
145
|
+
vals = []
|
|
146
|
+
for i, feature in enumerate(src):
|
|
147
|
+
vals.append(create_feature(name, feature))
|
|
148
|
+
|
|
149
|
+
insert_rows(conn, insert(table), vals)
|
|
150
|
+
|
|
151
|
+
step1 = perf_counter()
|
|
152
|
+
elapsed = step1 - start
|
|
153
|
+
echo(
|
|
154
|
+
f"Imported {i+1} features for dataset "
|
|
155
|
+
+ style(name, bold=True)
|
|
156
|
+
+ f" in {elapsed:.2f} seconds"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
sql = get_sql("cache-feature-dataset")
|
|
160
|
+
conn.execute(sql, dict(dataset_id=name))
|
|
161
|
+
|
|
162
|
+
elapsed = perf_counter() - step1
|
|
163
|
+
echo(f" cached transformed features in {elapsed:.2f} seconds")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def create_rotation_row(model_id, line):
|
|
167
|
+
"""
|
|
168
|
+
Import a line from a GPlates rotations file
|
|
169
|
+
"""
|
|
170
|
+
if line.startswith("*"):
|
|
171
|
+
return None, []
|
|
172
|
+
|
|
173
|
+
data, meta = line.strip().split("!", 1)
|
|
174
|
+
row = data.split()
|
|
175
|
+
|
|
176
|
+
if int(row[0]) == 999:
|
|
177
|
+
return None, []
|
|
178
|
+
|
|
179
|
+
# Insert first plate id
|
|
180
|
+
plate_id = row[0]
|
|
181
|
+
ref_plate_id = row[5]
|
|
182
|
+
# Not sure that every plate_id-ref_plate_id
|
|
183
|
+
# pair is actually unique as would be needed
|
|
184
|
+
# here...
|
|
185
|
+
ref_plate = dict(id=ref_plate_id, model_id=model_id)
|
|
186
|
+
plate = dict(id=plate_id, model_id=model_id, parent_id=ref_plate_id)
|
|
187
|
+
|
|
188
|
+
rotation = dict(
|
|
189
|
+
plate_id=plate_id,
|
|
190
|
+
model_id=model_id,
|
|
191
|
+
t_step=row[1],
|
|
192
|
+
latitude=row[2],
|
|
193
|
+
longitude=row[3],
|
|
194
|
+
angle=row[4],
|
|
195
|
+
ref_plate_id=ref_plate_id,
|
|
196
|
+
# Postgres chokes on NULL characters in strings
|
|
197
|
+
metadata=meta.replace("\x00", ""),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Return the rotation and the plates that it refers to
|
|
201
|
+
return rotation, [ref_plate, plate]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def import_rotations(conn, model_id, filename):
|
|
205
|
+
"""
|
|
206
|
+
Import a set of plate rotations defined
|
|
207
|
+
in a GPlates rotation file structure.
|
|
208
|
+
|
|
209
|
+
Constructed on 2019-03-15 for use with
|
|
210
|
+
PaleoPlates data, so it might not work
|
|
211
|
+
well for other `.rot` files in its current
|
|
212
|
+
form.
|
|
213
|
+
"""
|
|
214
|
+
rotations = []
|
|
215
|
+
plates = []
|
|
216
|
+
start = perf_counter()
|
|
217
|
+
with open(filename, "r") as f:
|
|
218
|
+
for i, line in enumerate(f):
|
|
219
|
+
rotation, next_plates = create_rotation_row(model_id, line)
|
|
220
|
+
if rotation is not None:
|
|
221
|
+
rotations.append(rotation)
|
|
222
|
+
plates.extend(next_plates)
|
|
223
|
+
|
|
224
|
+
plate = reflect_table(conn, "plate")
|
|
225
|
+
insert_rows(
|
|
226
|
+
conn,
|
|
227
|
+
insert(plate).on_conflict_do_nothing(
|
|
228
|
+
index_elements=(plate.c.id, plate.c.model_id)
|
|
229
|
+
),
|
|
230
|
+
plates,
|
|
231
|
+
)
|
|
232
|
+
rotation = reflect_table(conn, "rotation")
|
|
233
|
+
insert_rows(conn, insert(rotation).on_conflict_do_nothing(), rotations)
|
|
234
|
+
|
|
235
|
+
elapsed = perf_counter() - start
|
|
236
|
+
print(f"Imported {i+1} rotations in {elapsed:.2f} seconds")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def import_model(
|
|
240
|
+
conn,
|
|
241
|
+
name,
|
|
242
|
+
plates,
|
|
243
|
+
rotations,
|
|
244
|
+
fields=None,
|
|
245
|
+
overwrite=False,
|
|
246
|
+
min_age=None,
|
|
247
|
+
max_age=None,
|
|
248
|
+
):
|
|
249
|
+
print("Importing model", name)
|
|
250
|
+
q = text("SELECT count(*) FROM corelle.model WHERE name=:name")
|
|
251
|
+
res = conn.execute(q, dict(name=name)).scalar()
|
|
252
|
+
if res == 1 and not overwrite:
|
|
253
|
+
print("Model has already been imported.")
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
model_id = create_model(conn, name, min_age=min_age, max_age=max_age)
|
|
257
|
+
import_plates(conn, model_id, plates, fields=load_fields(fields))
|
|
258
|
+
import_rotations(conn, model_id, rotations)
|
|
259
|
+
# Right now we don't cache rotations on model import, but we could.
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def load_fields(fn):
|
|
263
|
+
if not fn:
|
|
264
|
+
return None
|
|
265
|
+
ext = splitext(fn)[1]
|
|
266
|
+
with open(fn, "r") as f:
|
|
267
|
+
if ext == ".json":
|
|
268
|
+
return json.load(f)
|
|
269
|
+
if ext in [".yaml", ".yml"]:
|
|
270
|
+
return yaml.load(f, Loader=yaml.SafeLoader)
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
corelle_data_dir = Path(__file__).parent.parent.parent.parent.parent / "data"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def load_basic_data(conn):
|
|
278
|
+
"""Load basic model and feature datasets"""
|
|
279
|
+
with working_directory(str(corelle_data_dir)):
|
|
280
|
+
import_model(
|
|
281
|
+
conn,
|
|
282
|
+
"PalaeoPlates",
|
|
283
|
+
"eglington/PlatePolygons2016All.json",
|
|
284
|
+
"eglington/T_Rot_Model_PalaeoPlates_2019_20190302_experiment.rot",
|
|
285
|
+
fields="eglington-fields.yaml",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
import_model(
|
|
289
|
+
conn,
|
|
290
|
+
"Seton2012",
|
|
291
|
+
"seton_2012.geojson",
|
|
292
|
+
"Seton_etal_ESR2012_2012.1.rot",
|
|
293
|
+
fields="seton-fields.yaml",
|
|
294
|
+
min_age=0,
|
|
295
|
+
max_age=200,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
import_model(
|
|
299
|
+
conn,
|
|
300
|
+
"Wright2013",
|
|
301
|
+
"wright_plates.geojson",
|
|
302
|
+
"wright_2013.rot",
|
|
303
|
+
fields="wright-fields.yaml",
|
|
304
|
+
min_age=0,
|
|
305
|
+
max_age=550,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
import_model(
|
|
309
|
+
conn,
|
|
310
|
+
"Scotese",
|
|
311
|
+
"scotese.geojson",
|
|
312
|
+
"scotese.rot",
|
|
313
|
+
fields="scotese-fields.yaml",
|
|
314
|
+
min_age=0,
|
|
315
|
+
max_age=550,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Import Natural Earth features
|
|
319
|
+
# Download natural earth data to temp file
|
|
320
|
+
prefix = "https://raw.githubusercontent.com/martynafford/natural-earth-geojson/master/110m/"
|
|
321
|
+
|
|
322
|
+
# Create a temp directory
|
|
323
|
+
with download_context():
|
|
324
|
+
for dataset in [
|
|
325
|
+
prefix + "cultural/ne_110m_populated_places_simple.json",
|
|
326
|
+
prefix + "physical/ne_110m_land.json",
|
|
327
|
+
]:
|
|
328
|
+
fn = dataset.split("/")[-1]
|
|
329
|
+
dsn = fn.split(".")[0]
|
|
330
|
+
|
|
331
|
+
# Check if this dataset has already been imported
|
|
332
|
+
q = text(
|
|
333
|
+
"SELECT count(*) FROM (SELECT DISTINCT dataset_id FROM corelle.feature WHERE dataset_id=:name) AS a"
|
|
334
|
+
)
|
|
335
|
+
res = conn.execute(q, dict(name=dsn)).scalar()
|
|
336
|
+
if res == 1:
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
# Download the dataset
|
|
340
|
+
download(dataset, out=fn)
|
|
341
|
+
|
|
342
|
+
# Import the dataset
|
|
343
|
+
import_features(conn, dsn, fn, overwrite=True)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@contextmanager
|
|
347
|
+
def download_context():
|
|
348
|
+
with TemporaryDirectory() as tmpdir:
|
|
349
|
+
with working_directory(tmpdir):
|
|
350
|
+
yield tmpdir
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
SELECT
|
|
2
|
+
p.id
|
|
3
|
+
FROM corelle.plate p
|
|
4
|
+
JOIN corelle.plate_polygon pp
|
|
5
|
+
ON p.id = pp.plate_id
|
|
6
|
+
AND p.model_id = pp.model_id
|
|
7
|
+
JOIN corelle.model m
|
|
8
|
+
ON p.model_id = m.id
|
|
9
|
+
WHERE m.name = :model_name
|
|
10
|
+
AND coalesce(pp.old_lim, m.max_age) > :time
|
|
11
|
+
AND coalesce(pp.young_lim, m.min_age) < :time
|