Skip to content

Commit 4b7ee6d

Browse files
Merge pull request #132 from NREL/dev_geospatial_templates_for_analysis
Geospatial AutoTemplating Integration
2 parents 2aa7332 + c1883fb commit 4b7ee6d

File tree

7 files changed

+2085
-4121
lines changed

7 files changed

+2085
-4121
lines changed

docs/source/whatsnew/releases/v0.4.2.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ v0.4.2 (2024-09-13)
44

55
Bug Fixes
66
---------
7-
* Remove duplicate gid's from `pvdeg.geospatial.elevation_stochastic_downselection`
7+
* Remove duplicate gid's from ``pvdeg.geospatial.elevation_stochastic_downselection``
88

99
Tests
1010
-----
11-
* Added a test for Xmin in `test_standards.py` and removed dependency on pvgis.
11+
* Added a test for Xmin in ``test_standards.py`` and removed dependency on pvgis.
1212

1313
Contributors
1414
~~~~~~~~~~~~

docs/source/whatsnew/releases/v0.4.3.rst

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
v0.4.3 (2024-10-10)
2-
===================
1+
v0.4.3 (2024-11-1)
2+
=======================
33

44
Enhancements
5-
------------
5+
-------------
6+
``pvdeg.geospatial.analysis`` implements autotemplating. No need to specify a template for common ``pvdeg`` functions during analysis. Manually creating and providing templates is still an option. Docstrings updated with examples.
7+
68
Suite of utility functions to facilitate accessing material parameter json files.
79

810
* ``pvdeg.utilities.read_material`` creates a public api to replace the private ``pvdeg.untility._read_material`` function (to be deprecated soon)

pvdeg/decorators.py

+79
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
Private API, should only be used in PVDeg implemenation files.
44
"""
55

6+
import functools
7+
import inspect
8+
import warnings
69

710
def geospatial_quick_shape(numeric_or_timeseries: bool, shape_names: list[str]) -> None:
811
"""
@@ -57,3 +60,79 @@ def decorator(func):
5760
return func
5861

5962
return decorator
63+
64+
# Taken from: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically
65+
# A future Python version (after 3.13) will include the warnings.deprecated decorator
66+
def deprecated(reason):
67+
"""
68+
This is a decorator which can be used to mark functions
69+
as deprecated. It will result in a warning being emitted
70+
when the function is used.
71+
"""
72+
73+
string_types = (type(b''), type(u''))
74+
75+
if isinstance(reason, string_types):
76+
77+
# The @deprecated is used with a 'reason'.
78+
#
79+
# .. code-block:: python
80+
#
81+
# @deprecated("please, use another function")
82+
# def old_function(x, y):
83+
# pass
84+
85+
def decorator(func1):
86+
87+
if inspect.isclass(func1):
88+
fmt1 = "Call to deprecated class {name} ({reason})."
89+
else:
90+
fmt1 = "Call to deprecated function {name} ({reason})."
91+
92+
@functools.wraps(func1)
93+
def new_func1(*args, **kwargs):
94+
warnings.simplefilter('always', DeprecationWarning)
95+
warnings.warn(
96+
fmt1.format(name=func1.__name__, reason=reason),
97+
category=DeprecationWarning,
98+
stacklevel=2
99+
)
100+
warnings.simplefilter('default', DeprecationWarning)
101+
return func1(*args, **kwargs)
102+
103+
return new_func1
104+
105+
return decorator
106+
107+
elif inspect.isclass(reason) or inspect.isfunction(reason):
108+
109+
# The @deprecated is used without any 'reason'.
110+
#
111+
# .. code-block:: python
112+
#
113+
# @deprecated
114+
# def old_function(x, y):
115+
# pass
116+
117+
func2 = reason
118+
119+
if inspect.isclass(func2):
120+
fmt2 = "Call to deprecated class {name}."
121+
else:
122+
fmt2 = "Call to deprecated function {name}."
123+
124+
@functools.wraps(func2)
125+
def new_func2(*args, **kwargs):
126+
warnings.simplefilter('always', DeprecationWarning)
127+
warnings.warn(
128+
fmt2.format(name=func2.__name__),
129+
category=DeprecationWarning,
130+
stacklevel=2
131+
)
132+
warnings.simplefilter('default', DeprecationWarning)
133+
return func2(*args, **kwargs)
134+
135+
return new_func2
136+
137+
else:
138+
raise TypeError(repr(type(reason)))

pvdeg/geospatial.py

+88-16
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
humidity,
88
letid,
99
utilities,
10+
decorators,
1011
)
1112

1213
import xarray as xr
@@ -190,7 +191,12 @@ def calc_block(weather_ds_block, future_meta_df, func, func_kwargs):
190191

191192
def analysis(weather_ds, meta_df, func, template=None, **func_kwargs):
192193
"""
193-
Applies a function to each gid of a weather dataset.
194+
Applies a function to each gid of a weather dataset. `analysis` will attempt to create a template using `geospatial.auto_template`.
195+
If this process fails you will have to provide a geospatial template to the template argument.
196+
197+
ValueError: <function-name> cannot be autotemplated. create a template manually with `geospatial.output_template`
198+
199+
194200
195201
Parameters
196202
----------
@@ -212,8 +218,10 @@ def analysis(weather_ds, meta_df, func, template=None, **func_kwargs):
212218
"""
213219

214220
if template is None:
215-
param = template_parameters(func)
216-
template = output_template(weather_ds, **param)
221+
template = auto_template(
222+
func=func,
223+
ds_gids=weather_ds
224+
)
217225

218226
# future_meta_df = client.scatter(meta_df)
219227
kwargs = {"func": func, "future_meta_df": meta_df, "func_kwargs": func_kwargs}
@@ -241,14 +249,43 @@ def analysis(weather_ds, meta_df, func, template=None, **func_kwargs):
241249

242250

243251
def output_template(
244-
ds_gids, shapes, attrs=dict(), global_attrs=dict(), add_dims=dict()
252+
ds_gids: xr.Dataset,
253+
shapes: dict,
254+
attrs=dict(),
255+
global_attrs=dict(),
256+
add_dims=dict()
245257
):
246258
"""
247259
Generates a xarray template for output data. Output variables and
248260
associated dimensions need to be specified via the shapes dictionary.
249261
The dimension length are derived from the input data. Additonal output
250262
dimensions can be defined with the add_dims argument.
251263
264+
Examples
265+
--------
266+
Providing the shapes dictionary can be confusing. Here is what the `shapes` dictionary should look like for `pvdeg.standards.standoff`.
267+
Refer to the docstring, the function will have one result per location so the only dimension for each return value is "gid", a geospatial ID number.
268+
269+
.. code-block:: python
270+
shapes = {
271+
"x": ("gid",),
272+
"T98_inf": ("gid",),
273+
"T98_0": ("gid",),
274+
}
275+
276+
**Note: The dimensions are stored in a tuple, this this why all of the parenthesis have commas after the single string, otherwise python will interpret the value as a string.**
277+
278+
This is what the shapes dictinoary should look like for `pvdeg.humidity.module`. Refering to the docstring,
279+
we can see that the function will return a timeseries result for each location. This means we need dimensions of "gid" and "time".
280+
281+
.. code-block:: python
282+
shapes = {
283+
"RH_surface_outside": ("gid", "time"),
284+
"RH_front_encap": ("gid", "time"),
285+
"RH_back_encap": ("gid", "time"),
286+
"RH_backsheet": ("gid", "time"),
287+
}
288+
252289
Parameters
253290
----------
254291
ds_gids : xarray.Dataset
@@ -290,9 +327,9 @@ def output_template(
290327
return output_template
291328

292329

293-
# we should be able to get rid of this with the new autotemplating function and decorator
294-
# this is helpful for users so we should move it to a section in the documenation,
295-
# discuss with group
330+
# This has been replaced with pvdeg.geospatial.auto_templates inside of pvdeg.geospatial.analysis.
331+
# it is here for completeness. it can be removed.
332+
@decorators.deprecated(reason="use geospatial.auto_template or create a template with geospatial.output_template")
296333
def template_parameters(func):
297334
"""
298335
Output parameters for xarray template.
@@ -410,18 +447,39 @@ def zero_template(
410447

411448
return res
412449

450+
def can_auto_template(func) -> None:
451+
"""
452+
Check if we can use `geospatial.auto_template on a given function.
453+
454+
Raise an error if the function was not declared with the `@geospatial_quick_shape` decorator.
455+
No error raised if we can run `geospatial.auto_template` on provided function, `func`.
456+
457+
Parameters
458+
----------
459+
func: callable
460+
function to create template from.
461+
462+
Returns
463+
-------
464+
None
465+
"""
466+
if not (hasattr(func, "numeric_or_timeseries") and hasattr(func, "shape_names")):
467+
raise ValueError(
468+
f"{func.__name__} cannot be autotemplated. create a template manually"
469+
)
470+
471+
413472

414473
def auto_template(func: Callable, ds_gids: xr.Dataset) -> xr.Dataset:
415474
"""
416475
Automatically create a template for a target function: `func`.
417476
Only works on functions that have the `numeric_or_timeseries` and `shape_names` attributes.
418477
These attributes are assigned at function definition with the `@geospatial_quick_shape` decorator.
419478
420-
Otherwise you will have to create your own template.
421-
Don't worry, this is easy. See the Geospatial Templates Notebook
422-
for more information.
479+
Otherwise you will have to create your own template using `geospatial.output_template`.
480+
See the Geospatial Templates Notebook for more information.
423481
424-
examples:
482+
Examples
425483
---------
426484
427485
the function returns a numeric value
@@ -430,17 +488,31 @@ def auto_template(func: Callable, ds_gids: xr.Dataset) -> xr.Dataset:
430488
the function returns a timeseries result
431489
>>> pvdeg.module.humidity
432490
433-
counter example:
491+
Counter example:
434492
----------------
435493
the function could either return a single numeric or a series based on changed in
436494
the input. Because it does not have a known result shape we cannot determine the
437495
attributes required for autotemplating ahead of time.
496+
497+
Parameters
498+
----------
499+
func: callable
500+
function to create template from. This will raise an error if the function was not declared with the `@geospatial_quick_shape` decorator.
501+
ds_gids : xarray.Dataset
502+
Dataset containing the gids and their associated dimensions. (geospatial weather dataset)
503+
Dataset should already be chunked.
504+
505+
Returns
506+
-------
507+
output_template : xarray.Dataset
508+
Template for output data.
438509
"""
439510

440-
if not (hasattr(func, "numeric_or_timeseries") and hasattr(func, "shape_names")):
441-
raise ValueError(
442-
f"{func.__name__} cannot be autotemplated. create a template manually"
443-
)
511+
can_auto_template(func=func)
512+
# if not (hasattr(func, "numeric_or_timeseries") and hasattr(func, "shape_names")):
513+
# raise ValueError(
514+
# f"{func.__name__} cannot be autotemplated. create a template manually"
515+
# )
444516

445517
if func.numeric_or_timeseries == 0:
446518
shapes = {datavar: ("gid",) for datavar in func.shape_names}

0 commit comments

Comments
 (0)