Skip to content

Commit

Permalink
Merge pull request #132 from NREL/dev_geospatial_templates_for_analysis
Browse files Browse the repository at this point in the history
Geospatial AutoTemplating Integration
  • Loading branch information
martin-springer authored Dec 5, 2024
2 parents 2aa7332 + c1883fb commit 4b7ee6d
Show file tree
Hide file tree
Showing 7 changed files with 2,085 additions and 4,121 deletions.
4 changes: 2 additions & 2 deletions docs/source/whatsnew/releases/v0.4.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ v0.4.2 (2024-09-13)

Bug Fixes
---------
* Remove duplicate gid's from `pvdeg.geospatial.elevation_stochastic_downselection`
* Remove duplicate gid's from ``pvdeg.geospatial.elevation_stochastic_downselection``

Tests
-----
* Added a test for Xmin in `test_standards.py` and removed dependency on pvgis.
* Added a test for Xmin in ``test_standards.py`` and removed dependency on pvgis.

Contributors
~~~~~~~~~~~~
Expand Down
8 changes: 5 additions & 3 deletions docs/source/whatsnew/releases/v0.4.3.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
v0.4.3 (2024-10-10)
===================
v0.4.3 (2024-11-1)
=======================

Enhancements
------------
-------------
``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.

Suite of utility functions to facilitate accessing material parameter json files.

* ``pvdeg.utilities.read_material`` creates a public api to replace the private ``pvdeg.untility._read_material`` function (to be deprecated soon)
Expand Down
79 changes: 79 additions & 0 deletions pvdeg/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
Private API, should only be used in PVDeg implemenation files.
"""

import functools
import inspect
import warnings

def geospatial_quick_shape(numeric_or_timeseries: bool, shape_names: list[str]) -> None:
"""
Expand Down Expand Up @@ -57,3 +60,79 @@ def decorator(func):
return func

return decorator

# Taken from: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically
# A future Python version (after 3.13) will include the warnings.deprecated decorator
def deprecated(reason):
"""
This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted
when the function is used.
"""

string_types = (type(b''), type(u''))

if isinstance(reason, string_types):

# The @deprecated is used with a 'reason'.
#
# .. code-block:: python
#
# @deprecated("please, use another function")
# def old_function(x, y):
# pass

def decorator(func1):

if inspect.isclass(func1):
fmt1 = "Call to deprecated class {name} ({reason})."
else:
fmt1 = "Call to deprecated function {name} ({reason})."

@functools.wraps(func1)
def new_func1(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning)
warnings.warn(
fmt1.format(name=func1.__name__, reason=reason),
category=DeprecationWarning,
stacklevel=2
)
warnings.simplefilter('default', DeprecationWarning)
return func1(*args, **kwargs)

return new_func1

return decorator

elif inspect.isclass(reason) or inspect.isfunction(reason):

# The @deprecated is used without any 'reason'.
#
# .. code-block:: python
#
# @deprecated
# def old_function(x, y):
# pass

func2 = reason

if inspect.isclass(func2):
fmt2 = "Call to deprecated class {name}."
else:
fmt2 = "Call to deprecated function {name}."

@functools.wraps(func2)
def new_func2(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning)
warnings.warn(
fmt2.format(name=func2.__name__),
category=DeprecationWarning,
stacklevel=2
)
warnings.simplefilter('default', DeprecationWarning)
return func2(*args, **kwargs)

return new_func2

else:
raise TypeError(repr(type(reason)))
104 changes: 88 additions & 16 deletions pvdeg/geospatial.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
humidity,
letid,
utilities,
decorators,
)

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

def analysis(weather_ds, meta_df, func, template=None, **func_kwargs):
"""
Applies a function to each gid of a weather dataset.
Applies a function to each gid of a weather dataset. `analysis` will attempt to create a template using `geospatial.auto_template`.
If this process fails you will have to provide a geospatial template to the template argument.
ValueError: <function-name> cannot be autotemplated. create a template manually with `geospatial.output_template`
Parameters
----------
Expand All @@ -212,8 +218,10 @@ def analysis(weather_ds, meta_df, func, template=None, **func_kwargs):
"""

if template is None:
param = template_parameters(func)
template = output_template(weather_ds, **param)
template = auto_template(
func=func,
ds_gids=weather_ds
)

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


def output_template(
ds_gids, shapes, attrs=dict(), global_attrs=dict(), add_dims=dict()
ds_gids: xr.Dataset,
shapes: dict,
attrs=dict(),
global_attrs=dict(),
add_dims=dict()
):
"""
Generates a xarray template for output data. Output variables and
associated dimensions need to be specified via the shapes dictionary.
The dimension length are derived from the input data. Additonal output
dimensions can be defined with the add_dims argument.
Examples
--------
Providing the shapes dictionary can be confusing. Here is what the `shapes` dictionary should look like for `pvdeg.standards.standoff`.
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.
.. code-block:: python
shapes = {
"x": ("gid",),
"T98_inf": ("gid",),
"T98_0": ("gid",),
}
**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.**
This is what the shapes dictinoary should look like for `pvdeg.humidity.module`. Refering to the docstring,
we can see that the function will return a timeseries result for each location. This means we need dimensions of "gid" and "time".
.. code-block:: python
shapes = {
"RH_surface_outside": ("gid", "time"),
"RH_front_encap": ("gid", "time"),
"RH_back_encap": ("gid", "time"),
"RH_backsheet": ("gid", "time"),
}
Parameters
----------
ds_gids : xarray.Dataset
Expand Down Expand Up @@ -290,9 +327,9 @@ def output_template(
return output_template


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

return res

def can_auto_template(func) -> None:
"""
Check if we can use `geospatial.auto_template on a given function.
Raise an error if the function was not declared with the `@geospatial_quick_shape` decorator.
No error raised if we can run `geospatial.auto_template` on provided function, `func`.
Parameters
----------
func: callable
function to create template from.
Returns
-------
None
"""
if not (hasattr(func, "numeric_or_timeseries") and hasattr(func, "shape_names")):
raise ValueError(
f"{func.__name__} cannot be autotemplated. create a template manually"
)



def auto_template(func: Callable, ds_gids: xr.Dataset) -> xr.Dataset:
"""
Automatically create a template for a target function: `func`.
Only works on functions that have the `numeric_or_timeseries` and `shape_names` attributes.
These attributes are assigned at function definition with the `@geospatial_quick_shape` decorator.
Otherwise you will have to create your own template.
Don't worry, this is easy. See the Geospatial Templates Notebook
for more information.
Otherwise you will have to create your own template using `geospatial.output_template`.
See the Geospatial Templates Notebook for more information.
examples:
Examples
---------
the function returns a numeric value
Expand All @@ -430,17 +488,31 @@ def auto_template(func: Callable, ds_gids: xr.Dataset) -> xr.Dataset:
the function returns a timeseries result
>>> pvdeg.module.humidity
counter example:
Counter example:
----------------
the function could either return a single numeric or a series based on changed in
the input. Because it does not have a known result shape we cannot determine the
attributes required for autotemplating ahead of time.
Parameters
----------
func: callable
function to create template from. This will raise an error if the function was not declared with the `@geospatial_quick_shape` decorator.
ds_gids : xarray.Dataset
Dataset containing the gids and their associated dimensions. (geospatial weather dataset)
Dataset should already be chunked.
Returns
-------
output_template : xarray.Dataset
Template for output data.
"""

if not (hasattr(func, "numeric_or_timeseries") and hasattr(func, "shape_names")):
raise ValueError(
f"{func.__name__} cannot be autotemplated. create a template manually"
)
can_auto_template(func=func)
# if not (hasattr(func, "numeric_or_timeseries") and hasattr(func, "shape_names")):
# raise ValueError(
# f"{func.__name__} cannot be autotemplated. create a template manually"
# )

if func.numeric_or_timeseries == 0:
shapes = {datavar: ("gid",) for datavar in func.shape_names}
Expand Down
Loading

0 comments on commit 4b7ee6d

Please sign in to comment.