dpolcat 0.2.0.dev1__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.
dpolcat/utils.py ADDED
@@ -0,0 +1,412 @@
1
+ import marimo
2
+
3
+ __generated_with = "0.25.1"
4
+ app = marimo.App(width="medium")
5
+
6
+ with app.setup:
7
+ from collections import defaultdict
8
+ from collections.abc import Iterable
9
+ from functools import partial
10
+ from pathlib import Path
11
+ import tomllib
12
+ from typing import Literal
13
+ import xml.etree.ElementTree as ET
14
+
15
+ import bokeh.models
16
+ import dask
17
+ # from dask.diagnostics import ProgressBar
18
+ import geopandas as gpd
19
+ import numpy as np
20
+ import odc.stac
21
+ # from osgeo import gdal
22
+ import pandas as pd
23
+ import pystac
24
+ import pystac_client
25
+ # import planetary_computer
26
+ import shapely.geometry as sg
27
+ import xarray
28
+ import holoviews as hv
29
+ import hvplot.pandas
30
+ import hvplot.xarray
31
+ import rioxarray
32
+ import zarr
33
+
34
+ import dpolcat.core as dpolcat
35
+ import dpolcat.fast_frost as ff
36
+ from dpolcat.pc_rio_driver import PCRioDriver
37
+
38
+ # gdal.UseExceptions()
39
+
40
+ # hv.extension("bokeh")
41
+
42
+ # Want total coverage, with some wiggle room due to reprojection
43
+ MIN_COVERAGE_FRACTION = 0.99
44
+
45
+
46
+ @app.cell
47
+ def _():
48
+ import marimo as mo
49
+
50
+ return (mo,)
51
+
52
+
53
+ @app.cell(hide_code=True)
54
+ def _(mo):
55
+ mo.md(r"""
56
+ Luke McQuade, October 2026
57
+
58
+ # `dpolcat utils`
59
+
60
+ Utility/helper functions etc. for working with *dpolcat* in various settings.
61
+
62
+ /// admonition | Info
63
+
64
+ This notebook is designed around the concept of [marimo reusable functions](https://docs.marimo.io/guides/reusing_functions/). Be careful changing things!
65
+
66
+ ///
67
+ """)
68
+ return
69
+
70
+
71
+ @app.cell(hide_code=True)
72
+ def _(mo):
73
+ mo.md(r"""
74
+ ## Search
75
+
76
+ Finding Sentinel-1 STAC items that work well together for a *dpolcat* time series is a little more involved than for optical imagery. For consistent comparisons over time, especially in hilly/mountainous areas of interest (AoIs), the sensing geometry should be as similar as possible. I.e., items should should have the same orbit direction and relative orbit number. However, items covering the AoI with the same `datatake_id` (continuous take over a day's orbit), can be safely combined (which are then referred to as 'scenes'). These are the rules governing the search functions here.
77
+ """)
78
+ return
79
+
80
+
81
+ @app.function
82
+ def find_stac_items(
83
+ aoi: gpd.GeoDataFrame, date_range: str
84
+ ) -> gpd.GeoDataFrame:
85
+ """
86
+ Find Sentinel-1 RTC scenes from the Microsoft Planetary Computer catalog intersecting the `aoi`.
87
+ - `aoi` is a single-row GeoDataFrame.
88
+ - `date_range` is a string with format `{from_date}/{to_date}`, e.g., "2020-01-01/2020-03-31".
89
+
90
+ Returns a GeoDataFrame representing the STAC items, ordered by datetime. The original pystac.Item is stashed in the _item field.
91
+ """
92
+ STAC_COLLECTION = "sentinel-1-rtc"
93
+
94
+ catalog = pystac_client.Client.open(
95
+ "https://planetarycomputer.microsoft.com/api/stac/v1",
96
+ )
97
+
98
+ if len(aoi.geometry) != 1:
99
+ raise ValueError(
100
+ "Expected only a single geometry for the given AoI."
101
+ )
102
+
103
+ search = catalog.search(
104
+ collections=[STAC_COLLECTION],
105
+ datetime=date_range,
106
+ intersects=aoi.to_crs("EPSG:4326").geometry[0],
107
+ )
108
+
109
+ items = search.item_collection()
110
+
111
+ items_gdf = gpd.GeoDataFrame.from_features(items, crs="EPSG:4326")
112
+
113
+ # Add the item ID, which isn't included in the above.
114
+ items_gdf["id"] = [item.id for item in items]
115
+
116
+ # datetime is originally a string, so convert it.
117
+ items_gdf["datetime"] = pd.to_datetime(items_gdf["datetime"])
118
+
119
+ # Stash the original item (a shallow copy) for later.
120
+ items_gdf["_item"] = items
121
+
122
+ return items_gdf.sort_values("datetime")
123
+
124
+
125
+ @app.function
126
+ def calc_coverages(
127
+ items_gdf: gpd.GeoDataFrame, aoi: gpd.GeoDataFrame
128
+ ) -> pd.DataFrame:
129
+ """
130
+ Calculate full vs partial scene coverages by orbit for the given AoI, grouped by `datatake_id`, i.e.,
131
+ approx. same time.
132
+ - `items_gdf` is a GeoDataFrame representing the STAC items.
133
+ - `aoi` is a single-row GeoDataFrame.
134
+
135
+ Returns a DataFrame containing the orbit, and number of `full_scenes` and `partial_scenes` covered,
136
+ ordered by number of `full_scenes` (highest first).
137
+ """
138
+
139
+ # Reproject to UTM for area calculation, which is planar.
140
+ utm_crs = items_gdf.estimate_utm_crs()
141
+ items_gdf = items_gdf.to_crs(utm_crs)
142
+ aoi = aoi.to_crs(utm_crs)
143
+
144
+ intersection = items_gdf.dissolve(
145
+ by=["sat:orbit_state", "sat:relative_orbit", "s1:datatake_id"]
146
+ ).intersection(aoi.geometry[0])
147
+ overlap_fraction = (intersection.area / aoi.area[0]).rename(
148
+ "overlap_fraction"
149
+ )
150
+
151
+ full_scenes_count = (
152
+ overlap_fraction[overlap_fraction >= MIN_COVERAGE_FRACTION]
153
+ .groupby(["sat:orbit_state", "sat:relative_orbit"])
154
+ .count()
155
+ )
156
+ partial_scenes_count = (
157
+ overlap_fraction[overlap_fraction < MIN_COVERAGE_FRACTION]
158
+ .groupby(["sat:orbit_state", "sat:relative_orbit"])
159
+ .count()
160
+ )
161
+
162
+ df_coverages = pd.DataFrame(
163
+ {
164
+ "full_scenes": full_scenes_count,
165
+ "partial_scenes": partial_scenes_count,
166
+ }
167
+ ).fillna(0)
168
+
169
+ return df_coverages.sort_values("full_scenes", ascending=False)
170
+
171
+
172
+ @app.function
173
+ def select_by_orbit(
174
+ items_gdf,
175
+ orbit_direction: Literal["ascending", "descending"],
176
+ relative_orbit: int,
177
+ ) -> Iterable[pystac.Item]:
178
+ """
179
+ Selects the STAC items for the given orbit direction and relative orbit.
180
+ - `items_gdf` is a GeoDataFrame representing the source STAC items.
181
+ - `orbit_direction` is either "ascending" or "descending".
182
+ - `relative_orbit` is the relative orbit number.
183
+ Returns a filtered collection of `pystac.Item`s.
184
+ """
185
+ return items_gdf.loc[
186
+ (items_gdf["sat:orbit_state"] == orbit_direction)
187
+ & (items_gdf["sat:relative_orbit"] == relative_orbit)
188
+ ]
189
+
190
+
191
+ @app.cell(hide_code=True)
192
+ def _(mo):
193
+ mo.md(r"""
194
+ ### Test/demo
195
+ """)
196
+ return
197
+
198
+
199
+ @app.cell
200
+ def _():
201
+ test_aoi = gpd.read_file("gis/aois/salzburg_south_4326.gpkg")
202
+ test_date_range = "2025-03-01/2025-03-31"
203
+ return test_aoi, test_date_range
204
+
205
+
206
+ @app.cell
207
+ def _(test_aoi, test_date_range):
208
+ test_items = find_stac_items(aoi=test_aoi, date_range=test_date_range)
209
+ return (test_items,)
210
+
211
+
212
+ @app.cell
213
+ def _(test_items):
214
+ test_items
215
+ return
216
+
217
+
218
+ @app.cell
219
+ def _(test_aoi, test_items):
220
+ test_scene_coverages = calc_coverages(test_items, aoi=test_aoi)
221
+ test_scene_coverages
222
+ return (test_scene_coverages,)
223
+
224
+
225
+ @app.cell(hide_code=True)
226
+ def _(mo):
227
+ mo.md(r"""
228
+ Find the orbit with the highest number of fully-covering scenes.
229
+ """)
230
+ return
231
+
232
+
233
+ @app.cell
234
+ def _(test_scene_coverages):
235
+ _best_match = test_scene_coverages.reset_index().iloc[0]
236
+ test_best_match = _best_match["sat:orbit_state"], _best_match["sat:relative_orbit"]
237
+ test_best_match
238
+ return (test_best_match,)
239
+
240
+
241
+ @app.cell
242
+ def _(test_best_match, test_items):
243
+ test_sel_items = select_by_orbit(test_items,*test_best_match)
244
+ test_sel_items["_item"].values
245
+ return
246
+
247
+
248
+ @app.cell(hide_code=True)
249
+ def _(mo):
250
+ mo.md(r"""
251
+ ## Analysis
252
+ """)
253
+ return
254
+
255
+
256
+ @app.function
257
+ def frost_xr(
258
+ da: xarray.DataArray, nanny=True, damping_factor=2.0, win_size=5
259
+ ) -> xarray.DataArray:
260
+ """Apply 2D (y, x) Frost filter to 3D (y, x, time) xarray DataArray; See underlying filter function for damping_factor and win_size."""
261
+ # Mostly Claude (Opus 5.5)
262
+ f = ff.frost_filter_nanny if nanny else ff.frost_filter_fast
263
+ g = np.vectorize(
264
+ partial(f, damping_factor=damping_factor, win_size=win_size),
265
+ signature="(m,n)->(m,n)",
266
+ )
267
+
268
+ # In case da holds a materialized np array, operate directly...
269
+ if da.chunks is None:
270
+ return da.copy(data=g(da.data))
271
+
272
+
273
+ # ...otherwise, set up parallel calc with overlapping windows
274
+ # (because it's a neighbourhood operation).
275
+ h = win_size // 2
276
+ data = da.data.map_overlap(
277
+ g,
278
+ depth=(0, h, h),
279
+ boundary="none",
280
+ meta=np.array((), dtype=da.dtype),
281
+ )
282
+ return da.copy(data=data)
283
+
284
+
285
+ @app.function
286
+ def normalize_db(arr, min=-30, max=10):
287
+ """Shift, scale and clip the input array in dB to the range 0 to 1"""
288
+ # E.g. range -30 to +10 -> 0.0 to 1.0
289
+
290
+ return ((arr - min) / (max - min)).clip(0.0, 1.0)
291
+
292
+
293
+ @app.cell(hide_code=True)
294
+ def _(mo):
295
+ mo.md(r"""
296
+ ## Visualization
297
+ """)
298
+ return
299
+
300
+
301
+ @app.function
302
+ def make_cmap():
303
+ """Make a HoloViews-compatible categorical colormap (string key is needed)."""
304
+ return {
305
+ str(i): list(dpolcat.color_list[i])
306
+ for i in range(len(dpolcat.color_list))
307
+ }
308
+
309
+
310
+ @app.function
311
+ def get_hex_colors(cat_indexes = range(dpolcat.NUM_CATEGORIES)):
312
+ """Make a hex-string-format colour list for the given categories."""
313
+ return [
314
+ f"#{r:02x}{g:02x}{b:02x}"
315
+ for r, g, b in dpolcat.color_list[cat_indexes]
316
+ ]
317
+
318
+
319
+ @app.function
320
+ def make_legend(size=350):
321
+ """Create a legend (in the form of a Holoviews plot)."""
322
+ n_steps = 100
323
+ steps = np.linspace(-30.5, 10.5, n_steps)
324
+ vv_ramp = np.vstack([steps] * n_steps)
325
+ vh_ramp = vv_ramp.T
326
+
327
+ cat_values = xarray.DataArray(
328
+ dpolcat.categorize_np(vv_ramp, vh_ramp),
329
+ coords=[steps, steps],
330
+ dims=["VH", "VV"],
331
+ )
332
+
333
+ label_data = []
334
+ for i in range(1, dpolcat.NUM_CATEGORIES):
335
+ box = cat_values.where(cat_values == i, drop=True)
336
+ center_x = float(box["VV"].mean())
337
+ center_y = float(box["VH"].mean())
338
+ label_data.append((center_x, center_y, str(i)))
339
+
340
+ labels = hv.Labels(
341
+ label_data, kdims=["VV", "VH"], vdims=["text"]
342
+ ).opts(text_color="black")
343
+
344
+ return (
345
+ cat_values.astype("str")
346
+ .hvplot(cmap=make_cmap())
347
+ .opts(
348
+ width=size,
349
+ height=size,
350
+ title="NoData (0) is black",
351
+ xlabel="VV (dB)",
352
+ ylabel="VH (dB)",
353
+ )
354
+ * labels
355
+ )
356
+
357
+
358
+ @app.cell(hide_code=True)
359
+ def _(mo):
360
+ mo.md(r"""
361
+ ### Test/demo
362
+ """)
363
+ return
364
+
365
+
366
+ @app.cell
367
+ def _():
368
+ make_legend()
369
+ return
370
+
371
+
372
+ @app.cell
373
+ def _():
374
+ _n = dpolcat.NUM_CATEGORIES
375
+ _color = get_hex_colors(range(_n))
376
+ pd.DataFrame(
377
+ {
378
+ "category": range(_n),
379
+ "count": [10] * _n,
380
+ "color": _color,
381
+ }
382
+ ).hvplot.bar(x="category", y="count", color="color")
383
+ return
384
+
385
+
386
+ @app.cell(hide_code=True)
387
+ def _(mo):
388
+ mo.md(r"""
389
+ ### QGIS Colour Map
390
+
391
+ Create a QGIS Color Map file that can be used for dpolcat layer symbology.
392
+ """)
393
+ return
394
+
395
+
396
+ @app.function
397
+ def write_qgis_colormap():
398
+ lines = []
399
+
400
+ for i, c in enumerate(dpolcat.color_list):
401
+ label = i
402
+ a = 255
403
+ r, g, b = c
404
+ lines.append(f"{i} {r} {g} {b} {a} {label}\n")
405
+
406
+ f = open("dpolcat-qgis-cmap.clr", "w")
407
+ f.writelines(lines)
408
+ f.close()
409
+
410
+
411
+ if __name__ == "__main__":
412
+ app.run()
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.5
2
+ Name: dpolcat
3
+ Version: 0.2.0.dev1
4
+ Summary: Semantic categorization of dual-polarimetric synthetic aperture radar (SAR) imagery.
5
+ Project-URL: Homepage, https://github.com/lukesdm/dpolcat
6
+ Project-URL: Issues, https://github.com/lukesdm/dpolcat/issues
7
+ Author-email: Luke McQuade <2824165+lukesdm@users.noreply.github.com>
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: geopandas<2,>=1.2.0
12
+ Requires-Dist: marimo<0.26,>=0.25.1
13
+ Requires-Dist: numba<0.69,>=0.68.0
14
+ Requires-Dist: odc-stac<0.6,>=0.5.3
15
+ Requires-Dist: pandas<4,>=3.0.6
16
+ Requires-Dist: planetary-computer<2,>=1.0.0
17
+ Requires-Dist: pystac-client<0.10,>=0.9.0
18
+ Requires-Dist: rioxarray<0.24,>=0.23.0
19
+ Requires-Dist: xarray<2027,>=2026.9.0
20
+ Provides-Extra: gdal
21
+ Requires-Dist: gdal<4,>=3.13.3; extra == 'gdal'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # dpolcat
25
+
26
+ *dpolcat* is a tool for the semantic categorization of dual-polarimetric synthetic aperture radar (SAR) imagery. (Specifically, Sentinel-1 with VV and VH polarizations at present.)
27
+
28
+ It is inspired by Dr. Andrea Baraldi's [Satellite Image Automatic Mapper™](http://siam.andreabaraldi.com/) (SIAM™) concept of *spectral categories* for optical data, the [Sen2Cube](https://www.sen2cube.at/) project, and Luuk van Der Meer's [Semantique](https://github.com/zgis/semantique).
29
+
30
+ Status: This is a research project with occasional updates.
31
+
32
+ ## Overview
33
+
34
+ Different objects on the earth's surface scatter the microwave signals that radar satellites emit and receive, in different ways.
35
+
36
+ ![Scatterer types](media/scatterer-types.png)
37
+
38
+ We bin the backscatter received in the same- (VV) and cross-polarized (VH) signals into distinct *polarimetric categories* that give a hint at what's on the ground.
39
+
40
+ ![Our polarimetric categories](media/categories.png)
41
+
42
+ TODO: 👆 update this (new categories).
43
+
44
+ We can then make inferences based on how these categories are distributed in an area, or change over time.
45
+
46
+
47
+ ![Demo panels, flood mapping](media/demo-panels.png)
48
+
49
+ TODO: 👆 update this (colour scheme, result output).
50
+
51
+ [*]: [Copernicus EMSR517](https://emergency.copernicus.eu/mapping/ems-product-component/EMSR517_AOI06_DEL_MONIT01_r1_RTP03/1)
52
+
53
+
54
+ This is implemented in a Python library that performs scaling, speckle-filtering, and then categorization, using a decision-tree algorithm.
55
+
56
+ ![Block diagram](media/block-diagram.png).
57
+
58
+ TODO: 👆 scaler should be linear to dB; add speckle filter.
59
+
60
+ It uses the [Numba](https://numba.pydata.org/) just-in-time compiler to increase performance, and [Dask](https://www.dask.org) for parallelization.
61
+
62
+ ### Colour palette
63
+
64
+ A note on the colour palette. The choices **loosely** follow these rules:
65
+ - Natural phenomena map to pale colours.
66
+ - Man-made phenonema map to bright colours.
67
+
68
+ Sometimes the colours resemble certain land cover types. This may not be coincidence. BUT, there's no one-to-one mapping of land cover type to category/colour.
69
+
70
+
71
+ ### Category numbering
72
+
73
+ The numbering of the categories, i.e., their indices, has a property with an interesting side effect: they loosely increase in order of magnitude of the VV+VH vector they represent. This means modelling and visualization can sometimes use the category index alone as an input. E.g.,
74
+
75
+ TODO: B/W & viridis map of Graz + caption.
76
+
77
+ ## Environment
78
+
79
+ Launch marimo notebooks via either:
80
+ - Pixi - run `pixi run marimo edit` (a browser opens automatically).
81
+ - Docker - use locally with, e.g., `docker compose up` and browse to localhost:8080. ⚠️ This opens a notebook server without authentication, make sure to be behind a properly configured firewall.
82
+
83
+
84
+ ## Contents
85
+
86
+ '🟢' indicates the file can be run as a marimo notebook.
87
+
88
+ #### 📄 `dpolcat.py`
89
+
90
+ The *dpolcat* module, containing the main algorithms/functions for performing polarimetric categorization.
91
+
92
+
93
+ #### 📄🟢 `dpolcat.processor.py`
94
+
95
+ A parameterized notebook containing a batch processing workflow.
96
+
97
+
98
+ #### 📄 `dpolcat_modelling.py`
99
+
100
+ Helpers for creating semantic models with categories.
101
+
102
+
103
+ #### 📄🟢 `dpolcat.utils.py`
104
+
105
+ Contains reusable utility/helper functions etc. for working with *dpolcat* in various settings.
106
+
107
+
108
+ #### 📄🟢 `demo_01_intro.py`
109
+
110
+ An introduction to *dpolcat*.
111
+
112
+
113
+ #### 📄🟢 `demo_02_flood.py`
114
+
115
+ Demonstration of a simple flood modelling and mapping workflow using *dpolcat*.
116
+
117
+
118
+ #### 📄🟢 `demo_03_batch_processing.py`
119
+
120
+ A batch processing/datacube-writing demonstration.
121
+
122
+
123
+ #### 📄🟢 `demo_04_urban.py`
124
+
125
+ Demonstration of some simple urban change modelling and mapping.
126
+
127
+
128
+ #### 📄🟢 `gis/inverness_*.py`
129
+
130
+ Exploratory analysis of SAR signals and dpolcat processing in a study area of Inverness, Scotland.
131
+
132
+
133
+ #### 📄 `pc_rio_driver.py`
134
+
135
+ A utility class for [odc-stac](https://odc-stac.readthedocs.io/en/latest/intro.html) which takes care of Planetary Computer asset signing.
136
+
137
+
138
+ #### 📄 `fast_frost.py`
139
+
140
+ A numba-based implementation of a Frost speckle filter.
141
+
142
+
143
+ #### 📁 `gis/duisburg`
144
+
145
+ Supplementary folder for the demo notebook's flood mapping example, containing a QGIS project and associated data for accuracy assessment.
146
+
147
+
148
+ #### 📁 `gis/graz`
149
+
150
+ Supplementary folder inc. QGIS project for the Graz-related demos.
151
+
152
+
153
+ #### 📁 `archive`
154
+
155
+ Some things based on earlier versions, which may be useful for additional context.
156
+
157
+
158
+
159
+
160
+
161
+
162
+
163
+ #### 📄 `media/EARSeL2024-Slides.pdf`
164
+
165
+ Slides presented at the 43rd EARSeL Symposium in Manchester, UK, 17-June-2024. (Based on an older version.)
166
+
167
+
168
+ #### 📄 `media/poster.pdf`
169
+
170
+ A simple poster about the project presented internally (based on an even older version).
171
+
172
+
173
+ ## Credits
174
+
175
+ Imagery: Contains modified Copernicus Sentinel data, processed by ESA and others.
176
+
177
+ Flood reference: [Copernicus Emergency Mapping EMSR517](https://emergency.copernicus.eu/mapping/ems-product-component/EMSR517_AOI06_DEL_MONIT01_r1_RTP03/1)
178
+
179
+ Graz aerial imagery: TODO: add source.
180
+
181
+ ## Authors
182
+
183
+ Created by Luke McQuade at [Z_GIS](https://www.plus.ac.at/geoinformatik/?lang=en), originally as part of the the Applied Geoinformatics MSc programme.
184
+
185
+ ## Acknowledgements
186
+
187
+ We would like to greatly thank the members of the [EO Analytics Group](https://www.plus.ac.at/geoinformatik/research/research-areas/eo-analytics/?lang=en) and Dr. Zhara Dabiri of the [Risk, Hazard and Climate Lab](https://www.plus.ac.at/geoinformatik/research/research-areas/risk-hazard-climate/?lang=en) for their ideas and support, and Assoc. Prof. Hermann Klug and Dr. Bernhard Zagel for their instruction and continuous feedback.
188
+
189
+ Further developments have been part of my masters thesis, supervised by Prof. Dr. Dirk Tiede, Dr. Martin Sudmanns, and Dr. Zhara Dabiri.
190
+
191
+
192
+ ## References
193
+
194
+ Augustin, H., Sudmanns, M., Tiede, D., Lang, S., & Baraldi, A. (2019). Semantic Earth observation data cubes. Data, 4(3), 102.
195
+ DOI: 10.3390/data4030102
196
+
197
+ Baraldi, A., Humber, M.L., Tiede, D., Lang, S. (2018). GEO-CEOS stage 4 validation of the Satellite Image Automatic Mapper lightweight computer program for ESA Earth observation level 2 product generation – Part 2: Validation. Cogent Geosci. 4, 1–52.
198
+ DOI: 10.1080/23312041.2018.1467254
199
+
200
+
201
+ Meyer, F. (2019), Ch. 2, The SAR Handbook: Comprehensive Methodologies for Forest Monitoring and Biomass Estimation.
202
+ DOI: 10.25966/nr2c-s697
@@ -0,0 +1,11 @@
1
+ dpolcat/__init__.py,sha256=3JcOfymPgsGgWUAvwMAroqk1JdKGC99cnE77qqYQCfQ,92
2
+ dpolcat/core.py,sha256=NzNtjK28KYITnIyFEFRpt37lRrkfsYD1ZDFlLtt6Gm4,4059
3
+ dpolcat/fast_frost.py,sha256=tIzWu7w-Fv95DV-nFQU8oQoorepk44HKNyYTyte3bVw,6441
4
+ dpolcat/modelling.py,sha256=A-yhtbhIL8sRx-nBRvbTU6xW52J8FCmhTEUMIlz29GM,3853
5
+ dpolcat/pc_rio_driver.py,sha256=o8XdBI-q4HU8OX6fFpe90B45NgH8rKhlgkKmyA_a-EM,1021
6
+ dpolcat/processor.py,sha256=RIQ-beRJQcfdZZJcbgPTRjLYuVY-m4zWpJL9GxV3Kww,22119
7
+ dpolcat/utils.py,sha256=U6JwCAjHqtZUI4Qw9ijFBlwMxyceRXBwcAK2XMroDMQ,11244
8
+ dpolcat-0.2.0.dev1.dist-info/METADATA,sha256=6yu5q1fnND-1ZdUjC2yLgi-5Lr4AejZjqeNvUK6xiwE,7239
9
+ dpolcat-0.2.0.dev1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ dpolcat-0.2.0.dev1.dist-info/licenses/LICENSE,sha256=fkPhb5zI5mg5fHiiRNNb-HVxPHsFF0tsk9a_eFtMm2o,11541
11
+ dpolcat-0.2.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any