Skip to content

Commit 1e58f57

Browse files
committed
auto_templating in GeospatialScenario
1 parent a4630af commit 1e58f57

File tree

3 files changed

+176
-39
lines changed

3 files changed

+176
-39
lines changed

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

+29-4
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
@@ -328,6 +329,7 @@ def output_template(
328329

329330
# This has been replaced with pvdeg.geospatial.auto_templates inside of pvdeg.geospatial.analysis.
330331
# 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")
331333
def template_parameters(func):
332334
"""
333335
Output parameters for xarray template.
@@ -445,6 +447,28 @@ def zero_template(
445447

446448
return res
447449

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+
448472

449473
def auto_template(func: Callable, ds_gids: xr.Dataset) -> xr.Dataset:
450474
"""
@@ -484,10 +508,11 @@ def auto_template(func: Callable, ds_gids: xr.Dataset) -> xr.Dataset:
484508
Template for output data.
485509
"""
486510

487-
if not (hasattr(func, "numeric_or_timeseries") and hasattr(func, "shape_names")):
488-
raise ValueError(
489-
f"{func.__name__} cannot be autotemplated. create a template manually"
490-
)
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+
# )
491516

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

pvdeg/scenario.py

+68-35
Original file line numberDiff line numberDiff line change
@@ -1594,49 +1594,88 @@ def set_geospatial_data(self, weather_ds: xr.Dataset, meta_df: pd.DataFrame ) ->
15941594
self.weather_data, self.meta_data = weather_ds, meta_df
15951595

15961596

1597+
# def addJob(
1598+
# self,
1599+
# func: Callable = None,
1600+
# func_params: dict = {},
1601+
# see_added: bool = False,
1602+
# ):
1603+
# """
1604+
# Add a pvdeg function to the scenario pipeline
1605+
1606+
# Parameters:
1607+
# -----------
1608+
# func : function
1609+
# pvdeg function to use for geospatial analysis.
1610+
# *Note: geospatial analysis is only available with a limited subset of pvdeg
1611+
# functions*
1612+
# Current supported functions for geospatial analysis: ``pvdeg.standards.standoff``,
1613+
# ``pvdeg.humidity.module``, ``pvdeg.letid.calc_letid_outdoors``
1614+
# func_params : dict
1615+
# job specific keyword argument dictionary to provide to the function
1616+
# see_added : bool
1617+
# set flag to get a userWarning notifying the user of the job added
1618+
# to the pipeline in method call. ``default = False``
1619+
# """
1620+
# try:
1621+
# pvdeg.geospatial.template_parameters(func)
1622+
# except ValueError:
1623+
# return ValueError(
1624+
# f"{func.__name__} does does not have a valid geospatial results template or does not exist"
1625+
# )
1626+
1627+
# geo_job_dict = {"geospatial_job": {"job": func, "params": func_params}}
1628+
1629+
# self.pipeline = geo_job_dict
1630+
1631+
# if see_added:
1632+
# message = f"{func.__name__} added to pipeline as \n {geo_job_dict}"
1633+
# warnings.warn(message, UserWarning)
1634+
15971635
def addJob(
15981636
self,
1599-
func: Callable = None,
1637+
func: Callable,
1638+
template: xr.Dataset = None,
16001639
func_params: dict = {},
1601-
see_added: bool = False,
1602-
):
1640+
see_added: bool = False
1641+
) -> None:
16031642
"""
1604-
Add a pvdeg function to the scenario pipeline
1643+
Add a pvdeg geospatial function to the scenario pipeline. If no template is provided, `addJob` attempts to use `geospatial.auto_template` this will raise an
16051644
16061645
Parameters:
16071646
-----------
16081647
func : function
1609-
pvdeg function to use for geospatial analysis.
1610-
*Note: geospatial analysis is only available with a limited subset of pvdeg
1611-
functions*
1612-
Current supported functions for geospatial analysis: ``pvdeg.standards.standoff``,
1613-
``pvdeg.humidity.module``, ``pvdeg.letid.calc_letid_outdoors``
1648+
pvdeg function to use for geospatial analysis.
1649+
template : xarray.Dataset
1650+
Template for output data. Only required if a function is not supported by `geospatial.auto_template`.
16141651
func_params : dict
16151652
job specific keyword argument dictionary to provide to the function
16161653
see_added : bool
16171654
set flag to get a userWarning notifying the user of the job added
1618-
to the pipeline in method call. ``default = False``
1655+
to the pipeline in method call. ``default = False``
16191656
"""
1620-
try:
1621-
pvdeg.geospatial.template_parameters(func)
1622-
except ValueError:
1623-
return ValueError(
1624-
f"{func.__name__} does does not have a valid geospatial results template or does not exist"
1625-
)
16261657

1627-
geo_job_dict = {"geospatial_job": {"job": func, "params": func_params}}
1658+
if template is None:
1659+
1660+
# take the weather datapoints specified by metadata and create a template based on them.
1661+
geo_weather_sub = self.weather_data.sel(gid=self.meta_data.index)
1662+
template = pvdeg.geospatial.auto_template(func=func, ds_gids=self.weather_data)
16281663

1629-
self.pipeline = geo_job_dict
1664+
self.template = template
1665+
self.func = func
1666+
self.func_params = func_params
16301667

16311668
if see_added:
1632-
message = f"{func.__name__} added to pipeline as \n {geo_job_dict}"
1669+
message = f"{func.__name__} added to scenario with arguments {func_params} using template: {template}"
16331670
warnings.warn(message, UserWarning)
16341671

1672+
1673+
16351674
def run(self, hpc_worker_conf: Optional[dict] = None) -> None:
16361675
"""
1637-
Run the function in the geospatial pipeline.
1638-
GeospatialScenario only supports one geospatial pipeline job at a time
1639-
unlike Scenario which supports unlimited conventional pipeline jobs.
1676+
Run the geospatial scenario stored in the geospatial scenario object.
1677+
1678+
Only supports one function at a time. Unlike `Scenario` which supports unlimited conventional pipeline jobs.
16401679
Results are stored in the `GeospatialScenario.results` attribute.
16411680
16421681
Creates a dask cluster or client using the hpc_worker_conf parameter.
@@ -1675,20 +1714,14 @@ def run(self, hpc_worker_conf: Optional[dict] = None) -> None:
16751714
"""
16761715
client = pvdeg.geospatial.start_dask(hpc=hpc_worker_conf)
16771716

1678-
geo_weather_sub = self.weather_data.sel(gid=self.meta_data.index)
1679-
1680-
func = self.pipeline["geospatial_job"]["job"]
1681-
1682-
if func == pvdeg.standards.standoff or func == pvdeg.humidity.module:
1683-
geo = {
1684-
"func": func,
1685-
"weather_ds": geo_weather_sub,
1686-
"meta_df": self.meta_data,
1687-
}
1688-
1689-
analysis_result = pvdeg.geospatial.analysis(**geo)
1717+
analysis_result = pvdeg.geospatial.analysis(
1718+
weather_ds=self.weather_data,
1719+
meta_df=self.meta_data,
1720+
func=self.func,
1721+
template=self.template, # provided or generated via autotemplate in GeospatialScenario.addJob
1722+
)
16901723

1691-
self.results = analysis_result
1724+
self.results = analysis_result
16921725

16931726
client.shutdown()
16941727

0 commit comments

Comments
 (0)