Skip to content

Commit 1454d5f

Browse files
Added FUSION metrics (#70)
* added base FUSION metrics * added percentiles as separate functions * Added L-moment metrics and profile area * Fixing a few metrics, removing errant breakpoint, adding test for metrics * fixing metric test, removing mode method for now * Fixing divide by zero errors in metrics * Added simple count and check on number of points for skewness and kurtosis * added htthreshhold and coverthreshold to initialize cli * continuing with adding thresholds to metric functions * fixing __call__ * fixing m_mode call in m_abovemode * fixing m_mode call in m_madmode * skipping m_madmode * omitting m_madmode * adding use of htthreshold * fixing m_mode * fixing m_mode again * m_mode again * m_mode again * m_mode * dropping m_mode (temp) * fixing typos * backing out application of ht and cover thresholds from metrics functions * exploring ht filtering * removed debug printing * added example docstrings for m_mean and m_mode * Added constants for NODATA and height thresholds * Backed out use of htthreshold and coverthreshold * Added docstrings to all metrics functions * Kmann/metrics (#91) Merging kmann/metrics - Adds metric dependencies - Adds metric graphs - Adds filters for metrics - Changes to lmoments3 python package for L moments - Moves to dataframes for metric manipulation - Updated base SilviMetric package --------- Co-authored-by: kylemann16 <kyle@hobu.co>
1 parent e0db165 commit 1454d5f

33 files changed

+925
-278
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,9 @@ stats/
2121
**/tifs_test/**
2222
autzen-classified.copc.laz
2323

24+
#sample metrics
25+
metrics/
26+
metrics_aligned/
27+
autzen-aligned.tdb/
2428

2529
.DS_Store

docs/source/api/resources/entry.rst

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
Entry
2-
----------------------------------
3-
4-
.. autoclass:: silvimetric.resources.entry.Entry
5-
61
Attribute
72
----------------------------------
83

9-
.. autoclass:: silvimetric.resources.entry.Attribute
4+
.. automodule:: silvimetric.resources.attribute
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:
108

119
Metric
1210
----------------------------------
1311

14-
.. autoclass:: silvimetric.resources.metric.Metric
12+
.. automodule:: silvimetric.resources.metric
13+
:members:
14+
:undoc-members:
15+
:show-inheritance:

docs/source/conf.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
# -- Options for HTML output -------------------------------------------------
3838
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
3939

40-
html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "")
4140
html_theme = "sphinx_rtd_theme"
4241
html_static_path = ['_static']
4342
html_context = {
@@ -49,8 +48,6 @@
4948
'conf_py_path': '/docs/source/'
5049
}
5150

52-
if os.environ.get("READXTHEDOCS", "") == "True":
53-
html_context["READTHEDOCS"] = True
5451

5552

5653
def read_version(filename):

docs/source/index.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ SilviMetric is an open source library and set of utilities from
1212
data into raster and raster-like products.
1313

1414
Find out more about SilviMetric by visiting :ref:`about`. A slide deck about
15-
SilviMetric is also available on `Google Slides <https://docs.google.com/presentation/d/1E561EgWwLgN5R9P0LBxuI1r7kG155u8E6-MOWpkycSM/edit?usp=sharing>`__,
16-
and examples are available for viewing in `Google Colab <https://colab.research.google.com/drive/1u3Qdq3Fdy2du36WG823rKVlQtBL8eK0g#scrollTo=kY0ikB6JQ2G7>`__.
17-
15+
SilviMetric is also available on `Google Slides <https://docs.google.com/presentation/d/1E561EgWwLgN5R9P0LBxuI1r7kG155u8E6-MOWpkycSM/edit?usp=sharing>`__.
1816

1917
.. toctree::
2018
:caption: Contents

docs/source/tutorial.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ Example:
189189
$ METRIC_PATH="./path/to/python_metrics.py"
190190
$ silvimetric --database $DB_NAME initialize --bounds "$BOUNDS" \
191191
--crs "EPSG:$EPSG" \
192-
-m $METRIC_PATH -m min -m max -m mean
192+
-m "${METRIC_PATH},min,max,mean"
193193
194194
.. warning::
195195

@@ -259,8 +259,7 @@ SilviMetric will take all the previously defined variables like the bounds,
259259
resolution, and our tile size, and it will split all data values up into their
260260
respective bins. From here, SilviMetric will perform each `Metric` previously
261261
defined in :ref:`initialize` over the data in each cell. At the end of all that,
262-
this data will be written to a `SparseArray` in `TileDB`, where it will be much
263-
easier to access.
262+
this data will be written to a `SparseArray` in `TileDB`, where it will be much easier to access.
264263

265264
Usage:
266265

environment.yml

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

src/silvimetric/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
from .resources.bounds import Bounds
44
from .resources.extents import Extents
55
from .resources.storage import Storage
6-
from .resources.metric import Metric, Metrics
6+
from .resources.metric import Metric, run_metrics
7+
from .resources.metrics import grid_metrics, l_moments, percentiles, statistics, all_metrics
8+
from .resources.metrics import product_moments
79
from .resources.log import Log
810
from .resources.data import Data
9-
from .resources.entry import Attribute, Pdal_Attributes, Attributes
10-
from .resources.config import StorageConfig, ShatterConfig, ExtractConfig, ApplicationConfig
11-
from .resources.array_extensions import AttributeArray, AttributeDtype
11+
from .resources.attribute import Attribute, Pdal_Attributes, Attributes
12+
from .resources.config import StorageConfig, ShatterConfig, ExtractConfig
13+
from .resources.config import ApplicationConfig
1214

1315
from .commands.shatter import shatter
1416
from .commands.extract import extract

src/silvimetric/cli/cli.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
@click.option("--debug", is_flag=True, default=False, help="Changes logging level from INFO to DEBUG.")
1717
@click.option("--log-dir", default=None, help="Directory for log output", type=str)
1818
@click.option("--progress", is_flag=True, default=True, type=bool, help="Report progress")
19-
@click.option("--workers", type=int, default=10, help="Number of workers for Dask")
20-
@click.option("--threads", type=int, default=4, help="Number of threads per worker for Dask")
19+
@click.option("--workers", type=int, help="Number of workers for Dask")
20+
@click.option("--threads", type=int, help="Number of threads per worker for Dask")
2121
@click.option("--watch", is_flag=True, default=False, type=bool,
2222
help="Open dask diagnostic page in default web browser.")
2323
@click.option("--dasktype", default='processes', type=click.Choice(['threads',
2424
'processes']), help="What Dask uses for parallelization. For more"
2525
"information see here https://docs.dask.org/en/stable/scheduling.html#local-threads")
26-
@click.option("--scheduler", default='distributed', type=click.Choice(['distributed',
26+
@click.option("--scheduler", default='local', type=click.Choice(['distributed',
2727
'local', 'single-threaded']), help="Type of dask scheduler. Both are "
2828
"local, but are run with different dask libraries. See more here "
2929
"https://docs.dask.org/en/stable/scheduling.html.")
@@ -133,14 +133,13 @@ def scan_cmd(app, resolution, point_count, pointcloud, bounds, depth, filter):
133133
help="Coordinate system of data")
134134
@click.option("--attributes", "-a", multiple=True, type=AttrParamType(),
135135
help="List of attributes to include in Database")
136-
@click.option("--metrics", "-m", multiple=True, type=MetricParamType(),
137-
help="List of metrics to include in Database")
136+
@click.option("--metrics", "-m", type=MetricParamType(), default=[],
137+
help="List of metrics to include in output, eg. '-m stats,percentiles'")
138138
@click.option("--resolution", type=float, default=30.0,
139139
help="Summary pixel resolution")
140140
@click.pass_obj
141141
def initialize_cmd(app: ApplicationConfig, bounds: Bounds, crs: pyproj.CRS,
142142
attributes: list[Attribute], resolution: float, metrics: list[Metric]):
143-
import itertools
144143
"""Initialize silvimetrics DATABASE"""
145144

146145
storageconfig = StorageConfig(tdb_dir = app.tdb_dir,
@@ -205,8 +204,8 @@ def shatter_cmd(app, pointcloud, bounds, report, tilesize, date, dates):
205204
@cli.command('extract')
206205
@click.option("--attributes", "-a", multiple=True, type=AttrParamType(), default=[],
207206
help="List of attributes to include output")
208-
@click.option("--metrics", "-m", multiple=True, type=MetricParamType(), default=[],
209-
help="List of metrics to include in output")
207+
@click.option("--metrics", "-m", type=MetricParamType(), default=[],
208+
help="List of metrics to include in output, eg. '-m stats,percentiles'")
210209
@click.option("--bounds", type=BoundsParamType(), default=None,
211210
help="Bounds for data to include in output")
212211
@click.option("--outdir", "-o", type=click.Path(exists=False), required=True,

src/silvimetric/cli/common.py

Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import dask
66
from dask.diagnostics import ProgressBar
77
from dask.distributed import Client, LocalCluster
8+
from ..resources.metrics import l_moments, percentiles, statistics, product_moments
9+
from ..resources.metrics import aad, grid_metrics, all_metrics
810

9-
from .. import Bounds, Attribute, Metric, Attributes, Metrics, Log
11+
from .. import Bounds, Attribute, Metric, Attributes, Log
1012

1113

1214
class BoundsParamType(click.ParamType):
@@ -22,7 +24,7 @@ def convert(self, value, param, ctx):
2224
class CRSParamType(click.ParamType):
2325
name = "CRS"
2426

25-
def convert(self, value, param, ctx):
27+
def convert(self, value, param, ctx) -> pyproj.CRS:
2628
try:
2729
crs = pyproj.CRS.from_user_input(value)
2830
return crs
@@ -44,40 +46,68 @@ def convert(self, value, param, ctx) -> list[Attribute]:
4446
self.fail(f"{value!r} is of an invalid type, {e}", param, ctx)
4547

4648
class MetricParamType(click.ParamType):
47-
name="Metrics"
49+
name="metrics"
4850
def convert(self, value, param, ctx) -> list[Metric]:
49-
if '.py' in value:
50-
try:
51-
import importlib.util
52-
import os
53-
from pathlib import Path
54-
55-
cwd = os.getcwd()
56-
p = Path(cwd, value)
57-
if not p.exists():
58-
self.fail("Failed to find import file for metrics at"
59-
f" {str(p)}", param, ctx)
60-
61-
spec = importlib.util.spec_from_file_location('user_metrics', str(p))
62-
user_metrics = importlib.util.module_from_spec(spec)
63-
spec.loader.exec_module(user_metrics)
64-
ms = user_metrics.metrics()
65-
except Exception as e:
66-
self.fail(f"Failed to import metrics from {str(p)} with error {e}",
67-
param, ctx)
68-
69-
for m in ms:
70-
if not isinstance(m, Metric):
71-
self.fail(f"Invalid Metric supplied: {m}")
72-
return user_metrics.metrics()
73-
74-
try:
75-
return Metrics[value]
76-
except Exception as e:
77-
self.fail(f"{value!r} is not available in Metrics, {e}", param, ctx)
51+
if value is None or not value:
52+
return list(all_metrics.values())
53+
parsed_values = value.split(',')
54+
metrics: set[Metric] = set()
55+
for val in parsed_values:
56+
if '.py' in val:
57+
# user imported metrics from external file
58+
try:
59+
import importlib.util
60+
import os
61+
from pathlib import Path
62+
63+
cwd = os.getcwd()
64+
p = Path(cwd, val)
65+
if not p.exists():
66+
self.fail("Failed to find import file for metrics at"
67+
f" {str(p)}", param, ctx)
68+
69+
spec = importlib.util.spec_from_file_location('user_metrics', str(p))
70+
user_metrics = importlib.util.module_from_spec(spec)
71+
spec.loader.exec_module(user_metrics)
72+
ms = user_metrics.metrics()
73+
except Exception as e:
74+
self.fail(f"Failed to import metrics from {str(p)} with error {e}",
75+
param, ctx)
76+
77+
for m in ms:
78+
if not isinstance(m, Metric):
79+
self.fail(f"Invalid Metric supplied: {m}")
80+
81+
metrics.update(list(user_metrics.metrics()))
82+
else:
83+
# SilviMetric defined metrics
84+
try:
85+
if val == 'stats':
86+
metrics.update(list(statistics.values()))
87+
elif val == 'p_moments':
88+
metrics.update(list(product_moments.values()))
89+
elif val == 'l_moments':
90+
metrics.update(list(l_moments.values()))
91+
elif val == 'percentiles':
92+
metrics.update(list(percentiles.values()))
93+
elif val == 'aad':
94+
metrics.update(list(aad.aad.values()))
95+
elif val == 'grid_metrics':
96+
metrics.update(list(grid_metrics.values()))
97+
elif val == 'all':
98+
metrics.update(list(all_metrics.values()))
99+
else:
100+
m = all_metrics[val]
101+
if isinstance(m, Metric):
102+
metrics.add(m)
103+
else:
104+
metrics.udpate(list(m))
105+
except Exception as e:
106+
self.fail(f"{val!r} is not available in Metrics", param, ctx)
107+
return list(metrics)
78108

79109
def dask_handle(dasktype: str, scheduler: str, workers: int, threads: int,
80-
watch: bool, log: Log):
110+
watch: bool, log: Log) -> None:
81111
dask_config = { }
82112

83113
if dasktype == 'threads':
@@ -88,9 +118,6 @@ def dask_handle(dasktype: str, scheduler: str, workers: int, threads: int,
88118
dask_config['threads_per_worker'] = threads
89119

90120
if scheduler == 'local':
91-
if scheduler != 'distributed':
92-
log.warning("Selected scheduler type does not support continuously"
93-
"updated config information.")
94121
# fall back to dask type to determine the scheduler type
95122
dask_config['scheduler'] = dasktype
96123
if watch:
@@ -117,7 +144,7 @@ def dask_handle(dasktype: str, scheduler: str, workers: int, threads: int,
117144

118145
dask.config.set(dask_config)
119146

120-
def close_dask():
147+
def close_dask() -> None:
121148
client = dask.config.get('distributed.client')
122149
if isinstance(client, Client):
123150
client.close()

src/silvimetric/commands/extract.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from itertools import chain
33

44

5+
from typing import Union
56
from osgeo import gdal, osr
67
import dask
78
import numpy as np
@@ -54,7 +55,7 @@ def write_tif(xsize: int, ysize: int, data:np.ndarray, name: str,
5455
tif.FlushCache()
5556
tif = None
5657

57-
def get_metrics(data_in: pd.DataFrame, storage: Storage):
58+
def get_metrics(data_in: pd.DataFrame, storage: Storage) -> Union[None, pd.DataFrame]:
5859
"""
5960
Reruns a metric over this cell. Only called if there is overlapping data.
6061

src/silvimetric/commands/scan.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,15 @@ def scan(tdb_dir: str, pointcloud: str, bounds: Bounds, point_count:int=600000,
4747
cell_counts = extent_handle(extents, data, resolution, point_count,
4848
depth, log)
4949

50-
50+
num_cells = np.sum(cell_counts).item()
5151
std = np.std(cell_counts)
5252
mean = np.mean(cell_counts)
5353
rec = int(mean + std)
5454

5555
pc_info = dict(pc_info=dict(storage_bounds=tdb.config.root.to_json(),
56-
data_bounds=data.bounds.to_json(), count=dask.compute(count)))
57-
tiling_info = dict(tile_info=dict(num_cells=len(cell_counts), mean=mean,
58-
std_dev=std, recommended=rec))
56+
data_bounds=data.bounds.to_json(), count=dask.compute(count)))
57+
tiling_info = dict(tile_info=dict(num_cells=num_cells,
58+
num_tiles=len(cell_counts), mean=mean, std_dev=std, recommended=rec))
5959

6060
final_info = pc_info | tiling_info
6161
logger.info(json.dumps(final_info, indent=2))

0 commit comments

Comments
 (0)