@fxi/zartigl 0.1.2 → 0.1.4

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.
package/README.md CHANGED
@@ -59,6 +59,70 @@ This requires Python >= 3.12, [uv](https://docs.astral.sh/uv/), and a free [Cope
59
59
 
60
60
  [GLOBAL_ANALYSISFORECAST_PHY_001_024](https://data.marine.copernicus.eu/product/GLOBAL_ANALYSISFORECAST_PHY_001_024) from Copernicus Marine Service — `uo`/`vo` surface current velocity at ~0.083deg resolution, streamed as ARCO Zarr directly from Copernicus S3.
61
61
 
62
+ ## Other use cases
63
+
64
+ Any 2D vector field stored as U/V components in a Zarr store works as a source. Examples:
65
+
66
+ - **Atmospheric wind** — reanalysis products (ERA5, MERRA-2) or NWP model output; visualise jet streams, storm tracks, or surface winds at any pressure level
67
+ - **Ocean surface waves** — Stokes drift or wave propagation direction fields
68
+ - **Ice drift** — sea-ice motion vectors from passive microwave retrievals
69
+ - **River discharge / flood routing** — depth-averaged U/V from hydrodynamic models (Delft3D, SCHISM)
70
+ - **Air quality transport** — horizontal advection of pollutant plumes from dispersion models
71
+ - **Climate projections** — CMIP6 ocean or atmosphere circulation at any time slice; compare historical vs. SSP scenarios
72
+ - **Groundwater flow** — Darcy velocity fields from subsurface models
73
+ - **Wildfire smoke** — wind-driven particle transport overlaid on active fire data
74
+
75
+ The only requirement is a Zarr v2 store with two named float variables representing the east and north components of the field.
76
+
77
+ ## How it works
78
+
79
+ A plain-language walkthrough for GIS analysts unfamiliar with WebGL or Zarr.
80
+
81
+ ### 1. Zarr is a tiled raster cube
82
+
83
+ Think of a Cloud Optimized GeoTIFF extended into four dimensions: longitude × latitude × depth × time. The data is split into chunks — here: 1 time step × 1 depth level × 512 rows × 2048 columns. Zartigl requests only the chunks that intersect the current viewport at the selected time and depth, exactly like a COG tile request.
84
+
85
+ ### 2. Velocity becomes a two-channel raster
86
+
87
+ Each chunk holds two float arrays — **U** (eastward m/s) and **V** (northward m/s). These are normalized and packed into the R and G channels of an image uploaded to the GPU:
88
+
89
+ ```
90
+ R = eastward velocity (0 → max west, 128 → still, 255 → max east)
91
+ G = northward velocity
92
+ A = 255 (ocean) | 0 (land / nodata) ← validity mask
93
+ ```
94
+
95
+ The result is a global velocity raster in GPU memory. The alpha channel is a nodata mask.
96
+
97
+ ### 3. Particles are virtual drifters
98
+
99
+ Thousands of virtual oceanographic drifters are seeded randomly across the viewport. Every frame (~60 fps), the GPU runs this logic for all of them simultaneously:
100
+
101
+ > *Where am I? → sample the velocity raster at my position → move a tiny step in that direction → if I drift off-screen or hit land (A = 0), teleport to a random ocean pixel inside the viewport.*
102
+
103
+ Same logic as a drift model — just running on 65 000+ drifters at 60 frames per second.
104
+
105
+ ### 4. Trails are faded time-lapse
106
+
107
+ Before each new frame, the previous frame is multiplied by ~0.9999. Particles leave a fading streak — like a long-exposure photograph of drifters. Streak length encodes time; color encodes speed magnitude via a configurable color ramp.
108
+
109
+ ### 5. Particle state lives in GPU memory
110
+
111
+ Drifter positions are never stored in a CPU array. They are encoded as pixel colors in a GPU texture (X → red, Y → blue, sub-pixel precision in the low byte). Two textures alternate each frame — read from A, write to B, swap — so the GPU never waits on itself. This is the **ping-pong** pattern.
112
+
113
+ ### Pipeline at a glance
114
+
115
+ | Step | What happens | GIS analogy |
116
+ |---|---|---|
117
+ | Fetch | Request Zarr chunks for viewport + time + depth | COG tile request |
118
+ | Pack | Encode U/V floats into R/G/A texture | Raster → GPU image |
119
+ | Seed | Place N drifters randomly in viewport | Random point sampling |
120
+ | Advect | Each frame: sample velocity, move each drifter | Drift model at 60 fps |
121
+ | Mask | Discard drifters over land (A = 0) | Nodata mask |
122
+ | Fade | Dim previous frame slightly | Long-exposure photography |
123
+ | Color | Map speed to color ramp | Graduated color by value |
124
+ | Composite | Blend result over basemap | Raster layer blending |
125
+
62
126
  ## License
63
127
 
64
128
  MIT
@@ -1,20 +1,20 @@
1
1
  {
2
- "generated": "2026-02-11T08:27:15.746557+00:00",
3
- "datasets": [
2
+ "generated": "2026-03-18T12:00:00.000000+00:00",
3
+ "views": [
4
4
  {
5
- "id": "cmems_mod_glo_phy-cur_anfc_0.083deg_PT6H-i",
6
- "product": "GLOBAL_ANALYSISFORECAST_PHY_001_024",
7
- "label": "Global Ocean Currents (analysis+forecast)",
8
- "zarr_url": "https://s3.waw3-1.cloudferro.com/mdl-arco-time-009/arco/GLOBAL_ANALYSISFORECAST_PHY_001_024/cmems_mod_glo_phy-cur_anfc_0.083deg_PT6H-i_202406/timeChunked.zarr",
9
- "variables": {
10
- "uo": {
11
- "standard_name": "eastward_sea_water_velocity",
12
- "units": "m s-1"
13
- },
14
- "vo": {
15
- "standard_name": "northward_sea_water_velocity",
16
- "units": "m s-1"
17
- }
5
+ "id": "ocean-current-velocity",
6
+ "label": "Ocean Current Velocity",
7
+ "description": "Surface ocean currents — global analysis/forecast, 6-hourly",
8
+ "category": "Ocean",
9
+ "type": "vector",
10
+ "source_dataset": "cmems_mod_glo_phy-cur_anfc_0.083deg_PT6H-i",
11
+ "zarr_url_geo": "https://s3.waw3-1.cloudferro.com/mdl-arco-time-009/arco/GLOBAL_ANALYSISFORECAST_PHY_001_024/cmems_mod_glo_phy-cur_anfc_0.083deg_PT6H-i_202406/timeChunked.zarr",
12
+ "zarr_url_time": "https://s3.waw3-1.cloudferro.com/mdl-arco-geo-009/arco/GLOBAL_ANALYSISFORECAST_PHY_001_024/cmems_mod_glo_phy-cur_anfc_0.083deg_PT6H-i_202406/geoChunked.zarr",
13
+ "variable_u": "uo",
14
+ "variable_v": "vo",
15
+ "variable_meta": {
16
+ "standard_name": "sea_water_velocity",
17
+ "units": "m s-1"
18
18
  },
19
19
  "dimensions": {
20
20
  "depth": {
@@ -95,13 +95,256 @@
95
95
  },
96
96
  "time": {
97
97
  "axis": "t",
98
- "size": 5445,
98
+ "size": 5585,
99
99
  "min": 1654041600000.0,
100
- "max": 1771632000000.0,
100
+ "max": 1774656000000.0,
101
101
  "step": 21600000.0,
102
102
  "chunk_size": 1,
103
103
  "units": "milliseconds since 1970-01-01 00:00:00Z (no leap seconds)"
104
104
  }
105
+ },
106
+ "vertical_label": "depth",
107
+ "defaults": {
108
+ "palette": "rdylbu",
109
+ "renderMode": "particles",
110
+ "particleDensity": 0.033,
111
+ "speedMin": 0.14,
112
+ "speedMax": 1.11,
113
+ "fadeMin": 0.9,
114
+ "fadeMax": 0.9913000000000001,
115
+ "dropRate": 0.003,
116
+ "dropRateBump": 0,
117
+ "opacity": 1,
118
+ "logScale": true,
119
+ "vibrance": 0.39
120
+ }
121
+ },
122
+ {
123
+ "id": "surface-wind",
124
+ "label": "Surface Wind",
125
+ "description": "Global hourly sea surface wind from scatterometer and model",
126
+ "category": "Atmosphere",
127
+ "type": "vector",
128
+ "source_dataset": "cmems_obs-wind_glo_phy_nrt_l4_0.125deg_PT1H",
129
+ "zarr_url_geo": "https://s3.waw3-1.cloudferro.com/mdl-arco-time-050/arco/WIND_GLO_PHY_L4_NRT_012_004/cmems_obs-wind_glo_phy_nrt_l4_0.125deg_PT1H_202207/timeChunked.zarr",
130
+ "zarr_url_time": "https://s3.waw3-1.cloudferro.com/mdl-arco-geo-050/arco/WIND_GLO_PHY_L4_NRT_012_004/cmems_obs-wind_glo_phy_nrt_l4_0.125deg_PT1H_202207/geoChunked.zarr",
131
+ "variable_u": "eastward_wind",
132
+ "variable_v": "northward_wind",
133
+ "variable_meta": {
134
+ "standard_name": "wind_velocity",
135
+ "units": "m s-1"
136
+ },
137
+ "dimensions": {
138
+ "time": {
139
+ "axis": "t",
140
+ "size": 20352,
141
+ "min": 1700438400000.0,
142
+ "max": 1773702000000.0,
143
+ "step": 3600000.0,
144
+ "chunk_size": 1,
145
+ "units": "milliseconds since 1970-01-01 00:00:00Z (no leap seconds)"
146
+ },
147
+ "latitude": {
148
+ "axis": "y",
149
+ "size": 1440,
150
+ "min": -89.9375,
151
+ "max": 89.9375,
152
+ "step": 0.125,
153
+ "chunk_size": 512,
154
+ "units": "degrees_north"
155
+ },
156
+ "longitude": {
157
+ "axis": "x",
158
+ "size": 2880,
159
+ "min": -179.9375,
160
+ "max": 179.9375,
161
+ "step": 0.125,
162
+ "chunk_size": 1024,
163
+ "units": "degrees_east"
164
+ }
165
+ },
166
+ "defaults": {
167
+ "palette": "rdylbu",
168
+ "renderMode": "particles",
169
+ "particleDensity": 0.004000000000000002,
170
+ "speedMin": 0.010000000000000005,
171
+ "speedMax": 0.07000000000000002,
172
+ "fadeMin": 0.9174,
173
+ "fadeMax": 0.9793000000000001,
174
+ "dropRate": 0.005,
175
+ "dropRateBump": 0,
176
+ "opacity": 0.9,
177
+ "logScale": true,
178
+ "vibrance": 1
179
+ }
180
+ },
181
+ {
182
+ "id": "phytoplankton-chlorophyll",
183
+ "label": "Phytoplankton (Chlorophyll-a)",
184
+ "description": "Daily surface chlorophyll-a concentration",
185
+ "category": "Biology",
186
+ "type": "scalar",
187
+ "source_dataset": "cmems_mod_glo_bgc-pft_anfc_0.25deg_P1D-m",
188
+ "zarr_url_geo": "https://s3.waw3-1.cloudferro.com/mdl-arco-time-006/arco/GLOBAL_ANALYSISFORECAST_BGC_001_028/cmems_mod_glo_bgc-pft_anfc_0.25deg_P1D-m_202311/timeChunked.zarr",
189
+ "zarr_url_time": "https://s3.waw3-1.cloudferro.com/mdl-arco-geo-006/arco/GLOBAL_ANALYSISFORECAST_BGC_001_028/cmems_mod_glo_bgc-pft_anfc_0.25deg_P1D-m_202311/geoChunked.zarr",
190
+ "variable": "chl",
191
+ "variable_meta": {
192
+ "standard_name": "mass_concentration_of_chlorophyll_a_in_sea_water",
193
+ "units": "mg m-3"
194
+ },
195
+ "dimensions": {
196
+ "time": {
197
+ "axis": "t",
198
+ "size": 1608,
199
+ "min": 1635724800000.0,
200
+ "max": 1774569600000.0,
201
+ "step": 86400000.0,
202
+ "chunk_size": 1,
203
+ "units": "milliseconds since 1970-01-01 00:00:00Z (no leap seconds)"
204
+ },
205
+ "depth": {
206
+ "axis": "z",
207
+ "values": [
208
+ 5727.91650390625,
209
+ 5274.7841796875,
210
+ 4833.29052734375,
211
+ 4405.22412109375,
212
+ 3992.48388671875,
213
+ 3597.031982421875,
214
+ 3220.8203125,
215
+ 2865.70263671875,
216
+ 2533.336181640625,
217
+ 2225.077880859375,
218
+ 1941.8934326171875,
219
+ 1684.284423828125,
220
+ 1452.2509765625,
221
+ 1245.2911376953125,
222
+ 1062.439697265625,
223
+ 902.3392944335938,
224
+ 763.3330688476562,
225
+ 643.5668334960938,
226
+ 541.0889282226562,
227
+ 453.937744140625,
228
+ 380.2130126953125,
229
+ 318.12744140625,
230
+ 266.0402526855469,
231
+ 222.4751739501953,
232
+ 186.1255645751953,
233
+ 155.85072326660156,
234
+ 130.66598510742188,
235
+ 109.72927856445312,
236
+ 92.3260726928711,
237
+ 77.85385131835938,
238
+ 65.8072738647461,
239
+ 55.76428985595703,
240
+ 47.373687744140625,
241
+ 40.344051361083984,
242
+ 34.43415451049805,
243
+ 29.44472885131836,
244
+ 25.211408615112305,
245
+ 21.59881591796875,
246
+ 18.495559692382812,
247
+ 15.810072898864746,
248
+ 13.467138290405273,
249
+ 11.40500259399414,
250
+ 9.572997093200684,
251
+ 7.92956018447876,
252
+ 6.440614223480225,
253
+ 5.078223705291748,
254
+ 3.8194947242736816,
255
+ 2.6456685066223145,
256
+ 1.5413753986358643,
257
+ 0.4940253794193268
258
+ ],
259
+ "size": 50,
260
+ "chunk_size": 1,
261
+ "units": "m"
262
+ },
263
+ "latitude": {
264
+ "axis": "y",
265
+ "size": 681,
266
+ "min": -80.0,
267
+ "max": 90.0,
268
+ "step": 0.25,
269
+ "chunk_size": 681,
270
+ "units": "degrees_north"
271
+ },
272
+ "longitude": {
273
+ "axis": "x",
274
+ "size": 1440,
275
+ "min": -180.0,
276
+ "max": 179.75,
277
+ "step": 0.25,
278
+ "chunk_size": 1440,
279
+ "units": "degrees_east"
280
+ }
281
+ },
282
+ "vertical_label": "depth",
283
+ "defaults": {
284
+ "palette": "matter",
285
+ "renderMode": "raster",
286
+ "opacity": 0.9,
287
+ "logScale": true,
288
+ "vibrance": 0.2
289
+ }
290
+ }
291
+ ,{
292
+ "id": "wave-stokes-drift",
293
+ "label": "Wave Stokes Drift",
294
+ "description": "Net horizontal wave-driven transport (Stokes drift) from global 3-hourly wave analysis — visualises wave energy propagation direction.",
295
+ "category": "Ocean",
296
+ "type": "vector",
297
+ "source_dataset": "cmems_mod_glo_wav_anfc_0.083deg_PT3H-i",
298
+ "zarr_url_geo": "https://s3.waw3-1.cloudferro.com/mdl-arco-time-015/arco/GLOBAL_ANALYSISFORECAST_WAV_001_027/cmems_mod_glo_wav_anfc_0.083deg_PT3H-i_202411/timeChunked.zarr",
299
+ "zarr_url_time": "https://s3.waw3-1.cloudferro.com/mdl-arco-geo-015/arco/GLOBAL_ANALYSISFORECAST_WAV_001_027/cmems_mod_glo_wav_anfc_0.083deg_PT3H-i_202411/geoChunked.zarr",
300
+ "variable_u": "VSDX",
301
+ "variable_v": "VSDY",
302
+ "variable_meta": {
303
+ "standard_name": "sea_surface_wave_stokes_drift_velocity",
304
+ "units": "m s-1"
305
+ },
306
+ "dimensions": {
307
+ "time": {
308
+ "axis": "t",
309
+ "size": 9944,
310
+ "min": 1667271600000,
311
+ "max": 1774656000000,
312
+ "step": 10800000,
313
+ "chunk_size": 1,
314
+ "units": "milliseconds since 1970-01-01 00:00:00Z (no leap seconds)"
315
+ },
316
+ "latitude": {
317
+ "axis": "y",
318
+ "size": 2041,
319
+ "min": -80.0,
320
+ "max": 90.0,
321
+ "step": 0.08333333333333333,
322
+ "chunk_size": 512,
323
+ "units": "degrees_north"
324
+ },
325
+ "longitude": {
326
+ "axis": "x",
327
+ "size": 4320,
328
+ "min": -180.0,
329
+ "max": 179.91666666666663,
330
+ "step": 0.08333333333333333,
331
+ "chunk_size": 2048,
332
+ "units": "degrees_east"
333
+ }
334
+ },
335
+ "defaults": {
336
+ "palette": "rdylbu",
337
+ "renderMode": "particles",
338
+ "particleDensity": 0.024,
339
+ "speedMin": 0.010000000000000005,
340
+ "speedMax": 1.59,
341
+ "fadeMin": 0.975,
342
+ "fadeMax": 0.9935,
343
+ "dropRate": 0.003,
344
+ "dropRateBump": 0,
345
+ "opacity": 1,
346
+ "logScale": true,
347
+ "vibrance": 1
105
348
  }
106
349
  }
107
350
  ]