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.
Files changed (51) hide show
  1. corelle_engine-3.0.0/PKG-INFO +31 -0
  2. corelle_engine-3.0.0/corelle/engine/__init__.py +1 -0
  3. corelle_engine-3.0.0/corelle/engine/cache.py +78 -0
  4. corelle_engine-3.0.0/corelle/engine/cli.py +188 -0
  5. corelle_engine-3.0.0/corelle/engine/database.py +35 -0
  6. corelle_engine-3.0.0/corelle/engine/load_data.py +350 -0
  7. corelle_engine-3.0.0/corelle/engine/query/__init__.py +7 -0
  8. corelle_engine-3.0.0/corelle/engine/query/active-plates-at-time.sql +11 -0
  9. corelle_engine-3.0.0/corelle/engine/query/cache-feature-dataset.sql +37 -0
  10. corelle_engine-3.0.0/corelle/engine/query/delete-extraneous-cache.sql +7 -0
  11. corelle_engine-3.0.0/corelle/engine/query/feature-dataset-cached.sql +4 -0
  12. corelle_engine-3.0.0/corelle/engine/query/feature-dataset.sql +21 -0
  13. corelle_engine-3.0.0/corelle/engine/query/modern-plate-polygons-cached.sql +3 -0
  14. corelle_engine-3.0.0/corelle/engine/query/modern-plate-polygons.sql +14 -0
  15. corelle_engine-3.0.0/corelle/engine/query/plate-for-point.sql +7 -0
  16. corelle_engine-3.0.0/corelle/engine/query/plate-time-ranges.sql +9 -0
  17. corelle_engine-3.0.0/corelle/engine/query/plates-for-model.sql +6 -0
  18. corelle_engine-3.0.0/corelle/engine/query/rotation-pairs-for-model.sql +64 -0
  19. corelle_engine-3.0.0/corelle/engine/query/rotation-pairs-for-time.sql +63 -0
  20. corelle_engine-3.0.0/corelle/engine/query/rotation-pairs.sql +64 -0
  21. corelle_engine-3.0.0/corelle/engine/query/update-cache.sql +9 -0
  22. corelle_engine-3.0.0/corelle/engine/rotate.py +265 -0
  23. corelle_engine-3.0.0/corelle/engine/schema/00-init.sql +2 -0
  24. corelle_engine-3.0.0/corelle/engine/schema/01-schema.sql +88 -0
  25. corelle_engine-3.0.0/corelle/engine/schema/02-functions.sql +233 -0
  26. corelle_engine-3.0.0/corelle/engine/schema/03-testing-functions.sql +186 -0
  27. corelle_engine-3.0.0/corelle/engine/schema/04-views.sql +47 -0
  28. corelle_engine-3.0.0/corelle/engine/storage.py +11 -0
  29. corelle_engine-3.0.0/corelle/tests/__init__.py +0 -0
  30. corelle_engine-3.0.0/corelle/tests/conftest.py +32 -0
  31. corelle_engine-3.0.0/corelle/tests/fixtures/Seton2012-relative-rotations-2Ma.csv +692 -0
  32. corelle_engine-3.0.0/corelle/tests/fixtures/Seton2012-rotations-100Ma.csv +579 -0
  33. corelle_engine-3.0.0/corelle/tests/fixtures/Seton2012-rotations-10Ma.csv +689 -0
  34. corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-request-africa.geojson +36 -0
  35. corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-request.geojson +56 -0
  36. corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-0.geojson +56 -0
  37. corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-1.geojson +1 -0
  38. corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-10.geojson +56 -0
  39. corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-120.geojson +56 -0
  40. corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-140.geojson +1 -0
  41. corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-200.geojson +1 -0
  42. corelle_engine-3.0.0/corelle/tests/fixtures/seton2012-gws-response-africa-140.geojson +36 -0
  43. corelle_engine-3.0.0/corelle/tests/rotation_testing_functions.py +232 -0
  44. corelle_engine-3.0.0/corelle/tests/test_database_rotations.py +310 -0
  45. corelle_engine-3.0.0/corelle/tests/test_gplates.py +57 -0
  46. corelle_engine-3.0.0/corelle/tests/test_imports.py +33 -0
  47. corelle_engine-3.0.0/corelle/tests/test_model.py +154 -0
  48. corelle_engine-3.0.0/corelle/tests/test_rotate.py +91 -0
  49. corelle_engine-3.0.0/corelle/tests/utils.py +21 -0
  50. corelle_engine-3.0.0/pyproject.toml +59 -0
  51. 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,7 @@
1
+ from macrostrat.utils import relative_path
2
+ from sqlalchemy import text
3
+
4
+
5
+ def get_sql(key):
6
+ fn = relative_path(__file__, key + ".sql")
7
+ return text(open(fn).read())
@@ -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