b3alien 0.0.1__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.
- b3alien/__init__.py +0 -0
- b3alien/b3cube/__init__.py +23 -0
- b3alien/b3cube/b3cube.py +511 -0
- b3alien/griis/__init__.py +14 -0
- b3alien/griis/griis.py +198 -0
- b3alien/simulation/__init__.py +11 -0
- b3alien/simulation/simulation.py +126 -0
- b3alien/utils.py +73 -0
- b3alien/visualisation/__init__.py +13 -0
- b3alien/visualisation/b3gee.py +35 -0
- b3alien/visualisation/visualisation.py +68 -0
- b3alien-0.0.1.dist-info/METADATA +225 -0
- b3alien-0.0.1.dist-info/RECORD +16 -0
- b3alien-0.0.1.dist-info/WHEEL +5 -0
- b3alien-0.0.1.dist-info/licenses/LICENSE +21 -0
- b3alien-0.0.1.dist-info/top_level.txt +1 -0
b3alien/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Biodiversity data cube functions
|
|
4
|
+
========================================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .b3cube import OccurrenceCube
|
|
9
|
+
from .b3cube import plot_richness
|
|
10
|
+
from .b3cube import cumulative_species
|
|
11
|
+
from .b3cube import calculate_rate
|
|
12
|
+
from .b3cube import get_survey_effort
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"plot_richness",
|
|
16
|
+
"cumulative_species",
|
|
17
|
+
"plot_cumsum",
|
|
18
|
+
"filter_multiple_cells",
|
|
19
|
+
"filter_multiple_occ",
|
|
20
|
+
"calculate_rate",
|
|
21
|
+
"get_survey_effort"
|
|
22
|
+
# purposely exclude OccurrenceCube
|
|
23
|
+
]
|
b3alien/b3cube/b3cube.py
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import geopandas as gpd
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import xarray as xr
|
|
4
|
+
import sparse
|
|
5
|
+
import dask.array as da
|
|
6
|
+
import numpy as np
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import matplotlib
|
|
9
|
+
matplotlib.use("Agg")
|
|
10
|
+
import shapely
|
|
11
|
+
from shapely import geos
|
|
12
|
+
import gcsfs
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
Create a class object of the occurrence cube
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
class OccurrenceCube():
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
Load a GeoParquet file (local or from GCS) into a sparse xarray cube.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
filepath : str
|
|
27
|
+
Path to the GeoParquet file (e.g. 'gs://bucket/file.parquet').
|
|
28
|
+
dims : list or tuple, optional
|
|
29
|
+
Dimension names. Default is ['time', 'cell', 'species'].
|
|
30
|
+
coords : dict, optional
|
|
31
|
+
Optional coordinates to assign to the cube.
|
|
32
|
+
index_col : str or list, optional
|
|
33
|
+
Column(s) to use for reshaping if needed.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
b3cube.OccurrenceCube
|
|
39
|
+
A sparse data cube loaded from the GeoParquet file.
|
|
40
|
+
self.df contains a geopandas.DataFrame
|
|
41
|
+
self.data a sparse xarray.Xarray
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, filepath: str, dims=None, coords=None, index_col=None):
|
|
45
|
+
|
|
46
|
+
self.filepath = filepath
|
|
47
|
+
self.dims = dims or ("time", "cell", "species")
|
|
48
|
+
self.coords = coords
|
|
49
|
+
self.index_col = index_col
|
|
50
|
+
|
|
51
|
+
# Load GeoParquet
|
|
52
|
+
self.df = self._load_geoparquet(filepath)
|
|
53
|
+
|
|
54
|
+
# Create cube
|
|
55
|
+
self.data = self._create_xcube(self.df)
|
|
56
|
+
|
|
57
|
+
def _load_geoparquet(self, path):
|
|
58
|
+
"""
|
|
59
|
+
Load a GeoParquet file from local disk or GCS using GeoPandas.
|
|
60
|
+
"""
|
|
61
|
+
if path.startswith("gs://"):
|
|
62
|
+
fs = gcsfs.GCSFileSystem()
|
|
63
|
+
with fs.open(path) as f:
|
|
64
|
+
gdf = gpd.read_parquet(f)
|
|
65
|
+
else:
|
|
66
|
+
gdf = gpd.read_parquet(path)
|
|
67
|
+
|
|
68
|
+
if 'geometry' not in gdf.columns:
|
|
69
|
+
raise ValueError("The input file must contain a 'geometry' column.")
|
|
70
|
+
|
|
71
|
+
# Ensure geometry is parsed and valid
|
|
72
|
+
gdf["geometry"] = gdf["geometry"].apply(wkt.loads) if gdf["geometry"].dtype == object else gdf["geometry"]
|
|
73
|
+
gdf = gdf.set_geometry("geometry")
|
|
74
|
+
|
|
75
|
+
return gdf
|
|
76
|
+
|
|
77
|
+
def _create_xcube(self, df):
|
|
78
|
+
"""
|
|
79
|
+
Convert a GeoDataFrame into a sparse xarray cube with geometry metadata.
|
|
80
|
+
"""
|
|
81
|
+
# Convert to categorical
|
|
82
|
+
df["yearmonth"] = pd.Categorical(df["yearmonth"])
|
|
83
|
+
df["cellCode"] = pd.Categorical(df["cellCode"])
|
|
84
|
+
df["specieskey"] = pd.Categorical(df["specieskey"])
|
|
85
|
+
|
|
86
|
+
# Align geometries with cell categories
|
|
87
|
+
cell_categories = df["cellCode"].cat.categories
|
|
88
|
+
geometry_per_cell = df.drop_duplicates("cellCode").set_index("cellCode").loc[cell_categories]["geometry"]
|
|
89
|
+
|
|
90
|
+
# Encode to integers
|
|
91
|
+
time_codes = df["yearmonth"].cat.codes.values
|
|
92
|
+
cell_codes = df["cellCode"].cat.codes.values
|
|
93
|
+
species_codes = df["specieskey"].cat.codes.values
|
|
94
|
+
|
|
95
|
+
# Build sparse cube
|
|
96
|
+
sparse_cube = sparse.COO(
|
|
97
|
+
coords=[time_codes, cell_codes, species_codes],
|
|
98
|
+
data=df["occurrences"].astype("float32").values,
|
|
99
|
+
shape=(
|
|
100
|
+
df["yearmonth"].cat.categories.size,
|
|
101
|
+
df["cellCode"].cat.categories.size,
|
|
102
|
+
df["specieskey"].cat.categories.size
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Create xarray DataArray
|
|
107
|
+
cube = xr.DataArray(
|
|
108
|
+
sparse_cube,
|
|
109
|
+
dims=self.dims,
|
|
110
|
+
coords={
|
|
111
|
+
self.dims[0]: df["yearmonth"].cat.categories,
|
|
112
|
+
self.dims[1]: df["cellCode"].cat.categories,
|
|
113
|
+
self.dims[2]: df["specieskey"].cat.categories,
|
|
114
|
+
"geometry": (self.dims[1], geometry_per_cell.values)
|
|
115
|
+
},
|
|
116
|
+
name="occurrences"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return cube
|
|
120
|
+
|
|
121
|
+
def _species_richness(self, normalized=False):
|
|
122
|
+
# 1. Binary presence
|
|
123
|
+
presence = (self.data > 0)
|
|
124
|
+
|
|
125
|
+
# 2. Collapse time dimension using logical OR → was the species *ever* seen in this cell?
|
|
126
|
+
presence_any_time = presence.any(dim="time") # shape: (cell, species)
|
|
127
|
+
|
|
128
|
+
# 3. Sum species per cell (species richness)
|
|
129
|
+
species_richness = presence_any_time.sum(dim="species") # shape: (cell,)
|
|
130
|
+
|
|
131
|
+
total_occurrences = self.data.sum(dim=["time", "species"])
|
|
132
|
+
|
|
133
|
+
if normalized == False:
|
|
134
|
+
# 4. Get the non-zero values and indices
|
|
135
|
+
coords = species_richness.data.coords # (1D arrays of indices)
|
|
136
|
+
values = species_richness.data.data # the richness values
|
|
137
|
+
|
|
138
|
+
# 5. Convert integer cell indices to real labels (from .coords['cell'])
|
|
139
|
+
cell_labels = species_richness.coords["cell"].values
|
|
140
|
+
|
|
141
|
+
richness_df = pd.DataFrame({
|
|
142
|
+
"cell": cell_labels[coords[0]],
|
|
143
|
+
"richness": values
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
self.richness = richness_df
|
|
147
|
+
|
|
148
|
+
else:
|
|
149
|
+
epsilon = 1e-6
|
|
150
|
+
normalized_richness = species_richness / (total_occurrences + epsilon)
|
|
151
|
+
|
|
152
|
+
coords = normalized_richness.data.coords
|
|
153
|
+
values = normalized_richness.data.data
|
|
154
|
+
cell_labels = normalized_richness.coords["cell"].values
|
|
155
|
+
|
|
156
|
+
# Build a DataFrame
|
|
157
|
+
norm_df = pd.DataFrame({
|
|
158
|
+
"cell": cell_labels[coords[0]],
|
|
159
|
+
"normalized_richness": values
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
self.richness = norm_df
|
|
163
|
+
|
|
164
|
+
def _filter_species(self, speciesKey):
|
|
165
|
+
|
|
166
|
+
self.df = self.df[self.df['specieskey'].eq(speciesKey)]
|
|
167
|
+
self.data = self.data.sel(species=speciesKey)
|
|
168
|
+
|
|
169
|
+
def plot_richness(richness_df, gdf_from_gcs, geom='cellCode'):
|
|
170
|
+
"""
|
|
171
|
+
Create a plot of the species richness dataframe.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
richness_df : pandas.DataFrame
|
|
176
|
+
Datagrame containing the species richness per grid cell.
|
|
177
|
+
gdf_from_gcs : geopandas.Dataframe
|
|
178
|
+
GeoDataFrame containing the species occurrence cuve.
|
|
179
|
+
geom : str, optional
|
|
180
|
+
Name of the geometry column in the GeoDataFrame. Default is 'cellCode'
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
matplotlib.plot
|
|
185
|
+
A plot of the species richness.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
gdf_plot = pd.merge(richness_df, gdf_from_gcs, left_on='cell', right_on=geom)
|
|
189
|
+
|
|
190
|
+
gdf_plot = gpd.GeoDataFrame(gdf_plot, geometry="geometry", crs=gdf_from_gcs.crs)
|
|
191
|
+
|
|
192
|
+
fig, ax = plt.subplots(figsize=(10, 10))
|
|
193
|
+
gdf_plot.plot(
|
|
194
|
+
column="richness",
|
|
195
|
+
cmap="viridis",
|
|
196
|
+
legend=True,
|
|
197
|
+
linewidth=0.1,
|
|
198
|
+
edgecolor="grey",
|
|
199
|
+
ax=ax
|
|
200
|
+
)
|
|
201
|
+
ax.set_title("Species Richness per QDGC Cell")
|
|
202
|
+
ax.axis("off")
|
|
203
|
+
plt.show()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def cumulative_species(cube, species_to_keep):
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
Calculate the cumulative number of species in a OccurrenceCube.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
cube : b3alien.b3cube.OccurrenceCube
|
|
214
|
+
Species OccurrenceCube from GBIF.
|
|
215
|
+
species_to_keep : numpy.array
|
|
216
|
+
Array of GBIF speciesKeys that need to be taken into account to calculate the cumulative species number of a cube.
|
|
217
|
+
geom : str, optional
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
df1 : pandas.DataFrame
|
|
222
|
+
Sparse dataframe that still contains the cumulative sum per grid cell.
|
|
223
|
+
df2 : pandas.DataFrame
|
|
224
|
+
Cumulative dataframe cell independent.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
# Wrap sparse array in Dask array with one or more chunks
|
|
228
|
+
dask_sparse_array = da.from_array(cube.data.data, chunks=(100, 100, 1000)) # tune chunking for your use case
|
|
229
|
+
|
|
230
|
+
# Replace data in cube
|
|
231
|
+
cube_dask_sparse = cube.data.copy(data=dask_sparse_array)
|
|
232
|
+
|
|
233
|
+
species_mask = cube_dask_sparse["species"].isin(species_to_keep)
|
|
234
|
+
filtered_cube = cube_dask_sparse.where(species_mask, drop=True)
|
|
235
|
+
|
|
236
|
+
# Grab the underlying sparse.COO object from Dask
|
|
237
|
+
sparse_block = filtered_cube.data.compute() # Warning: loads full filtered cube into RAM!
|
|
238
|
+
|
|
239
|
+
# Extract sparse coordinates
|
|
240
|
+
coords = sparse_block.coords # shape: (ndim, nnz)
|
|
241
|
+
data = sparse_block.data # non-zero values
|
|
242
|
+
|
|
243
|
+
# Map indices to labels
|
|
244
|
+
time_labels = filtered_cube.coords["time"].values
|
|
245
|
+
species_labels = filtered_cube.coords["species"].values
|
|
246
|
+
cell_labels = filtered_cube.coords["cell"].values
|
|
247
|
+
|
|
248
|
+
# Use the sparse indices to create a DataFrame
|
|
249
|
+
df_sparse = pd.DataFrame({
|
|
250
|
+
"time": time_labels[coords[0]],
|
|
251
|
+
"cell": cell_labels[coords[1]],
|
|
252
|
+
"species": species_labels[coords[2]],
|
|
253
|
+
"occurrences": sparse_block.data
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
# Drop duplicates and compute cumulative species count
|
|
257
|
+
df_sparse = df_sparse.drop_duplicates()
|
|
258
|
+
df_sparse["seen"] = 1
|
|
259
|
+
df_time = (
|
|
260
|
+
df_sparse.groupby("time")["species"]
|
|
261
|
+
.nunique()
|
|
262
|
+
.cumsum()
|
|
263
|
+
.reset_index(name="cumulative_species")
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
df_time["time"] = pd.to_datetime(df_time["time"], format="%Y-%M", errors="coerce")
|
|
267
|
+
|
|
268
|
+
# fix to have the real cumsum
|
|
269
|
+
# Step 1: Remove duplicates (species × time)
|
|
270
|
+
df_sparse_unique = df_sparse[["time", "species"]].drop_duplicates()
|
|
271
|
+
|
|
272
|
+
# Step 2: Sort by time
|
|
273
|
+
df_sparse_unique = df_sparse_unique.sort_values("time")
|
|
274
|
+
|
|
275
|
+
# Step 3: Track cumulative species using a set
|
|
276
|
+
seen_species = set()
|
|
277
|
+
cumulative = []
|
|
278
|
+
|
|
279
|
+
for time, group in df_sparse_unique.groupby("time"):
|
|
280
|
+
new_species = set(group["species"])
|
|
281
|
+
seen_species.update(new_species)
|
|
282
|
+
cumulative.append((time, len(seen_species)))
|
|
283
|
+
|
|
284
|
+
# Step 4: Create cumulative DataFrame
|
|
285
|
+
df_cumulative = pd.DataFrame(cumulative, columns=["time", "cumulative_species"])
|
|
286
|
+
df_cumulative["time"] = pd.to_datetime(df_cumulative["time"], format="%Y-%M", errors="coerce")
|
|
287
|
+
|
|
288
|
+
return df_sparse, df_cumulative
|
|
289
|
+
|
|
290
|
+
def plot_cumsum(df_cumulative):
|
|
291
|
+
"""
|
|
292
|
+
Create a plot of the cumulative number of species.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
df_cumulative : pandas.DataFrame
|
|
297
|
+
Datagrame containing the cumulative number over tume.
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
matplotlib.plot
|
|
302
|
+
A plot of the cumulative number of species.
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
plt.figure(figsize=(10, 5))
|
|
306
|
+
plt.plot(df_cumulative["time"], df_cumulative["cumulative_species"], marker="o")
|
|
307
|
+
plt.title("Cumulative Unique Species Over Time")
|
|
308
|
+
plt.xlabel("Time")
|
|
309
|
+
plt.ylabel("Unique Species Observed")
|
|
310
|
+
plt.xticks(rotation=45)
|
|
311
|
+
plt.grid(True)
|
|
312
|
+
plt.tight_layout()
|
|
313
|
+
plt.show()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def filter_multiple_cells(df_sparse):
|
|
317
|
+
"""
|
|
318
|
+
Only count a species established when it is present in more than one cell.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
df_sparse : pandas.DataFrame
|
|
323
|
+
Datagrame containing the species richness per grid cell.
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
pandas.DataFrame
|
|
328
|
+
Cumulative species when in multiple cells.
|
|
329
|
+
"""
|
|
330
|
+
# Ensure cell is in your DataFrame
|
|
331
|
+
assert "cell" in df_sparse.columns
|
|
332
|
+
|
|
333
|
+
# 1. Count unique cells per (time, species)
|
|
334
|
+
species_cell_counts = (
|
|
335
|
+
df_sparse.groupby(["time", "species"])["cell"]
|
|
336
|
+
.nunique()
|
|
337
|
+
.reset_index(name="cell_count")
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# 2. Keep only species seen in at least 2 cells
|
|
341
|
+
species_multi_cell = species_cell_counts[species_cell_counts["cell_count"] >= 2]
|
|
342
|
+
|
|
343
|
+
# 3. Track cumulative set of species across time
|
|
344
|
+
species_multi_cell = species_multi_cell.sort_values("time")
|
|
345
|
+
|
|
346
|
+
seen_species = set()
|
|
347
|
+
cumulative = []
|
|
348
|
+
|
|
349
|
+
for time, group in species_multi_cell.groupby("time"):
|
|
350
|
+
new_species = set(group["species"])
|
|
351
|
+
seen_species.update(new_species)
|
|
352
|
+
cumulative.append((time, len(seen_species)))
|
|
353
|
+
|
|
354
|
+
df_cumulative_cells = pd.DataFrame(cumulative, columns=["time", "cumulative_species_2cells"])
|
|
355
|
+
|
|
356
|
+
return df_cumulative_cells
|
|
357
|
+
|
|
358
|
+
def filter_multiple_occ(df_sparse):
|
|
359
|
+
"""
|
|
360
|
+
Only count a species established when there are multiple occurrences in a cell.
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
df_sparse : pandas.DataFrame
|
|
365
|
+
Datagrame containing the species richness per grid cell.
|
|
366
|
+
|
|
367
|
+
Returns
|
|
368
|
+
-------
|
|
369
|
+
pandas.DataFrame
|
|
370
|
+
Cumulative species when multiple occurrences in a cell.
|
|
371
|
+
"""
|
|
372
|
+
# Ensure 'occurrences' and 'cell' are present
|
|
373
|
+
assert "occurrences" in df_sparse.columns and "cell" in df_sparse.columns
|
|
374
|
+
|
|
375
|
+
# 1. Total occurrences per (time, species, cell)
|
|
376
|
+
species_cell_occ = (
|
|
377
|
+
df_sparse.groupby(["time", "species", "cell"])["occurrences"]
|
|
378
|
+
.sum()
|
|
379
|
+
.reset_index()
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# 2. Filter for species that had ≥ 2 occurrences in any cell
|
|
383
|
+
species_with_2occ = (
|
|
384
|
+
species_cell_occ[species_cell_occ["occurrences"] >= 2]
|
|
385
|
+
.drop_duplicates(subset=["time", "species"])
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# 3. Cumulative species count logic
|
|
389
|
+
species_with_2occ = species_with_2occ.sort_values("time")
|
|
390
|
+
|
|
391
|
+
seen_species = set()
|
|
392
|
+
cumulative = []
|
|
393
|
+
|
|
394
|
+
for time, group in species_with_2occ.groupby("time"):
|
|
395
|
+
new_species = set(group["species"])
|
|
396
|
+
seen_species.update(new_species)
|
|
397
|
+
cumulative.append((time, len(seen_species)))
|
|
398
|
+
|
|
399
|
+
df_cumulative_occ = pd.DataFrame(cumulative, columns=["time", "cumulative_species_2occ"])
|
|
400
|
+
|
|
401
|
+
return df_cumulative_occ
|
|
402
|
+
|
|
403
|
+
def calculate_rate(df_cumulative):
|
|
404
|
+
"""
|
|
405
|
+
Calculate the rate of establishment from the cumulative distribution.
|
|
406
|
+
|
|
407
|
+
Parameters
|
|
408
|
+
----------
|
|
409
|
+
df_cumulative : pandas.DataFrame
|
|
410
|
+
Datagrame containing the cumulative distribution.
|
|
411
|
+
|
|
412
|
+
Returns
|
|
413
|
+
-------
|
|
414
|
+
s1 : pandas.Series
|
|
415
|
+
Series of the time axis.
|
|
416
|
+
s2 : pandas.Series
|
|
417
|
+
Series of the rate of establishment.
|
|
418
|
+
"""
|
|
419
|
+
# --- Processing GBIF data (Monthly) to get an approximate annual rate ---
|
|
420
|
+
df_cumulative["time"] = pd.to_datetime(df_cumulative["time"])
|
|
421
|
+
df_cumulative_rate = df_cumulative.sort_values(by="time").copy()
|
|
422
|
+
|
|
423
|
+
# Group data by year and calculate the total species difference for each year
|
|
424
|
+
annual_data = df_cumulative_rate.groupby(df_cumulative_rate['time'].dt.year).agg(
|
|
425
|
+
cumulative_species=('cumulative_species', 'last'), # Get the last cumulative species value for the year
|
|
426
|
+
first_time=('time', 'first') # Get the first timestamp for the year
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Calculate annual rate using the grouped data
|
|
430
|
+
annual_rate_gbif = []
|
|
431
|
+
annual_time_gbif = []
|
|
432
|
+
|
|
433
|
+
for i in range(1, len(annual_data)):
|
|
434
|
+
current_year_data = annual_data.iloc[i]
|
|
435
|
+
previous_year_data = annual_data.iloc[i - 1]
|
|
436
|
+
|
|
437
|
+
species_diff = current_year_data['cumulative_species'] - previous_year_data['cumulative_species']
|
|
438
|
+
time_diff_years = (current_year_data['first_time'] - previous_year_data['first_time']).days / 365.25
|
|
439
|
+
|
|
440
|
+
annual_rate = species_diff / time_diff_years
|
|
441
|
+
annual_rate_gbif.append(annual_rate)
|
|
442
|
+
annual_time_gbif.append(current_year_data.name) # Year is the index after groupby
|
|
443
|
+
|
|
444
|
+
annual_time_gbif = [int(year) for year in annual_time_gbif]
|
|
445
|
+
|
|
446
|
+
# Convert the lists to Pandas Series for easier plotting
|
|
447
|
+
annual_rate_gbif_series = pd.Series(annual_rate_gbif)
|
|
448
|
+
annual_time_gbif_series = pd.Series(annual_time_gbif)
|
|
449
|
+
|
|
450
|
+
return annual_time_gbif, annual_rate_gbif
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def get_survey_effort(cube, dateFormat='%Y-%m', calc_type='total'):
|
|
454
|
+
"""
|
|
455
|
+
Estimate the survey effort in an OccurrenceCube.
|
|
456
|
+
|
|
457
|
+
Parameters
|
|
458
|
+
----------
|
|
459
|
+
cube : b3alien.b3cube.OccurrenceCube
|
|
460
|
+
Species OccurrenceCube from GBIF.
|
|
461
|
+
dateFormat : str, optional
|
|
462
|
+
Dateformat stored in the OccurrenceCube. Default is '%Y-%m'
|
|
463
|
+
calc_type : str, optional
|
|
464
|
+
Type of survey effort to be calculated.
|
|
465
|
+
'distinct' : total number of distinct observers
|
|
466
|
+
'total' : total number of occurrences
|
|
467
|
+
Default is total.
|
|
468
|
+
|
|
469
|
+
Returns
|
|
470
|
+
-------
|
|
471
|
+
df : pandas.DataFrame
|
|
472
|
+
Dataframe containing time and the chosen measurement for survey effort.
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
if calc_type == 'distinct':
|
|
477
|
+
# Group by 'yearmonth' and sum
|
|
478
|
+
distinct_observers_over_time = cube.df.groupby('yearmonth', observed=True)['distinctobservers'].sum()
|
|
479
|
+
|
|
480
|
+
# Convert the index to datetime (if it's not already)
|
|
481
|
+
distinct_observers_over_time.index = pd.to_datetime(
|
|
482
|
+
distinct_observers_over_time.index, format=dateFormat, errors='coerce'
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Resample to yearly frequency using new 'YE' standard
|
|
486
|
+
distinct_observers_yearly = distinct_observers_over_time.resample('YE').sum()
|
|
487
|
+
|
|
488
|
+
# Filter for years from 1900 onward
|
|
489
|
+
distinct_observers_filtered = distinct_observers_yearly[distinct_observers_yearly.index.year >= 1900]
|
|
490
|
+
|
|
491
|
+
# Convert to DataFrame
|
|
492
|
+
df = distinct_observers_filtered.reset_index()
|
|
493
|
+
df.columns = ['date', 'distinct_observers'] # Rename columns for clarity
|
|
494
|
+
|
|
495
|
+
return df
|
|
496
|
+
|
|
497
|
+
else:
|
|
498
|
+
|
|
499
|
+
total_occurrences_over_time = cube.data.sum(dim=['cell', 'species'])
|
|
500
|
+
|
|
501
|
+
# Convert the time coordinates to datetime objects if they aren't already
|
|
502
|
+
total_occurrences_over_time['time'] = pd.to_datetime(total_occurrences_over_time['time'].values, format=dateFormat, errors='coerce')
|
|
503
|
+
df = pd.DataFrame({
|
|
504
|
+
'time': total_occurrences_over_time['time'].values,
|
|
505
|
+
'total_occurrences': total_occurrences_over_time.data.data
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
# (Optional) Drop rows with invalid or missing time
|
|
509
|
+
df = df.dropna(subset=['time'])
|
|
510
|
+
|
|
511
|
+
return df
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
GRIIS checklist functions
|
|
4
|
+
=========================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .griis import CheckList
|
|
9
|
+
from .griis import get_speciesKey
|
|
10
|
+
from .griis import do_taxon_matching
|
|
11
|
+
from .griis import read_checklist
|
|
12
|
+
from .griis import split_event_date
|
|
13
|
+
|
|
14
|
+
__all__ = ["get_speciesKey", "do_taxon_matching", "read_checklist", "split_event_date"]
|
b3alien/griis/griis.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from tqdm import tqdm
|
|
3
|
+
tqdm.pandas() # enables .progress_apply
|
|
4
|
+
from pygbif import species
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
class CheckList():
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
Load a GRIIS checklist from GBIF.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
filepath : str
|
|
15
|
+
Path to the distribution.txt file of the checklist.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
griis.Checklist
|
|
20
|
+
A checklist object containing the list of species.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, filePath: str):
|
|
24
|
+
self.filePath = filePath
|
|
25
|
+
|
|
26
|
+
# Create cube
|
|
27
|
+
self.species = self._load_GRIIS(filePath)
|
|
28
|
+
|
|
29
|
+
def _load_GRIIS(self, filePath):
|
|
30
|
+
|
|
31
|
+
df_merged = pd.read_csv(filePath, sep="\t")
|
|
32
|
+
species_to_keep = df_merged["speciesKey"].unique()
|
|
33
|
+
species_to_keep = np.where(species_to_keep == 'Uncertain', -1, species_to_keep)
|
|
34
|
+
species_to_keep = species_to_keep.astype(int)
|
|
35
|
+
|
|
36
|
+
return species_to_keep
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_speciesKey(sciname):
|
|
40
|
+
"""
|
|
41
|
+
Match text strings with the GBIF taxonomic backbone.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
sciname : str
|
|
46
|
+
Text string of a scientific name
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
speciesKey: an integer speciesKey number
|
|
51
|
+
"""
|
|
52
|
+
result = species.name_backbone(sciname, strict=True)
|
|
53
|
+
try:
|
|
54
|
+
speciesKey = result["speciesKey"]
|
|
55
|
+
except KeyError:
|
|
56
|
+
speciesKey = "Uncertain"
|
|
57
|
+
return speciesKey
|
|
58
|
+
|
|
59
|
+
def split_event_date(eventDate):
|
|
60
|
+
"""
|
|
61
|
+
Interprete the event date as introduction date and date of last seen,
|
|
62
|
+
when this information is available in the checklist.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
eventDate : str
|
|
67
|
+
Text string of eventDate
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
pd.Series
|
|
72
|
+
A series containing introduction date ('intro') and date last seen ('outro')
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(eventDate, str):
|
|
75
|
+
parts = eventDate.strip().split('/')
|
|
76
|
+
if len(parts) == 2:
|
|
77
|
+
intro = parts[0]
|
|
78
|
+
outro = parts[1]
|
|
79
|
+
else:
|
|
80
|
+
intro = outro = np.nan
|
|
81
|
+
return pd.Series([intro, outro])
|
|
82
|
+
else:
|
|
83
|
+
return pd.Series([np.nan, np.nan])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def do_taxon_matching(dirPath):
|
|
87
|
+
"""
|
|
88
|
+
Match keys between taxon.txt and distribution.txt
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
dirPath : str
|
|
93
|
+
Path to the directory of the checklist
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
Saves a new checklist file 'merged_distr.txt' in the checklist directory
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
taxon = dirPath + "taxon.txt"
|
|
101
|
+
distribution = dirPath + "distribution.txt"
|
|
102
|
+
|
|
103
|
+
df_t = pd.read_csv(taxon, sep="\t")
|
|
104
|
+
df_dist = pd.read_csv(distribution, sep="\t")
|
|
105
|
+
|
|
106
|
+
# Now apply this on the whole dataframe
|
|
107
|
+
|
|
108
|
+
df_t["speciesKey"] = df_t["scientificName"].progress_apply(get_speciesKey)
|
|
109
|
+
|
|
110
|
+
df_merged = df_dist.merge(df_t[['id', 'speciesKey']], on='id', how='left')
|
|
111
|
+
df_merged.to_csv(dirPath + 'merged_distr.txt', sep='\t', index=False)
|
|
112
|
+
|
|
113
|
+
# The rest assumes already a merged dataset
|
|
114
|
+
def read_checklist(filePath, cl_type='detailed', locality='Belgium'):
|
|
115
|
+
|
|
116
|
+
distribution = filePath + "distribution.txt"
|
|
117
|
+
|
|
118
|
+
df_cl = pd.read_csv(distribution, sep='\t', low_memory=False)
|
|
119
|
+
|
|
120
|
+
df_cl["speciesKey"] = df_cl["id"].str.rsplit("/", n=1).str[-1].astype("int64")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if cl_type == 'detailed':
|
|
124
|
+
|
|
125
|
+
species_to_keep = df_cl["speciesKey"].astype("int64").unique()
|
|
126
|
+
|
|
127
|
+
# 1. Filter rows where locality == 'Belgium' and eventDate is not missing
|
|
128
|
+
df = df_cl[df_cl["locality"] == locality].copy()
|
|
129
|
+
df = df[df["eventDate"].notna()]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
df[["introDate", "outroDate"]] = df["eventDate"].apply(split_event_date)
|
|
134
|
+
|
|
135
|
+
df["introDate"] = pd.to_datetime(df["introDate"], format="%Y", errors="coerce")
|
|
136
|
+
df["outroDate"] = pd.to_datetime(df["outroDate"], format="%Y", errors="coerce")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# 3. Clean rows with missing introDate
|
|
140
|
+
df_intro = df.dropna(subset=["introDate"]).copy()
|
|
141
|
+
|
|
142
|
+
# 4. Group by introDate and count species
|
|
143
|
+
in_species = (
|
|
144
|
+
df_intro.groupby("introDate", sort=True)["id"]
|
|
145
|
+
.count()
|
|
146
|
+
.reset_index(name="nspec")
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# 5. Cumulative sum
|
|
150
|
+
in_species["cumn"] = in_species["nspec"].cumsum()
|
|
151
|
+
|
|
152
|
+
# 6. Clean outro side and count outgoing species
|
|
153
|
+
df_outro = df.dropna(subset=["outroDate"]).copy()
|
|
154
|
+
|
|
155
|
+
out_species = (
|
|
156
|
+
df_outro.groupby("outroDate", sort=True)["id"]
|
|
157
|
+
.count()
|
|
158
|
+
.reset_index(name="nspeco")
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# 7. Merge intro and outro on date
|
|
162
|
+
n_species = pd.merge(in_species, out_species, how="outer", left_on="introDate", right_on="outroDate")
|
|
163
|
+
|
|
164
|
+
# 8. Replace NaNs with 0
|
|
165
|
+
n_species["nspec"] = n_species["nspec"].fillna(0).astype(int)
|
|
166
|
+
n_species["nspeco"] = n_species["nspeco"].fillna(0).astype(int)
|
|
167
|
+
|
|
168
|
+
# 9. Net species present at each time step
|
|
169
|
+
n_species["total"] = n_species["nspec"] - n_species["nspeco"]
|
|
170
|
+
|
|
171
|
+
# 10. Final frame with total species over time
|
|
172
|
+
tot_species = n_species[["introDate", "total"]].copy()
|
|
173
|
+
|
|
174
|
+
# 11. Optional: sort and compute cumulative total over time
|
|
175
|
+
tot_species = tot_species.sort_values("introDate")
|
|
176
|
+
tot_species["cumulative_total"] = tot_species["total"].cumsum()
|
|
177
|
+
|
|
178
|
+
return tot_species
|
|
179
|
+
|
|
180
|
+
else:
|
|
181
|
+
taxon = filePath + "taxon.txt"
|
|
182
|
+
distribution = filePath + "distribution.txt"
|
|
183
|
+
|
|
184
|
+
df_t = pd.read_csv(taxon, sep="\t")
|
|
185
|
+
df_dist = pd.read_csv(distribution, sep="\t")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Now apply this on the whole dataframe
|
|
189
|
+
|
|
190
|
+
df_t["speciesKey"] = df_t["scientificName"].apply(get_speciesKey)
|
|
191
|
+
|
|
192
|
+
df_merged = df_dist.merge(df_t[['id', 'speciesKey']], on='id', how='left')
|
|
193
|
+
|
|
194
|
+
species_to_keep = df_merged["speciesKey"].unique()
|
|
195
|
+
species_to_keep = np.where(species_to_keep == 'Uncertain', -1, species_to_keep)
|
|
196
|
+
species_to_keep = species_to_keep.astype(int)
|
|
197
|
+
|
|
198
|
+
return species_to_keep
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
from scipy.optimize import minimize
|
|
5
|
+
from scipy.optimize import fmin
|
|
6
|
+
|
|
7
|
+
def count_m(t, params):
|
|
8
|
+
"""Calculates the mean, mu, from Solow and Costello (2004)."""
|
|
9
|
+
m0 = params[0]
|
|
10
|
+
m1 = params[1]
|
|
11
|
+
m = np.exp(m0 + m1 * t)
|
|
12
|
+
return m
|
|
13
|
+
|
|
14
|
+
def count_pi(s, t, params):
|
|
15
|
+
"""Calculates the variable pi from Solow and Costello (2004)."""
|
|
16
|
+
pi0 = params[2]
|
|
17
|
+
pi1 = params[3]
|
|
18
|
+
pi2 = params[4]
|
|
19
|
+
exponent = np.clip(pi0 + pi1 * t + pi2 * np.exp(t - s), -700, 700)
|
|
20
|
+
num = np.exp(exponent)
|
|
21
|
+
pi = np.divide(num, (1 + num), out=np.zeros_like(num), where=(1 + num) != 0)
|
|
22
|
+
pi = np.where(np.isinf(num), 1, pi)
|
|
23
|
+
return pi
|
|
24
|
+
|
|
25
|
+
def count_p(t, params):
|
|
26
|
+
"""Calculates the value p from Solow and Costello (2004).
|
|
27
|
+
It uses matrix coding for efficiency.
|
|
28
|
+
"""
|
|
29
|
+
S = np.tile(np.arange(1, t + 1), (t, 1))
|
|
30
|
+
thing = 1 - count_pi(S, S.T, params)
|
|
31
|
+
thing[t - 1, :] = 1
|
|
32
|
+
up = np.triu(np.ones_like(thing), 1)
|
|
33
|
+
thing2 = np.tril(thing) + up
|
|
34
|
+
product = np.prod(thing2, axis=0)
|
|
35
|
+
pst = product * count_pi(np.arange(1, t + 1), t, params)
|
|
36
|
+
return pst
|
|
37
|
+
|
|
38
|
+
def count_lambda(params, N):
|
|
39
|
+
"""
|
|
40
|
+
This function calculates lambda from Solow and Costello, 2004.
|
|
41
|
+
params is a vector of parameters
|
|
42
|
+
"""
|
|
43
|
+
lambda_result = np.zeros(N)
|
|
44
|
+
for t in range(1, N + 1):
|
|
45
|
+
S = np.arange(1, t + 1)
|
|
46
|
+
Am = count_m(S, params)
|
|
47
|
+
Ap = count_p(t, params)
|
|
48
|
+
lambda_result[t - 1] = np.dot(Am, Ap)
|
|
49
|
+
return lambda_result
|
|
50
|
+
|
|
51
|
+
def count_log_like(params, restrict, num_discov):
|
|
52
|
+
"""
|
|
53
|
+
This function file calculates the log likelihood function for Solow and
|
|
54
|
+
Costello (2004). It takes into account any possible restrictions (See
|
|
55
|
+
below)
|
|
56
|
+
|
|
57
|
+
params is a vector of parameters
|
|
58
|
+
restrict is a vector (same size as params) that places restrictions on the
|
|
59
|
+
parameters. If restrict[i]=99, then there is no restriction for the ith
|
|
60
|
+
parameter. If restrict[i]=0 (for example) then the restriction is exactly
|
|
61
|
+
that.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
f = np.where(restrict != 99)[0]
|
|
65
|
+
g = np.where(restrict == 99)[0]
|
|
66
|
+
new_params = params.copy()
|
|
67
|
+
new_params[g] = params[g]
|
|
68
|
+
new_params[f] = restrict[f]
|
|
69
|
+
|
|
70
|
+
summand2 = np.zeros_like(num_discov, dtype=float)
|
|
71
|
+
lambda_values = np.zeros_like(num_discov, dtype=float)
|
|
72
|
+
|
|
73
|
+
for t in range(1, len(num_discov) + 1):
|
|
74
|
+
S = np.arange(1, t + 1)
|
|
75
|
+
Am = count_m(S, new_params)
|
|
76
|
+
Ap = count_p(t, new_params)
|
|
77
|
+
lambda_t = np.dot(Am, Ap)
|
|
78
|
+
lambda_values[t - 1] = lambda_t
|
|
79
|
+
summand2[t - 1] = num_discov[t - 1] * np.log(lambda_t) - lambda_t if lambda_t > 0 else -lambda_t
|
|
80
|
+
|
|
81
|
+
LL = -np.sum(summand2)
|
|
82
|
+
return LL, lambda_values
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def simulate_solow_costello(annual_time_gbif, annual_rate_gbif, vis=False):
|
|
86
|
+
"""
|
|
87
|
+
Solow-Costello simulation of the rate of establishment.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
annual_time_gbif : pandas.Series
|
|
92
|
+
Time series of the rate of establishment.
|
|
93
|
+
annual_rate_gbif : pandas.Series
|
|
94
|
+
Rates corresponding to the time series.
|
|
95
|
+
vis : bool, optional
|
|
96
|
+
Create a plot of the simulation. Default is False.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
C1: numpy.Series
|
|
101
|
+
Result of the simulation.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
# global num_discov; # No need for global, pass as argument
|
|
105
|
+
num_discov = pd.Series(annual_rate_gbif).T # Load and transpose
|
|
106
|
+
T = pd.Series(annual_time_gbif) #np.arange(1851, 1996) # Create the time period
|
|
107
|
+
# options = optimset('TolFun',.01,'TolX',.01); # Tolerance is handled differently in scipy
|
|
108
|
+
|
|
109
|
+
guess = np.array([-1.1106, 0.0135, -1.4534, 0.1, 0.1]) # Initial guess
|
|
110
|
+
constr = 99 * np.ones_like(guess) # Constraint vector
|
|
111
|
+
|
|
112
|
+
vec1 = fmin(lambda x: count_log_like(x, constr, num_discov)[0], guess, xtol=0.01, ftol=0.01)
|
|
113
|
+
val1 = count_log_like(vec1, constr, num_discov)[0] # Get the function value at the minimum
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
C1 = count_lambda(vec1, len(num_discov)) # Calculate the mean of Y
|
|
117
|
+
|
|
118
|
+
if vis:
|
|
119
|
+
# Create the plot
|
|
120
|
+
plt.plot(T, np.cumsum(num_discov), 'k-', T, np.cumsum(C1), 'k--')
|
|
121
|
+
plt.legend(['Discoveries', 'Unrestricted'])
|
|
122
|
+
plt.xlabel('Time')
|
|
123
|
+
plt.ylabel('Cumulative Discovery')
|
|
124
|
+
plt.show()
|
|
125
|
+
|
|
126
|
+
return C1
|
b3alien/utils.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Additional utilities
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import geopandas as gpd
|
|
7
|
+
import pyarrow
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def detect_runtime():
|
|
11
|
+
"""
|
|
12
|
+
Detects the runtime environment where the code is executed.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
str: One of "Jupyter Notebook", "IPython Terminal", or "Standard Python Script"
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
from IPython import get_ipython
|
|
19
|
+
shell = get_ipython().__class__.__name__
|
|
20
|
+
if shell == 'ZMQInteractiveShell':
|
|
21
|
+
return "Jupyter Notebook"
|
|
22
|
+
elif shell == 'TerminalInteractiveShell':
|
|
23
|
+
return "IPython Terminal"
|
|
24
|
+
else:
|
|
25
|
+
return "Other IPython"
|
|
26
|
+
except (ImportError, AttributeError, NameError):
|
|
27
|
+
return "Standard Python Script"
|
|
28
|
+
|
|
29
|
+
def in_jupyter():
|
|
30
|
+
"""Returns True if running inside a Jupyter Notebook or Lab."""
|
|
31
|
+
return detect_runtime() == "Jupyter Notebook"
|
|
32
|
+
|
|
33
|
+
def in_ipython():
|
|
34
|
+
"""Returns True if running inside any IPython shell (not standard Python)."""
|
|
35
|
+
return detect_runtime() != "Standard Python Script"
|
|
36
|
+
|
|
37
|
+
def in_script():
|
|
38
|
+
"""Returns True if running in a standard Python script (non-interactive)."""
|
|
39
|
+
return detect_runtime() == "Standard Python Script"
|
|
40
|
+
|
|
41
|
+
def to_geoparquet(csvFile, geoFile, leftID=eqdcellcode, rightID=cellCode, exportPath='./data/export.parquet'):
|
|
42
|
+
"""
|
|
43
|
+
Convert a GBIF cube download into a GeoParquet file, using the geometry of a GPKG
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
csvFile : str
|
|
48
|
+
Path to the GBIF cube csv file.
|
|
49
|
+
geoFile : str
|
|
50
|
+
Path to the GeoPackage file.
|
|
51
|
+
leftID : str, optional
|
|
52
|
+
Column name within the GBIF cube to match the geometry. Default is 'edqcellcode'.
|
|
53
|
+
rightID : str, optional
|
|
54
|
+
Column name within the GeoPackage geometry. Default is 'cellCode'
|
|
55
|
+
exportPath : str, optional
|
|
56
|
+
Path to which the GeoParquet file needs to be exported.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
A GeoParquet file at the location of exportPath
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
data = pd.read_csv(csvFile, sep='\t')
|
|
64
|
+
geoRef = gpd.read_file(geoFile, engine='pyogrio', use_arrow=True, crs="EPSG:4326")
|
|
65
|
+
|
|
66
|
+
test_merge = pd.merge(data, qdgc_ref, left_on=leftID, right_on=rightID)
|
|
67
|
+
|
|
68
|
+
gdf = gpd.GeoDataFrame(test_merge, geometry='geometry')
|
|
69
|
+
if gdf.crs is None:
|
|
70
|
+
gdf.set_crs(crs, inplace=True)
|
|
71
|
+
|
|
72
|
+
gdf.to_parquet(exportPath, engine="pyarrow", index=False)
|
|
73
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Visualisation of Data Cubes and the resulting simulations
|
|
4
|
+
=========================================================
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from .b3gee import initialize
|
|
10
|
+
from .b3gee import gdf_to_ee_featurecollection
|
|
11
|
+
from .visualisation import visualize_ee_layers
|
|
12
|
+
|
|
13
|
+
__all__ = ["initialize", "gdf_to_ee_featurecollection", "visualize_ee_layers"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import ee
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
def initialize(project):
|
|
5
|
+
"""
|
|
6
|
+
Inititialise a Google project
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
project : str
|
|
11
|
+
Name of your Google project
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
ee.Initialize()
|
|
15
|
+
except Exception as e:
|
|
16
|
+
ee.Authenticate()
|
|
17
|
+
ee.Initialize(project=project)
|
|
18
|
+
|
|
19
|
+
def gdf_to_ee_featurecollection(gdf):
|
|
20
|
+
"""
|
|
21
|
+
Transform a GeoDataFrame in to an Earth Engine Feature collection
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
gdf : geopandas.DataFrame
|
|
26
|
+
Species OccurrenceCube from GBIF.
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
ee.FeatureCollection
|
|
31
|
+
"""
|
|
32
|
+
geojson = json.loads(gdf.to_json())
|
|
33
|
+
return ee.FeatureCollection(geojson)
|
|
34
|
+
|
|
35
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import ee
|
|
2
|
+
import geemap
|
|
3
|
+
import folium
|
|
4
|
+
import tempfile
|
|
5
|
+
import webbrowser
|
|
6
|
+
|
|
7
|
+
def add_ee_layer(self, ee_object, vis_params, name):
|
|
8
|
+
try:
|
|
9
|
+
if isinstance(ee_object, ee.image.Image):
|
|
10
|
+
map_id_dict = ee_object.getMapId(vis_params)
|
|
11
|
+
folium.raster_layers.TileLayer(
|
|
12
|
+
tiles=map_id_dict['tile_fetcher'].url_format,
|
|
13
|
+
attr='Google Earth Engine',
|
|
14
|
+
name=name,
|
|
15
|
+
overlay=True,
|
|
16
|
+
control=True
|
|
17
|
+
).add_to(self)
|
|
18
|
+
elif isinstance(ee_object, ee.featurecollection.FeatureCollection):
|
|
19
|
+
# Style the FeatureCollection (example with color)
|
|
20
|
+
styled_fc = ee_object.style(**(vis_params or {'color': 'FF0000'}))
|
|
21
|
+
map_id_dict = styled_fc.getMapId({})
|
|
22
|
+
folium.raster_layers.TileLayer(
|
|
23
|
+
tiles=map_id_dict['tile_fetcher'].url_format,
|
|
24
|
+
attr='Google Earth Engine',
|
|
25
|
+
name=name,
|
|
26
|
+
overlay=True,
|
|
27
|
+
control=True
|
|
28
|
+
).add_to(self)
|
|
29
|
+
else:
|
|
30
|
+
raise TypeError(f"Unsupported ee object type: {type(ee_object)}")
|
|
31
|
+
except Exception as e:
|
|
32
|
+
print("Could not add layer:", e)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Patch Folium to support EE layers
|
|
37
|
+
def patch_folium():
|
|
38
|
+
folium.Map.add_ee_layer = add_ee_layer
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def visualize_ee_layers(layers, center=[0, 0], zoom=2, save_path=None, show=True):
|
|
42
|
+
"""
|
|
43
|
+
Visualize multiple Earth Engine layers on a folium map.
|
|
44
|
+
|
|
45
|
+
Parameters:
|
|
46
|
+
layers: List of (ee_object, vis_params, layer_name)
|
|
47
|
+
center: Center [lat, lon] for map
|
|
48
|
+
zoom: Zoom level
|
|
49
|
+
save_path: Optional filepath to save the HTML map
|
|
50
|
+
show: If True, opens the map in the browser
|
|
51
|
+
"""
|
|
52
|
+
m = folium.Map(location=center, zoom_start=zoom)
|
|
53
|
+
|
|
54
|
+
for ee_object, vis_params, name in layers:
|
|
55
|
+
m.add_ee_layer(ee_object, vis_params, name)
|
|
56
|
+
|
|
57
|
+
folium.LayerControl().add_to(m)
|
|
58
|
+
|
|
59
|
+
if save_path:
|
|
60
|
+
m.save(save_path)
|
|
61
|
+
print(f"Map saved to {save_path}")
|
|
62
|
+
if show:
|
|
63
|
+
webbrowser.open(f"file://{os.path.abspath(save_path)}")
|
|
64
|
+
else:
|
|
65
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".html") as f:
|
|
66
|
+
m.save(f.name)
|
|
67
|
+
if show:
|
|
68
|
+
webbrowser.open(f.name)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: b3alien
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Calculating the CBD target 6.1 indicator from occurrence cubes
|
|
5
|
+
Author-email: Maarten Trekels <maarten.trekels@plantentuinmeise.be>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: homepage, https://github.com/mtrekels/b3alien
|
|
8
|
+
Project-URL: repository, https://github.com/mtrekels/b3alien
|
|
9
|
+
Project-URL: documentation, https://b3alien.readthedocs.io/
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: aiohappyeyeballs>=2.6.1
|
|
14
|
+
Requires-Dist: aiohttp>=3.11.18
|
|
15
|
+
Requires-Dist: aiosignal>=1.3.2
|
|
16
|
+
Requires-Dist: alabaster>=1.0.0
|
|
17
|
+
Requires-Dist: appdirs>=1.4.4
|
|
18
|
+
Requires-Dist: asttokens>=3.0.0
|
|
19
|
+
Requires-Dist: async-timeout>=5.0.1
|
|
20
|
+
Requires-Dist: attrs>=25.3.0
|
|
21
|
+
Requires-Dist: backports.tarfile>=1.2.0
|
|
22
|
+
Requires-Dist: bqplot>=0.12.44
|
|
23
|
+
Requires-Dist: branca>=0.8.1
|
|
24
|
+
Requires-Dist: build>=1.2.2.post1
|
|
25
|
+
Requires-Dist: cachetools>=5.5.2
|
|
26
|
+
Requires-Dist: cattrs>=24.1.3
|
|
27
|
+
Requires-Dist: certifi>=2025.4.26
|
|
28
|
+
Requires-Dist: cffi>=1.17.1
|
|
29
|
+
Requires-Dist: charset-normalizer>=3.4.2
|
|
30
|
+
Requires-Dist: click>=8.1.8
|
|
31
|
+
Requires-Dist: cloudpickle>=3.1.1
|
|
32
|
+
Requires-Dist: colour>=0.1.5
|
|
33
|
+
Requires-Dist: comm>=0.2.2
|
|
34
|
+
Requires-Dist: contourpy>=1.3.2
|
|
35
|
+
Requires-Dist: coverage>=7.8.0
|
|
36
|
+
Requires-Dist: cryptography>=44.0.3
|
|
37
|
+
Requires-Dist: cycler>=0.12.1
|
|
38
|
+
Requires-Dist: dask>=2025.4.1
|
|
39
|
+
Requires-Dist: decorator>=5.2.1
|
|
40
|
+
Requires-Dist: docutils>=0.21.2
|
|
41
|
+
Requires-Dist: earthengine-api>=1.5.13
|
|
42
|
+
Requires-Dist: eerepr>=0.1.1
|
|
43
|
+
Requires-Dist: exceptiongroup>=1.2.2
|
|
44
|
+
Requires-Dist: executing>=2.2.0
|
|
45
|
+
Requires-Dist: folium>=0.19.5
|
|
46
|
+
Requires-Dist: fonttools>=4.57.0
|
|
47
|
+
Requires-Dist: frozenlist>=1.6.0
|
|
48
|
+
Requires-Dist: fsspec>=2025.3.2
|
|
49
|
+
Requires-Dist: future>=1.0.0
|
|
50
|
+
Requires-Dist: gcsfs>=2025.3.2
|
|
51
|
+
Requires-Dist: geemap>=0.35.3
|
|
52
|
+
Requires-Dist: geocoder>=1.38.1
|
|
53
|
+
Requires-Dist: geojson-rewind>=1.1.0
|
|
54
|
+
Requires-Dist: geomet>=1.1.0
|
|
55
|
+
Requires-Dist: geopandas>=1.0.1
|
|
56
|
+
Requires-Dist: google-api-core>=2.24.2
|
|
57
|
+
Requires-Dist: google-api-python-client>=2.169.0
|
|
58
|
+
Requires-Dist: google-auth>=2.39.0
|
|
59
|
+
Requires-Dist: google-auth-httplib2>=0.2.0
|
|
60
|
+
Requires-Dist: google-auth-oauthlib>=1.2.2
|
|
61
|
+
Requires-Dist: google-cloud-core>=2.4.3
|
|
62
|
+
Requires-Dist: google-cloud-storage>=3.1.0
|
|
63
|
+
Requires-Dist: google-crc32c>=1.7.1
|
|
64
|
+
Requires-Dist: google-resumable-media>=2.7.2
|
|
65
|
+
Requires-Dist: googleapis-common-protos>=1.70.0
|
|
66
|
+
Requires-Dist: httplib2>=0.22.0
|
|
67
|
+
Requires-Dist: id>=1.5.0
|
|
68
|
+
Requires-Dist: idna>=3.10
|
|
69
|
+
Requires-Dist: imagesize>=1.4.1
|
|
70
|
+
Requires-Dist: importlib_metadata>=8.7.0
|
|
71
|
+
Requires-Dist: iniconfig>=2.1.0
|
|
72
|
+
Requires-Dist: ipyevents>=2.0.2
|
|
73
|
+
Requires-Dist: ipyfilechooser>=0.6.0
|
|
74
|
+
Requires-Dist: ipyleaflet>=0.19.2
|
|
75
|
+
Requires-Dist: ipython>=8.36.0
|
|
76
|
+
Requires-Dist: ipytree>=0.2.2
|
|
77
|
+
Requires-Dist: ipywidgets>=8.1.6
|
|
78
|
+
Requires-Dist: jaraco.classes>=3.4.0
|
|
79
|
+
Requires-Dist: jaraco.context>=6.0.1
|
|
80
|
+
Requires-Dist: jaraco.functools>=4.1.0
|
|
81
|
+
Requires-Dist: jedi>=0.19.2
|
|
82
|
+
Requires-Dist: jeepney>=0.9.0
|
|
83
|
+
Requires-Dist: Jinja2>=3.1.6
|
|
84
|
+
Requires-Dist: jupyter-leaflet>=0.19.2
|
|
85
|
+
Requires-Dist: jupyterlab_widgets>=3.0.14
|
|
86
|
+
Requires-Dist: keyring>=25.6.0
|
|
87
|
+
Requires-Dist: kiwisolver>=1.4.8
|
|
88
|
+
Requires-Dist: llvmlite>=0.44.0
|
|
89
|
+
Requires-Dist: locket>=1.0.0
|
|
90
|
+
Requires-Dist: markdown-it-py>=3.0.0
|
|
91
|
+
Requires-Dist: MarkupSafe>=3.0.2
|
|
92
|
+
Requires-Dist: matplotlib>=3.10.1
|
|
93
|
+
Requires-Dist: matplotlib-inline>=0.1.7
|
|
94
|
+
Requires-Dist: mdit-py-plugins>=0.4.2
|
|
95
|
+
Requires-Dist: mdurl>=0.1.2
|
|
96
|
+
Requires-Dist: more-itertools>=10.7.0
|
|
97
|
+
Requires-Dist: multidict>=6.4.3
|
|
98
|
+
Requires-Dist: myst-parser>=4.0.1
|
|
99
|
+
Requires-Dist: narwhals>=1.37.1
|
|
100
|
+
Requires-Dist: nh3>=0.2.21
|
|
101
|
+
Requires-Dist: numba>=0.61.2
|
|
102
|
+
Requires-Dist: numpy>=2.2.5
|
|
103
|
+
Requires-Dist: oauthlib>=3.2.2
|
|
104
|
+
Requires-Dist: packaging>=25.0
|
|
105
|
+
Requires-Dist: pandas>=2.2.3
|
|
106
|
+
Requires-Dist: parso>=0.8.4
|
|
107
|
+
Requires-Dist: partd>=1.4.2
|
|
108
|
+
Requires-Dist: pexpect>=4.9.0
|
|
109
|
+
Requires-Dist: pillow>=11.2.1
|
|
110
|
+
Requires-Dist: platformdirs>=4.3.7
|
|
111
|
+
Requires-Dist: plotly>=6.0.1
|
|
112
|
+
Requires-Dist: pluggy>=1.5.0
|
|
113
|
+
Requires-Dist: prompt_toolkit>=3.0.51
|
|
114
|
+
Requires-Dist: propcache>=0.3.1
|
|
115
|
+
Requires-Dist: proto-plus>=1.26.1
|
|
116
|
+
Requires-Dist: protobuf>=6.30.2
|
|
117
|
+
Requires-Dist: ptyprocess>=0.7.0
|
|
118
|
+
Requires-Dist: pure_eval>=0.2.3
|
|
119
|
+
Requires-Dist: pyarrow>=20.0.0
|
|
120
|
+
Requires-Dist: pyasn1>=0.6.1
|
|
121
|
+
Requires-Dist: pyasn1_modules>=0.4.2
|
|
122
|
+
Requires-Dist: pycparser>=2.22
|
|
123
|
+
Requires-Dist: pygbif>=0.6.5
|
|
124
|
+
Requires-Dist: Pygments>=2.19.1
|
|
125
|
+
Requires-Dist: pyogrio>=0.10.0
|
|
126
|
+
Requires-Dist: pyparsing>=3.2.3
|
|
127
|
+
Requires-Dist: pyperclip>=1.9.0
|
|
128
|
+
Requires-Dist: pyproj>=3.7.1
|
|
129
|
+
Requires-Dist: pyproject_hooks>=1.2.0
|
|
130
|
+
Requires-Dist: pyshp>=2.3.1
|
|
131
|
+
Requires-Dist: pytest>=8.3.5
|
|
132
|
+
Requires-Dist: pytest-cov>=6.1.1
|
|
133
|
+
Requires-Dist: python-box>=7.3.2
|
|
134
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
135
|
+
Requires-Dist: pytz>=2025.2
|
|
136
|
+
Requires-Dist: PyYAML>=6.0.2
|
|
137
|
+
Requires-Dist: ratelim>=0.1.6
|
|
138
|
+
Requires-Dist: readme_renderer>=44.0
|
|
139
|
+
Requires-Dist: requests>=2.32.3
|
|
140
|
+
Requires-Dist: requests-cache>=1.2.1
|
|
141
|
+
Requires-Dist: requests-oauthlib>=2.0.0
|
|
142
|
+
Requires-Dist: requests-toolbelt>=1.0.0
|
|
143
|
+
Requires-Dist: rfc3986>=2.0.0
|
|
144
|
+
Requires-Dist: rich>=14.0.0
|
|
145
|
+
Requires-Dist: rsa>=4.9.1
|
|
146
|
+
Requires-Dist: scipy>=1.15.2
|
|
147
|
+
Requires-Dist: scooby>=0.10.1
|
|
148
|
+
Requires-Dist: SecretStorage>=3.3.3
|
|
149
|
+
Requires-Dist: shapely>=2.1.0
|
|
150
|
+
Requires-Dist: six>=1.17.0
|
|
151
|
+
Requires-Dist: snowballstemmer>=3.0.0.1
|
|
152
|
+
Requires-Dist: sparse>=0.16.0
|
|
153
|
+
Requires-Dist: Sphinx>=8.1.3
|
|
154
|
+
Requires-Dist: sphinx-rtd-theme>=3.0.2
|
|
155
|
+
Requires-Dist: sphinxcontrib-applehelp>=2.0.0
|
|
156
|
+
Requires-Dist: sphinxcontrib-devhelp>=2.0.0
|
|
157
|
+
Requires-Dist: sphinxcontrib-htmlhelp>=2.1.0
|
|
158
|
+
Requires-Dist: sphinxcontrib-jquery>=4.1
|
|
159
|
+
Requires-Dist: sphinxcontrib-jsmath>=1.0.1
|
|
160
|
+
Requires-Dist: sphinxcontrib-qthelp>=2.0.0
|
|
161
|
+
Requires-Dist: sphinxcontrib-serializinghtml>=2.0.0
|
|
162
|
+
Requires-Dist: stack-data>=0.6.3
|
|
163
|
+
Requires-Dist: tdqm>=0.0.1
|
|
164
|
+
Requires-Dist: tomli>=2.2.1
|
|
165
|
+
Requires-Dist: toolz>=1.0.0
|
|
166
|
+
Requires-Dist: tqdm>=4.67.1
|
|
167
|
+
Requires-Dist: traitlets>=5.14.3
|
|
168
|
+
Requires-Dist: traittypes>=0.2.1
|
|
169
|
+
Requires-Dist: twine>=6.1.0
|
|
170
|
+
Requires-Dist: typing_extensions>=4.13.2
|
|
171
|
+
Requires-Dist: tzdata>=2025.2
|
|
172
|
+
Requires-Dist: uritemplate>=4.1.1
|
|
173
|
+
Requires-Dist: url-normalize>=2.2.1
|
|
174
|
+
Requires-Dist: urllib3>=2.4.0
|
|
175
|
+
Requires-Dist: wcwidth>=0.2.13
|
|
176
|
+
Requires-Dist: widgetsnbextension>=4.0.14
|
|
177
|
+
Requires-Dist: xarray>=2025.4.0
|
|
178
|
+
Requires-Dist: xyzservices>=2025.4.0
|
|
179
|
+
Requires-Dist: yarl>=1.20.0
|
|
180
|
+
Requires-Dist: zipp>=3.21.0
|
|
181
|
+
Provides-Extra: dev
|
|
182
|
+
Requires-Dist: pytest; extra == "dev"
|
|
183
|
+
Requires-Dist: flake8; extra == "dev"
|
|
184
|
+
Dynamic: license-file
|
|
185
|
+
|
|
186
|
+
# b3alien: a Python package to calculate the Target 6.1 headline indicator of the CBD
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
## Introduction
|
|
190
|
+
|
|
191
|
+
The historic Kunming-Montreal Global Biodiversity
|
|
192
|
+
Framework, which supports the achievement of the
|
|
193
|
+
Sustainable Development Goals and builds on the
|
|
194
|
+
Convention on Biological Diversity’s (CBD) previous
|
|
195
|
+
Strategic Plans, sets out an ambitious pathway to reach
|
|
196
|
+
the global vision of a world living in harmony with nature
|
|
197
|
+
by 2050. Among the Framework’s key elements are 23
|
|
198
|
+
targets for 2030. In order to track the progress on the
|
|
199
|
+
targets, a number of indicators were agreed upon for
|
|
200
|
+
each target. The B3ALIEN software provides a technical
|
|
201
|
+
solution to track Target 6: “Reduce the Introduction of
|
|
202
|
+
Invasive Alien Species by 50% and Minimize Their
|
|
203
|
+
Impact.” It mainly focusses on the headline indicator: rate
|
|
204
|
+
of invasive alien species establishment, but can provide
|
|
205
|
+
input to some of the complementary indicators.
|
|
206
|
+
|
|
207
|
+
Decision makers at local, regional, national and
|
|
208
|
+
international levels need accurate and reliable
|
|
209
|
+
information about status, trends, threats, and they need
|
|
210
|
+
data presented in an actionable and understandable
|
|
211
|
+
format, with measures of uncertainty. Furthermore, we
|
|
212
|
+
need synthesized data products that can be combined
|
|
213
|
+
with other environmental data, such as climate, soil
|
|
214
|
+
chemistry, land use, altitude... B3ALIEN is built upon the
|
|
215
|
+
concept of data cubes developed in the Horizon Europe
|
|
216
|
+
Biodiversity Building Blocks for Policy project (b-
|
|
217
|
+
cubed.eu). It uses the solid foundations of the GBIF
|
|
218
|
+
infrastructure, where tools such as the GBIF Taxonomic
|
|
219
|
+
Backbone and the Global Registry of Introduced and
|
|
220
|
+
Invasive Species are available by default. Readily available occurrence data is used to determine and estimate
|
|
221
|
+
accurately the rate of introduction of alien species..
|
|
222
|
+
|
|
223
|
+
## Architecture
|
|
224
|
+
|
|
225
|
+
put schema here
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
b3alien/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
b3alien/utils.py,sha256=4gyPatTcOY9uD9hZdcBgEpDVL2H9qEihoabmOiBgbTY,2361
|
|
3
|
+
b3alien/b3cube/__init__.py,sha256=oAJXCVhF88Rkl5Z_Fn28WKThwxu63ZDrbOEe_Kpq7TQ,487
|
|
4
|
+
b3alien/b3cube/b3cube.py,sha256=tVmaGPxXJX3YR-joX3W8q7tFuxF8OHflFH5SKKin-dY,17351
|
|
5
|
+
b3alien/griis/__init__.py,sha256=jp_pTU7hPntJBBBOiCBKNdNC37zZ7ggpBARXu8zowKc,321
|
|
6
|
+
b3alien/griis/griis.py,sha256=9M1g3fp-PmJFe32Eg3EB45S9yyvewSXp74FNGbAcboo,5926
|
|
7
|
+
b3alien/simulation/__init__.py,sha256=YEU2Uot9gD8NUseC9xxtpJ5E1JBLcB4AQhtNgV9UELE,140
|
|
8
|
+
b3alien/simulation/simulation.py,sha256=rSl8Lr3-i27WKpJEkymgPXCp-VaAoFATqIMoCtL7RPE,4276
|
|
9
|
+
b3alien/visualisation/__init__.py,sha256=smoKdeDgfZFdNfQnOFwekUY94vGV5LSePlRR1p45TsM,331
|
|
10
|
+
b3alien/visualisation/b3gee.py,sha256=FIANrsk_d451OeKY3fUWAHy7Z3P1hf9t6PX9K6ryYVY,733
|
|
11
|
+
b3alien/visualisation/visualisation.py,sha256=Pvl8e62y4AlkiYKDi0__9XHXTJXvR7AUHtQhiAmygjU,2249
|
|
12
|
+
b3alien-0.0.1.dist-info/licenses/LICENSE,sha256=OpaLK4BvTJYb7yT8EDbVlAZQP6DkkrNUXzPvosajAeE,1077
|
|
13
|
+
b3alien-0.0.1.dist-info/METADATA,sha256=ZSDq_IDMp1gOvXRSX3wvWHyqdsoRy1oGvrKDshdi9fA,8005
|
|
14
|
+
b3alien-0.0.1.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
|
15
|
+
b3alien-0.0.1.dist-info/top_level.txt,sha256=Pvcrm6IMnotsQRwRY2z9XT_ooE-bRZHaZK7DJmkcYrw,8
|
|
16
|
+
b3alien-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Botanic Garden Meise
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
b3alien
|