Skip to content

Commit 44de6a4

Browse files
Update bounds.py align_to_cell_lines() to match FUSION alignment (#112)
* Update bounds.py align_to_cell_lines() to match FUSION alignment Modified code to align cell centers with multiple of resolution. It might be better to add an option to the database for pixel-is-point or pixel-is-cell and rename the new version of align_to_cell_lines() to align_to_cell_centers(). There are just a few places where the function is called that would need to add code to use the db option. * Update bounds.py to include commented original code * adding option for cell alignment to DB * making adjust_alignment self-contained * Updating tests and CLI to use alignment * Setting default alignment to pixel_is_cell to pass tests * added alignment fixture * fixing duplicate parameter * Fixed default alignment in CLI * added alignment to s3_storage_config fixture * Changing pixel_is_point and pixel_is_cell to pixelispoint and pixelisarea. Adding shatter test for pixelispoint case * Correcting alignment in tests * testing alignment * Changing test_command_point() to modify storage.config with pixelispoint alignment * Removing test for pixelispoint...more complicated than I thought! * Adding CRS to bounds for pipeline reader * Adding CRS to bounds when pipeline is created * backed out CRS addition to bounds * Updated to use AlignToCorner and AlignToCenter value for pixel alignment * Parametrized Tests for alignment addition (#115) * tests working after addition of parametrized fixture for alignment * remove debugging line * Fixing GDAL data type mapping and adding check for what to make nanvalue if dtype isn't float * Grid metrics (#121) * adding grid_metrics and corresponding filters * adding lfs, grid_metrics, and a test for fusion comparisons * adding fusion test case file * adding fusion comparison rasters to test data * wip, debugging differences in intensity tif output * Add linting to keep consistency (#120) * linting the project according to ruff, added config to pyproject.toml * wip, getting tests outside of fusion working again * fixing tests, skipping fusion test * fixing linting errors * remove debug noise, fix filter param name in scan * fixed lmoments and cv metrics, now matching FUSION * changing mode to mimic how FUSION is doing it * added rest of fusion metrics * added methods for counts and covers so elevation key can be applied correctly * removing alignment from tests it's unnecessary in, fixing tests that relied on checking data * adding docstrings, updating cli docs * attempt to add fusion test back with lfs in gh action workflow --------- Co-authored-by: Kyle Mann <kyle@hobu.co>
1 parent 6a27183 commit 44de6a4

154 files changed

Lines changed: 1655 additions & 723 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tests/data/NoCAL_PlumasNF_B2_2018_TestingData_FUSIONNormalized.copc.laz filter=lfs diff=lfs merge=lfs -text

.github/workflows/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ jobs:
3030
steps:
3131
- name: Checkout
3232
uses: actions/checkout@v4
33+
with:
34+
lfs: 'true'
3335
- uses: mamba-org/setup-micromamba@v2
3436
with:
3537
init-shell: bash

docs/create.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/bin/bash
22

33
# This assumes you've already done pip install -r requirements.txt in docs dir
4+
conda run -n silvimetric jupyter-book config sphinx source
45
python -m sphinx -T -b html -d _build/doctrees -D language=en source html

docs/source/_toc.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ parts:
77
- caption: Getting Started
88
chapters:
99
- file: about
10-
- file: quickstart
1110
- caption: CLI
1211
chapters:
1312
- file: cli/extract

docs/source/tutorial.md

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,17 @@ Usage:
126126
```console
127127
Usage: silvimetric initialize [OPTIONS]
128128

129+
Initialize silvimetrics DATABASE
130+
129131
Options:
130-
--bounds BOUNDS Root bounds that encapsulates all data [required]
131-
--crs CRS Coordinate system of data [required]
132-
-a, --attributes ATTRS List of attributes to include in Database
133-
-m, --metrics METRICS List of metrics to include in Database
134-
--resolution FLOAT Summary pixel resolution
135-
--help Show this message and exit.
132+
--bounds BOUNDS Root bounds that encapsulates all data [required]
133+
--crs CRS Coordinate system of data [required]
134+
-a, --attributes ATTRS List of attributes to include in Database
135+
-m, --metrics METRICS List of metrics to include in output, eg. '-m
136+
stats,percentiles'
137+
--resolution FLOAT Summary pixel resolution
138+
--alignment TEXT Pixel alignment: 'AlignToCenter' or 'AlignToCorner'
139+
--help Show this message and exit.
136140
```
137141

138142
(udm)=
@@ -212,16 +216,17 @@ Usage:
212216
```console
213217
Usage: silvimetric scan [OPTIONS] POINTCLOUD
214218

215-
Scan point cloud and determine the optimal tile size.
219+
Scan point cloud, output information on it, and determine the optimal tile
220+
size.
216221

217222
Options:
218-
--resolution FLOAT Summary pixel resolution
219-
--filter Remove empty space in computation. Will take extra
220-
time.
221-
--point_count INTEGER Point count threshold.
222-
--depth INTEGER Quadtree depth threshold.
223-
--bounds BOUNDS Bounds to scan.
224-
--help Show this message and exit.
223+
--resolution FLOAT Summary pixel resolution
224+
--filter_empty Remove empty space in computation. Will take extra
225+
time.
226+
--point_count INTEGER Point count threshold.
227+
--depth INTEGER Quadtree depth threshold.
228+
--bounds BOUNDS Bounds to scan.
229+
--help Show this message and exit.
225230
```
226231

227232
Example:
@@ -246,14 +251,14 @@ Usage:
246251
Usage: silvimetric shatter [OPTIONS] POINTCLOUD
247252

248253
Options:
249-
--bounds BOUNDS Bounds for data to include in processing
250-
--tilesize INTEGER Number of cells to include per tile
251-
--report Whether or not to write a report of the
252-
process, useful for debugging
253-
--date [%Y-%m-%d|%Y-%m-%dT%H:%M:%SZ]
254-
Date the data was produced.
255-
--dates <DATETIME DATETIME>... Date range the data was produced during
256-
--help Show this message and exit.
254+
--bounds BOUNDS Bounds for data to include in processing
255+
--tilesize INTEGER Number of cells to include per tile
256+
--report Whether or not to write a report of the
257+
process, useful for debugging
258+
--date [%Y-%m-%d|%Y-%m-%dT%H:%M:%SZ]
259+
Date the data was produced.
260+
--dates <DATETIME DATETIME>... Date range the data was produced during
261+
--help Show this message and exit.
257262
```
258263

259264
Example:
@@ -279,12 +284,12 @@ Usage:
279284
Usage: silvimetric info [OPTIONS]
280285

281286
Options:
282-
--bounds BOUNDS Bounds to filter by
283-
--date [%Y-%m-%d|%Y-%m-%dT%H:%M:%SZ]
284-
Select processes with this date
285-
--dates <DATETIME DATETIME>... Select processes within this date range
286-
--name TEXT Select processes with this name
287-
--help Show this message and exit.
287+
--bounds BOUNDS Bounds to filter by
288+
--date [%Y-%m-%d|%Y-%m-%dT%H:%M:%SZ]
289+
Select processes with this date
290+
--dates <DATETIME DATETIME>... Select processes within this date range
291+
--name TEXT Select processes with this name
292+
--help Show this message and exit.
288293
```
289294

290295
Example:
@@ -359,11 +364,11 @@ Usage: silvimetric extract [OPTIONS]
359364
Extract silvimetric metrics from DATABASE
360365

361366
Options:
362-
-a, --attributes ATTRS List of attributes to include output
363-
-m, --metrics METRICS List of metrics to include in output
364-
--bounds BOUNDS Bounds for data to include in output
365-
-o, --outdir PATH Output directory. [required]
366-
--help Show this message and exit.
367+
-a, --attributes ATTRS List of attributes to include output
368+
-m, --metrics METRICS List of metrics to include in output
369+
--bounds BOUNDS Bounds for data to include in output
370+
-o, --outdir PATH Output directory. [required]
371+
--help Show this message and exit.
367372
```
368373

369374
Example:

environment.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,4 @@ dependencies:
1717
- python-json-logger
1818
- dill
1919
- pandas
20-
- lmoments3
21-
- typing_extensions
20+
- typing_extensions

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ docstring-code-format = true
4747
select=[
4848
"A",
4949
"B",
50+
"D101",
51+
"D201",
5052
"E",
5153
"F",
5254
"ISC",

src/silvimetric/cli/cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,12 @@ def scan_cmd(
255255
@click.option(
256256
'--resolution', type=float, default=30.0, help='Summary pixel resolution'
257257
)
258+
@click.option(
259+
'--alignment',
260+
type=str,
261+
default='AlignToCenter',
262+
help="Pixel alignment: 'AlignToCenter' or 'AlignToCorner'",
263+
)
258264
@click.pass_obj
259265
def initialize_cmd(
260266
app: ApplicationConfig,
@@ -263,6 +269,7 @@ def initialize_cmd(
263269
attributes: list[Attribute],
264270
resolution: float,
265271
metrics: list[Metric],
272+
alignment: str
266273
):
267274
"""Initialize silvimetrics DATABASE"""
268275

@@ -274,6 +281,7 @@ def initialize_cmd(
274281
attrs=attributes,
275282
metrics=metrics,
276283
resolution=resolution,
284+
alignment=alignment
277285
)
278286
return initialize.initialize(storageconfig)
279287

src/silvimetric/cli/common.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import click
22
import pyproj
33
import webbrowser
4-
from contextlib import nullcontext
54

65
import dask
76
from dask.diagnostics import ProgressBar
87
from dask.distributed import Client, LocalCluster
9-
from distributed.client import _get_global_client as get_client
108

119
from ..resources.metrics import (
1210
l_moments,
@@ -20,6 +18,14 @@
2018

2119

2220
class BoundsParamType(click.ParamType):
21+
"""Click parameter type for the Bounds class.
22+
23+
Accepts bounds as a stringified JSON object or bbox array. Examples:
24+
"([1,101],[2,102],[3,103])"
25+
"{\"minx\": 1,\"miny\": 2,\"maxx\": 101,\"maxy\": 102}"
26+
"[1,2,101,102]"
27+
"[1,2,3,101,102,103]"
28+
"""
2329
name = 'Bounds'
2430

2531
def convert(self, value, param, ctx):
@@ -31,6 +37,10 @@ def convert(self, value, param, ctx):
3137

3238

3339
class CRSParamType(click.ParamType):
40+
"""Click parameter type for the Coordinate Reference System of a project.
41+
42+
Accepts a string and returns an instance of the pyproj.CRS class.
43+
"""
3444
name = 'CRS'
3545

3646
def convert(self, value, param, ctx) -> pyproj.CRS:
@@ -42,6 +52,10 @@ def convert(self, value, param, ctx) -> pyproj.CRS:
4252

4353

4454
class AttrParamType(click.ParamType):
55+
"""Click parameter for SilviMetric Attributes.
56+
57+
Returns list of PDAL dimensions that match the strings input.
58+
"""
4559
name = 'Attrs'
4660

4761
# TODO add import similar to metrics
@@ -56,10 +70,15 @@ def convert(self, value, param, ctx) -> list[Attribute]:
5670
elif isinstance(value, str):
5771
return Attributes[value]
5872
else:
59-
self.fail(f'{value!r} is of an invalid type, {e}', param, ctx)
73+
self.fail(f'{value!r} is of an invalid type.', param, ctx)
6074

6175

6276
class MetricParamType(click.ParamType):
77+
"""Custom Click parameter type.
78+
79+
This param accepts names of metric groups or a path to a file containing
80+
custom metrics.
81+
"""
6382
name = 'metrics'
6483

6584
def convert(self, value, param, ctx) -> list[Metric]:
@@ -80,7 +99,7 @@ def convert(self, value, param, ctx) -> list[Metric]:
8099
if not p.exists():
81100
self.fail(
82101
'Failed to find import file for metrics at'
83-
f' {str(p)}',
102+
f' {p}',
84103
param,
85104
ctx,
86105
)
@@ -93,7 +112,7 @@ def convert(self, value, param, ctx) -> list[Metric]:
93112
ms = user_metrics.metrics()
94113
except Exception as e:
95114
self.fail(
96-
f'Failed to import metrics from {str(p)} with error {e}',
115+
f'Failed to import metrics from {p} with error {e}',
97116
param,
98117
ctx,
99118
)
@@ -126,7 +145,7 @@ def convert(self, value, param, ctx) -> list[Metric]:
126145
metrics.add(m)
127146
else:
128147
metrics.udpate(list(m))
129-
except Exception as e:
148+
except Exception:
130149
self.fail(
131150
f'{val!r} is not available in Metrics', param, ctx
132151
)

src/silvimetric/commands/extract.py

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
np_to_gdal_types = {
1515
np.dtype(np.byte).str: gdal.GDT_Byte,
16-
np.dtype(np.int8).str: gdal.GDT_Int16,
16+
np.dtype(np.uint8).str: gdal.GDT_Byte,
17+
np.dtype(np.int8).str: gdal.GDT_Int8,
1718
np.dtype(np.uint16).str: gdal.GDT_UInt16,
1819
np.dtype(np.int16).str: gdal.GDT_Int16,
1920
np.dtype(np.uint32).str: gdal.GDT_UInt32,
@@ -26,7 +27,12 @@
2627

2728

2829
def write_tif(
29-
xsize: int, ysize: int, data: np.ndarray, name: str, config: ExtractConfig
30+
xsize: int,
31+
ysize: int,
32+
data: np.ndarray,
33+
nan_val: float | int,
34+
name: str,
35+
config: ExtractConfig,
3036
) -> None:
3137
"""
3238
Write out a raster with GDAL
@@ -55,11 +61,17 @@ def write_tif(
5561

5662
driver = gdal.GetDriverByName('GTiff')
5763
gdal_type = np_to_gdal_types[np.dtype(data.dtype).str]
58-
tif = driver.Create(str(path), int(xsize), int(ysize), 1, gdal_type)
64+
tif = driver.Create(
65+
str(path),
66+
int(xsize),
67+
int(ysize),
68+
1,
69+
gdal_type,
70+
)
5971
tif.SetGeoTransform(transform)
6072
tif.SetProjection(srs.ExportToWkt())
6173
tif.GetRasterBand(1).WriteArray(data)
62-
tif.GetRasterBand(1).SetNoDataValue(np.nan)
74+
tif.GetRasterBand(1).SetNoDataValue(nan_val)
6375
tif.FlushCache()
6476
tif = None
6577

@@ -84,8 +96,8 @@ def expl(x):
8496

8597
attrs = [a.name for a in storage.config.attrs if a.name not in ['X', 'Y']]
8698

87-
# set index so we can apply to the whole dataset without needing to skip X and Y
88-
# then reset in the index because that's what metric.do expects
99+
# set index so we can apply to the whole dataset without needing to skip X
100+
# and Y then reset in the index because that's what metric.do expects
89101
exploded = data_in.set_index(['X', 'Y']).apply(expl)[attrs].reset_index()
90102
metric_data = dask.persist(
91103
*[m.do(exploded) for m in storage.config.metrics]
@@ -99,9 +111,9 @@ def handle_overlaps(
99111
config: ExtractConfig, storage: Storage, indices: np.ndarray
100112
) -> pd.DataFrame:
101113
"""
102-
Handle cells that have overlapping data. We have to re-run metrics over these
103-
cells as there's no other accurate way to determined metric values. If there
104-
are no overlaps, this will do nothing.
114+
Handle cells that have overlapping data. We have to re-run metrics over
115+
these cells as there's no other accurate way to determined metric values.
116+
If there are no overlaps, this will do nothing.
105117
106118
:param config: ExtractConfig.
107119
:param storage: Database storage object.
@@ -143,7 +155,7 @@ def handle_overlaps(
143155
else:
144156
data = pd.concat([data, d])
145157

146-
# should find values that are not unique, meaning they have multiple entries
158+
# find values that are not unique, means they have multiple entries
147159
data = data.set_index(['X', 'Y'])
148160
redo_indices = data.index[data.index.duplicated(keep='first')]
149161
if redo_indices.empty:
@@ -179,7 +191,12 @@ def extract(config: ExtractConfig) -> None:
179191
config.log.debug(f'Extracting metrics {[m for m in ma_list]}')
180192
root_bounds = storage.config.root
181193

182-
e = Extents(config.bounds, config.resolution, root=root_bounds)
194+
e = Extents(
195+
config.bounds,
196+
config.resolution,
197+
storage.config.alignment,
198+
root=root_bounds,
199+
)
183200
i = e.get_indices()
184201
xsize = e.x2
185202
ysize = e.y2
@@ -191,11 +208,17 @@ def extract(config: ExtractConfig) -> None:
191208
config.log.info(f'Writing rasters to {config.out_dir}')
192209
for ma in ma_list:
193210
# TODO should output in sections so we don't run into memory problems
194-
m_data = np.full(
195-
shape=(ysize, xsize), fill_value=np.nan, dtype=final[ma].dtype
196-
)
211+
dtype = final[ma].dtype
212+
if dtype.kind in ['u', 'i']:
213+
nan_val = np.iinfo(dtype).max
214+
elif dtype.kind == 'f':
215+
nan_val = np.nan
216+
else:
217+
raise ValueError('Invalid Raster data type {dtype}.')
218+
219+
m_data = np.full(shape=(ysize, xsize), fill_value=nan_val, dtype=dtype)
197220
a = final[['X', 'Y', ma]].to_numpy()
198221
for x, y, md in a[:]:
199222
m_data[int(y)][int(x)] = md
200223

201-
write_tif(xsize, ysize, m_data, ma, config)
224+
write_tif(xsize, ysize, m_data, nan_val, ma, config)

0 commit comments

Comments
 (0)