corelle.engine 3.0.0__py3-none-any.whl

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.
@@ -0,0 +1,37 @@
1
+ WITH a AS (
2
+ SELECT
3
+ model_id,
4
+ json_agg(json_build_object(
5
+ 'id', f.id,
6
+ 'properties', properties || jsonb_build_object(
7
+ 'plate_id', plate_id,
8
+ 'young_lim', coalesce(young_lim, m.min_age),
9
+ 'old_lim', coalesce(old_lim, m.max_age)
10
+ ),
11
+ 'geometry', ST_AsGeoJSON(
12
+ ST_Intersection(
13
+ ST_Buffer(p.geometry,0),
14
+ ST_Buffer(f.geometry, 0)
15
+ )
16
+ )::jsonb,
17
+ 'type', 'Feature'
18
+ )) geojson
19
+ FROM corelle.feature f
20
+ JOIN corelle.plate_polygon p
21
+ ON p.geometry && f.geometry
22
+ AND ST_Intersects(p.geometry, f.geometry)
23
+ JOIN corelle.model m
24
+ ON m.id = p.model_id
25
+ WHERE dataset_id = :dataset_id
26
+ GROUP BY model_id
27
+ )
28
+ INSERT INTO corelle.feature_cache (model_id, model_name, geojson, dataset_id)
29
+ SELECT
30
+ model_id,
31
+ (SELECT name FROM corelle.model WHERE id = model_id) AS model_name,
32
+ geojson,
33
+ :dataset_id dataset_id
34
+ FROM a
35
+ ON CONFLICT (model_id, dataset_id)
36
+ DO UPDATE SET
37
+ geojson = EXCLUDED.geojson;
@@ -0,0 +1,7 @@
1
+ DELETE
2
+ FROM corelle.rotation_cache rc
3
+ USING corelle.plate_polygon pp
4
+ WHERE rc.model_id = pp.model_id
5
+ AND rc.plate_id = pp.plate_id
6
+ AND t_step > coalesce(old_lim, 2500)
7
+ OR t_step < coalesce(young_lim, 0);
@@ -0,0 +1,4 @@
1
+ SELECT geojson
2
+ FROM corelle.feature_cache
3
+ WHERE model_name = :model_name
4
+ AND dataset_id = :dataset
@@ -0,0 +1,21 @@
1
+ SELECT
2
+ f.id,
3
+ properties || jsonb_build_object(
4
+ 'plate_id', plate_id,
5
+ 'young_lim', coalesce(young_lim, m.min_age),
6
+ 'old_lim', coalesce(old_lim, m.max_age)) properties,
7
+ ST_AsGeoJSON(
8
+ ST_Intersection(
9
+ ST_Buffer(p.geometry,0),
10
+ ST_Buffer(f.geometry, 0)
11
+ )
12
+ )::json geometry,
13
+ 'Feature' "type"
14
+ FROM corelle.feature f
15
+ JOIN corelle.plate_polygon p
16
+ ON p.geometry && f.geometry
17
+ AND ST_Intersects(p.geometry, f.geometry)
18
+ JOIN corelle.model m
19
+ ON m.id = p.model_id
20
+ WHERE dataset_id = :dataset
21
+ AND model_id = (SELECT id FROM corelle.model WHERE name = :model_name)
@@ -0,0 +1,3 @@
1
+ SELECT geojson
2
+ FROM corelle.plate_polygon_cache
3
+ WHERE model_name = :model_name
@@ -0,0 +1,14 @@
1
+ SELECT
2
+ pp.id,
3
+ pp.plate_id,
4
+ coalesce(young_lim, 0) young_lim,
5
+ old_lim,
6
+ ST_AsGeoJSON(geometry) geometry,
7
+ p.name
8
+ FROM corelle.plate_polygon pp
9
+ JOIN corelle.plate p
10
+ ON pp.plate_id = p.id
11
+ AND pp.model_id = p.model_id
12
+ JOIN corelle.model m
13
+ ON p.model_id = m.id
14
+ WHERE m.name = :model_name
@@ -0,0 +1,7 @@
1
+ SELECT
2
+ plate_id
3
+ FROM corelle.plate_polygon p
4
+ WHERE model_id = (SELECT id FROM corelle.model WHERE name = :model_name)
5
+ AND ST_Intersects(p.geometry, ST_SetSRID(ST_MakePoint(:lon, :lat), 4326))
6
+ AND coalesce(young_lim,0) < :time
7
+ AND :time < old_lim
@@ -0,0 +1,9 @@
1
+ SELECT
2
+ plate_id id,
3
+ model_id,
4
+ coalesce(p.old_lim, m.max_age, 4500) old_lim,
5
+ coalesce(p.young_lim, m.min_age, 0) young_lim
6
+ FROM corelle.plate_polygon p
7
+ JOIN corelle.model m
8
+ ON m.id = p.model_id
9
+ WHERE m.name = :model_name
@@ -0,0 +1,6 @@
1
+ SELECT id
2
+ FROM corelle.plate
3
+ WHERE model_id = (
4
+ SELECT id
5
+ FROM corelle.model
6
+ WHERE name = :model_name)
@@ -0,0 +1,64 @@
1
+ WITH plate_steps AS (
2
+ /*
3
+ Rotations for a specific plate and model id
4
+ */
5
+ SELECT
6
+ plate_id,
7
+ ref_plate_id,
8
+ metadata,
9
+ t_step,
10
+ ARRAY[longitude, latitude, angle] rotation,
11
+ lag(t_step, 1) OVER (
12
+ ORDER BY plate_id, ref_plate_id, t_step
13
+ ) prev_step
14
+ FROM corelle.rotation
15
+ WHERE model_id = (
16
+ SELECT id
17
+ FROM corelle.model
18
+ WHERE name = :model_name)
19
+ ORDER BY t_step
20
+ ),
21
+ step_pairs AS (
22
+ /*
23
+ Paired steps for interpolated rotations.
24
+ Unlike our previous attempt at the math, these
25
+ MUST have the same ref_plate_id, because this is
26
+ how GPlates is actually structured.
27
+ */
28
+ SELECT
29
+ p1.plate_id,
30
+ p1.ref_plate_id,
31
+ p1.t_step r1_step,
32
+ p2.t_step r2_step,
33
+ p1.rotation r1_rotation,
34
+ p2.rotation r2_rotation,
35
+ p1.metadata r1_metadata,
36
+ p2.metadata r2_metadata
37
+ FROM plate_steps p1
38
+ JOIN plate_steps p2
39
+ ON p1.t_step = p2.prev_step
40
+ AND p1.ref_plate_id = p2.ref_plate_id
41
+ AND p1.plate_id = p2.plate_id
42
+ )
43
+ -- Get rotation that are exactly at `time`, if they exist
44
+ SELECT
45
+ plate_id,
46
+ ref_plate_id,
47
+ t_step r1_step,
48
+ null r2_step,
49
+ rotation r1_rotation,
50
+ null r2_rotation,
51
+ metadata r1_metadata,
52
+ null r2_metadata,
53
+ false interpolated
54
+ FROM plate_steps
55
+ WHERE t_step <= :early_age
56
+ AND t_step >= :late_age
57
+ UNION ALL
58
+ -- Get rotations that are after `time`
59
+ SELECT
60
+ *,
61
+ true interpolated
62
+ FROM step_pairs
63
+ WHERE r1_step <= :early_age
64
+ AND r2_step > :late_age
@@ -0,0 +1,63 @@
1
+ WITH plate_steps AS (
2
+ /*
3
+ Rotations for a specific plate and model id
4
+ */
5
+ SELECT
6
+ plate_id,
7
+ ref_plate_id,
8
+ metadata,
9
+ t_step,
10
+ ARRAY[longitude, latitude, angle] rotation,
11
+ lag(t_step, 1) OVER (
12
+ ORDER BY plate_id, ref_plate_id, t_step
13
+ ) prev_step
14
+ FROM corelle.rotation
15
+ WHERE model_id = (
16
+ SELECT id
17
+ FROM corelle.model
18
+ WHERE name = :model_name)
19
+ ORDER BY t_step
20
+ ),
21
+ step_pairs AS (
22
+ /*
23
+ Paired steps for interpolated rotations.
24
+ Unlike our previous attempt at the math, these
25
+ MUST have the same ref_plate_id, because this is
26
+ how GPlates is actually structured.
27
+ */
28
+ SELECT
29
+ p1.plate_id,
30
+ p1.ref_plate_id,
31
+ p1.t_step r1_step,
32
+ p2.t_step r2_step,
33
+ p1.rotation r1_rotation,
34
+ p2.rotation r2_rotation,
35
+ p1.metadata r1_metadata,
36
+ p2.metadata r2_metadata
37
+ FROM plate_steps p1
38
+ JOIN plate_steps p2
39
+ ON p1.t_step = p2.prev_step
40
+ AND p1.ref_plate_id = p2.ref_plate_id
41
+ AND p1.plate_id = p2.plate_id
42
+ )
43
+ -- Get rotation that are exactly at `time`, if they exist
44
+ SELECT
45
+ plate_id,
46
+ ref_plate_id,
47
+ t_step r1_step,
48
+ null r2_step,
49
+ rotation r1_rotation,
50
+ null r2_rotation,
51
+ metadata r1_metadata,
52
+ null r2_metadata,
53
+ false interpolated
54
+ FROM plate_steps
55
+ WHERE t_step = :time
56
+ UNION ALL
57
+ -- Get rotations that are after `time`
58
+ SELECT
59
+ *,
60
+ true interpolated
61
+ FROM step_pairs
62
+ WHERE r1_step < :time
63
+ AND r2_step > :time
@@ -0,0 +1,64 @@
1
+ WITH plate_steps AS (
2
+ /*
3
+ Rotations for a specific plate and model id
4
+ */
5
+ SELECT
6
+ plate_id,
7
+ ref_plate_id,
8
+ metadata,
9
+ t_step,
10
+ ARRAY[longitude, latitude, angle] rotation,
11
+ lag(t_step, 1) OVER (
12
+ ORDER BY plate_id, ref_plate_id, t_step
13
+ ) prev_step
14
+ FROM corelle.rotation
15
+ WHERE plate_id = :plate_id
16
+ AND model_id = (
17
+ SELECT id
18
+ FROM corelle.model
19
+ WHERE name = :model_name)
20
+ ORDER BY t_step
21
+ ),
22
+ step_pairs AS (
23
+ /*
24
+ Paired steps for interpolated rotations.
25
+ Unlike our previous attempt at the math, these
26
+ MUST have the same ref_plate_id, because this is
27
+ how GPlates is actually structured.
28
+ */
29
+ SELECT
30
+ p1.plate_id,
31
+ p1.ref_plate_id,
32
+ p1.t_step r1_step,
33
+ p2.t_step r2_step,
34
+ p1.rotation r1_rotation,
35
+ p2.rotation r2_rotation,
36
+ p1.metadata r1_metadata,
37
+ p2.metadata r2_metadata
38
+ FROM plate_steps p1
39
+ JOIN plate_steps p2
40
+ ON p1.t_step = p2.prev_step
41
+ AND p1.ref_plate_id = p2.ref_plate_id
42
+ AND p1.plate_id = p2.plate_id
43
+ )
44
+ -- Get rotation that are exactly at `time`, if they exist
45
+ SELECT
46
+ plate_id,
47
+ ref_plate_id,
48
+ t_step r1_step,
49
+ null r2_step,
50
+ rotation r1_rotation,
51
+ null r2_rotation,
52
+ metadata r1_metadata,
53
+ null r2_metadata,
54
+ false interpolated
55
+ FROM plate_steps
56
+ WHERE t_step = :time
57
+ UNION ALL
58
+ -- Get rotations that are after `time`
59
+ SELECT
60
+ *,
61
+ true interpolated
62
+ FROM step_pairs
63
+ WHERE r1_step < :time
64
+ AND r2_step > :time
@@ -0,0 +1,9 @@
1
+ UPDATE corelle.rotation_cache rc SET
2
+ proj4text = corelle.build_proj_string(rotation),
3
+ proj4inv = corelle.build_proj_string(corelle.invert_rotation(rotation)),
4
+ envelope = ST_Envelope(corelle.rotate_geometry(pp.geometry, rotation))
5
+ FROM corelle.plate_polygon pp
6
+ WHERE rc.model_id = :model_id
7
+ AND t_step = :t_step
8
+ AND rc.plate_id = pp.plate_id
9
+ AND rc.model_id = pp.model_id;
@@ -0,0 +1,265 @@
1
+ import numpy as N
2
+ import quaternion as Q
3
+ from click import secho
4
+ from decimal import Decimal
5
+
6
+ from corelle.math import cart2sph, sph2cart, euler_to_quaternion, quaternion_to_euler
7
+
8
+ from .query import get_sql
9
+ from .storage import model_id
10
+
11
+
12
+ class RotationError(Exception):
13
+ pass
14
+
15
+
16
+ class UndefinedRotationError(RotationError):
17
+ def __init__(self, plate_id, time):
18
+ super().__init__(self, f"No rotation found for plate {plate_id} at time {time}")
19
+ self.plate_id = plate_id
20
+ self.time = time
21
+
22
+
23
+ cache = {}
24
+ cache_list = []
25
+
26
+
27
+ def build_cache(q, cache_args):
28
+ # Builds an in-memory cache of rotations
29
+ cache[cache_args] = q
30
+ cache_list.append(cache_args)
31
+ if len(cache_list) > 50000:
32
+ id = cache_list.pop(0)
33
+ try:
34
+ del cache[id]
35
+ except KeyError:
36
+ pass
37
+ return q
38
+
39
+
40
+ __sql = get_sql("rotation-pairs")
41
+
42
+
43
+ def check_model_id(conn, model_name):
44
+ # Make sure our model id actually exists
45
+ id = model_id(conn, model_name)
46
+ if id is None:
47
+ raise RotationError("Unknown model id")
48
+
49
+
50
+ # Cache this expensive, recursive function.
51
+ def get_rotation(
52
+ conn,
53
+ model_name,
54
+ plate_id,
55
+ time,
56
+ verbose=False,
57
+ depth=0,
58
+ rowset=None,
59
+ safe=True,
60
+ ):
61
+ """Core function to rotate a plate to a time by accumulating quaternions"""
62
+ time = float(time)
63
+ cache_args = (model_name, plate_id, Decimal(time))
64
+ if cache_args in cache:
65
+ return cache[cache_args]
66
+
67
+ if safe:
68
+ check_model_id(conn, model_name)
69
+
70
+ __cache = lambda q: build_cache(q, cache_args)
71
+
72
+ prefix = " " * depth
73
+ if verbose:
74
+ secho(prefix + str(plate_id))
75
+
76
+ if plate_id is None or plate_id == 0:
77
+ return __cache(N.quaternion(1, 0, 0, 0))
78
+
79
+ params = dict(plate_id=plate_id, model_name=model_name, time=time)
80
+
81
+ row = None
82
+ pairs = []
83
+ if rowset:
84
+ for ix, p in enumerate(rowset):
85
+ if p.plate_id == plate_id and p.r1_step <= time:
86
+ pairs = [p]
87
+ # Remove this row from the rowset (prevents weird recursion errors)
88
+ rowset = rowset[:ix] + rowset[ix + 1 :]
89
+ break
90
+ else:
91
+ # Fall back to fetching the data ourselves
92
+ pairs = conn.execute(__sql, params).fetchall()
93
+ if len(pairs) == 0:
94
+ return __cache(None)
95
+ if verbose:
96
+ for i, pair in enumerate(pairs):
97
+ color = "green" if i == 0 else "white"
98
+ secho(prefix + f"{pair.plate_id} → {pair.ref_plate_id}", fg=color)
99
+
100
+ row = pairs[0]
101
+
102
+ q1 = euler_to_quaternion(row.r1_rotation)
103
+
104
+ if not row.interpolated:
105
+ base = get_rotation(
106
+ conn,
107
+ model_name,
108
+ row.ref_plate_id,
109
+ row.r1_step,
110
+ verbose=verbose,
111
+ depth=depth + 1,
112
+ rowset=rowset,
113
+ safe=False,
114
+ )
115
+ if base is None:
116
+ return __cache(None)
117
+ # We have an exact match!
118
+ # Just a precautionary guard, this should be assured by our SQL
119
+ return __cache(base * q1)
120
+
121
+ # Interpolated rotations
122
+ base = get_rotation(
123
+ conn,
124
+ model_name,
125
+ row.ref_plate_id,
126
+ time,
127
+ verbose=verbose,
128
+ depth=depth + 1,
129
+ rowset=rowset,
130
+ safe=False,
131
+ )
132
+ if base is None:
133
+ return __cache(None)
134
+
135
+ q2 = euler_to_quaternion(row.r2_rotation)
136
+ # Calculate interpolated rotation between the two timesteps
137
+ res = Q.slerp(q1, q2, float(row.r1_step), float(row.r2_step), time)
138
+ return __cache(base * res)
139
+
140
+
141
+ def get_plate_id(conn, point, model, time):
142
+ sql = get_sql("plate-for-point")
143
+ return conn.execute(
144
+ sql, dict(lon=point[0], lat=point[1], model_name=model, time=time)
145
+ ).scalar()
146
+
147
+
148
+ def rotate_point(conn, point, model, time):
149
+ if time == 0:
150
+ return point
151
+ plate_id = get_plate_id(conn, point, model, time)
152
+ if plate_id is None:
153
+ return None
154
+ q = get_rotation(conn, model, plate_id, time)
155
+ v0 = sph2cart(*point)
156
+ v1 = Q.rotate_vectors(q, v0)
157
+ return cart2sph(v1)
158
+
159
+
160
+ def rotate_point_with_plate(conn, point, model, time):
161
+ """The rotated point and its plate ID, or None for a point on no plate."""
162
+ plate_id = get_plate_id(conn, point, model, time)
163
+ if plate_id is None:
164
+ return None
165
+ q = get_rotation(conn, model, plate_id, time)
166
+ v1 = Q.rotate_vectors(q, sph2cart(*point))
167
+ return cart2sph(v1), plate_id
168
+
169
+
170
+ __tstep_rotation_pairs = get_sql("rotation-pairs-for-time")
171
+ __model_rotation_pairs = get_sql("rotation-pairs-for-model")
172
+ __active_plates_sql = get_sql("active-plates-at-time")
173
+ __model_plates_sql = get_sql("plates-for-model")
174
+ __plate_time_ranges = get_sql("plate-time-ranges")
175
+
176
+
177
+ def plates_for_model(conn, model):
178
+ """Returns a list of plate IDs for a given model"""
179
+ return [k[0] for k in conn.execute(__model_plates_sql, dict(model_name=model))]
180
+
181
+
182
+ def get_all_rotations(
183
+ conn,
184
+ model,
185
+ time,
186
+ verbose=False,
187
+ active_only=True,
188
+ plates=None,
189
+ safe=True,
190
+ rowset=None,
191
+ ):
192
+ """Get all rotations for a model and a timestep
193
+
194
+ Parameters
195
+ ----------
196
+ model : str
197
+ The rotation model to use
198
+ time : float
199
+ The time to rotate to, in Myr before present.
200
+
201
+ Yields
202
+ ------
203
+ dict
204
+ """
205
+ if plates is None:
206
+ if active_only:
207
+ res = conn.execute(__active_plates_sql, dict(time=time, model_name=model))
208
+ plates = [p[0] for p in res]
209
+ else:
210
+ plates = plates_for_model(conn, model)
211
+
212
+ if rowset is None:
213
+ _rowset = conn.execute(
214
+ __tstep_rotation_pairs, dict(time=time, model_name=model)
215
+ ).fetchall()
216
+ else:
217
+ _rowset = [
218
+ r
219
+ for r in rowset
220
+ if (r.r1_step == time and r.r2_step is None)
221
+ or (r.r1_step < time and (r.r2_step or -1) > time)
222
+ ]
223
+
224
+ if safe:
225
+ check_model_id(conn, model)
226
+ for plate_id in plates:
227
+ q = get_rotation(
228
+ conn, model, plate_id, time, verbose=verbose, rowset=_rowset, safe=False
229
+ )
230
+ if q is None:
231
+ continue
232
+ if N.isnan(q.w):
233
+ continue
234
+ yield plate_id, q
235
+
236
+
237
+ def get_rotation_series(conn, model, *times, **kwargs):
238
+ # Pre-compute plates for speed...
239
+ # LOL this actually makes things slower
240
+ check_model_id(conn, model)
241
+
242
+ plate_ages = conn.execute(
243
+ __plate_time_ranges,
244
+ dict(model_name=model),
245
+ ).fetchall()
246
+
247
+ rowset = conn.execute(
248
+ __model_rotation_pairs,
249
+ dict(
250
+ model_name=model,
251
+ early_age=float(max(times)),
252
+ late_age=float(min(times)),
253
+ ),
254
+ ).fetchall()
255
+ for t in times:
256
+ t = float(t)
257
+ plates = [p.id for p in plate_ages if p.old_lim > t and p.young_lim < t]
258
+ kwargs["plates"] = plates
259
+ r = get_all_rotations(conn, model, t, safe=False, rowset=rowset, **kwargs)
260
+ yield dict(rotations=list(r), time=t)
261
+
262
+
263
+ def get_plate_rotations(conn, model, plate_id, verbose=False):
264
+ time_steps = range(0, 500, 1)
265
+ return ((get_rotation(conn, model, plate_id, t), t) for t in time_steps)
@@ -0,0 +1,2 @@
1
+ CREATE EXTENSION IF NOT EXISTS postgis;
2
+ CREATE SCHEMA IF NOT EXISTS corelle;
@@ -0,0 +1,88 @@
1
+ CREATE TABLE IF NOT EXISTS corelle.model (
2
+ id serial PRIMARY KEY,
3
+ name text UNIQUE NOT NULL,
4
+ min_age numeric DEFAULT 0,
5
+ max_age numeric
6
+ );
7
+
8
+ CREATE TABLE IF NOT EXISTS corelle.plate (
9
+ id integer,
10
+ model_id integer NOT NULL REFERENCES corelle.model(id),
11
+ parent_id integer,
12
+ name text,
13
+ cotid text,
14
+ coid text,
15
+ PRIMARY KEY (id, model_id),
16
+ FOREIGN KEY (parent_id, model_id)
17
+ REFERENCES corelle.plate (id, model_id)
18
+ ON DELETE CASCADE
19
+ );
20
+
21
+ CREATE TABLE IF NOT EXISTS corelle.plate_polygon (
22
+ id serial PRIMARY KEY,
23
+ plate_id integer NOT NULL,
24
+ model_id integer NOT NULL,
25
+ young_lim numeric,
26
+ old_lim numeric,
27
+ geometry geometry(MultiPolygon, 4326),
28
+ FOREIGN KEY (plate_id, model_id)
29
+ REFERENCES corelle.plate (id, model_id)
30
+ ON DELETE CASCADE
31
+ );
32
+
33
+ CREATE INDEX IF NOT EXISTS
34
+ corelle_plate_polygon_model_id_idx
35
+ ON corelle.plate_polygon (plate_id, model_id);
36
+
37
+ CREATE INDEX
38
+ IF NOT EXISTS corelle_plate_polygon_geometry_idx
39
+ ON corelle.plate_polygon
40
+ USING gist (geometry);
41
+
42
+ CREATE TABLE IF NOT EXISTS corelle.rotation (
43
+ id serial PRIMARY KEY,
44
+ plate_id integer NOT NULL,
45
+ model_id integer NOT NULL,
46
+ t_step numeric NOT NULL,
47
+ latitude numeric NOT NULL,
48
+ longitude numeric NOT NULL,
49
+ angle numeric NOT NULL,
50
+ ref_plate_id integer NOT NULL,
51
+ metadata text,
52
+ UNIQUE (plate_id, model_id, t_step, ref_plate_id),
53
+ FOREIGN KEY (plate_id, model_id)
54
+ REFERENCES corelle.plate (id, model_id)
55
+ ON DELETE CASCADE,
56
+ FOREIGN KEY (ref_plate_id, model_id)
57
+ REFERENCES corelle.plate (id, model_id)
58
+ ON DELETE CASCADE
59
+ );
60
+
61
+ -- Features that can be clipped by plate IDs and returned
62
+ CREATE TABLE IF NOT EXISTS corelle.feature (
63
+ id serial PRIMARY KEY,
64
+ dataset_id text NOT NULL,
65
+ geometry geometry(Geometry, 4326),
66
+ properties jsonb
67
+ );
68
+
69
+ /* Cache of features pre-converted to GeoJSON (probably should be deprecated!)*/
70
+ CREATE TABLE IF NOT EXISTS corelle.feature_cache (
71
+ model_id integer NOT NULL REFERENCES corelle.model(id),
72
+ model_name text NOT NULL REFERENCES corelle.model(name),
73
+ dataset_id text NOT NULL,
74
+ geojson json NOT NULL,
75
+ PRIMARY KEY (model_id, dataset_id)
76
+ );
77
+
78
+ /* Cache of rotations at various time steps */
79
+ CREATE TABLE IF NOT EXISTS corelle.rotation_cache (
80
+ model_id integer NOT NULL REFERENCES corelle.model(id),
81
+ plate_id integer NOT NULL,
82
+ t_step decimal NOT NULL,
83
+ rotation numeric[],
84
+ PRIMARY KEY (model_id, plate_id, t_step),
85
+ FOREIGN KEY (plate_id, model_id)
86
+ REFERENCES corelle.plate (id, model_id)
87
+ ON DELETE CASCADE
88
+ );