-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathhooks.py
More file actions
166 lines (124 loc) · 5.84 KB
/
hooks.py
File metadata and controls
166 lines (124 loc) · 5.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
"""Pre and post hooks on agents."""
from __future__ import annotations
__all__ = [
"asset_merge_factory",
"housekeeping_factory",
"merge_assets",
"new_assets_only",
"noop",
"old_assets_only",
"register_final_asset_transform",
"register_initial_asset_transform",
]
from collections.abc import Mapping, MutableMapping
from typing import Callable
from xarray import Dataset
from muse.agents import Agent
from muse.registration import registrator
INITIAL_ASSET_TRANSFORM: MutableMapping[str, Callable] = {}
""" Transform at the start of each step. """
FINAL_ASSET_TRANSFORM: MutableMapping[str, Callable] = {}
""" Transform at the end of each step, including new assets. """
def housekeeping_factory(settings: str | Mapping = "noop") -> Callable:
"""Returns a function for performing initial housekeeping.
For instance, remove technologies with no capacity now or in the future.
Available housekeeping functions should be registered with
:py:func:`@register_initial_asset_transform<register_initial_asset_transform>`.
"""
from muse.agents import AbstractAgent
if isinstance(settings, str):
name = settings
params: Mapping = {}
else:
params = {k: v for k, v in settings.items() if k != "name"}
name = settings["name"]
transform = INITIAL_ASSET_TRANSFORM[name]
def initial_assets_transform(agent: AbstractAgent, assets: Dataset) -> Dataset:
return transform(agent, assets, **params)
return initial_assets_transform
def asset_merge_factory(settings: str | Mapping = "new") -> Callable:
"""Returns a function for merging new investments into assets.
Available merging functions should be registered with
:py:func:`@register_final_asset_transform<register_final_asset_transform>`.
"""
"""Returns a function for performing initial housekeeping.
For instance, remove technologies with no capacity now or in the future.
Available housekeeping functions should be registered with
:py:func:`@register_initial_asset_transform<register_initial_asset_transform>`.
"""
if isinstance(settings, str):
name = settings
params: Mapping = {}
else:
params = {k: v for k, v in settings.items() if k != "name"}
name = settings["name"]
transform = FINAL_ASSET_TRANSFORM[name]
def final_assets_transform(old_assets: Dataset, new_assets):
return transform(old_assets, new_assets, **params)
final_assets_transform.__name__ = name
return final_assets_transform
@registrator(registry=INITIAL_ASSET_TRANSFORM, loglevel="info")
def register_initial_asset_transform(
function: Callable[[Agent, Dataset], Dataset],
) -> Callable:
"""Decorator to register a function for cleaning or transforming assets.
The transformation is applied at the start of each iteration. It any function which
take an agent and assets as input and any number of keyword arguments, and returns
the transformed assets. The agent should not be modified. It is only there to query
the current year, the region, etc.
"""
return function
@registrator(registry=FINAL_ASSET_TRANSFORM, loglevel="info")
def register_final_asset_transform(
function: Callable[[Dataset, Dataset], Dataset],
) -> Callable:
"""Decorator to register a function to merge new investments into current assets.
The transform is applied a the very end of the agent iteration. It can be any
function which takes as input the current set of assets, the new assets, and any
number of keyword arguments. The function must return a "merge" of the two assets.
For instance, the new assets could completely replace the old assets
(:py:func:`new_assets_only`), or they could be summed to the old assets
(:py:func:`merge_assets`).
"""
from functools import wraps
@wraps(function)
def decorated(old_assets: Dataset, new_assets: Dataset) -> Dataset:
result = function(old_assets, new_assets)
# missing values -> NaN -> integers become floats
for variable in set(result.variables).intersection(old_assets.variables):
result[variable] = result[variable].astype(old_assets[variable].dtype)
result = result.drop_vars(set(result.coords) - set(old_assets.coords))
return result
return decorated
@register_initial_asset_transform(name="default")
def noop(agent: Agent, assets: Dataset) -> Dataset:
"""Return assets as they are."""
return assets
@register_final_asset_transform(name="new")
def new_assets_only(old_assets: Dataset, new_assets: Dataset) -> Dataset:
"""Returns newly invested assets and ignores old assets."""
return new_assets
@register_final_asset_transform(name="old")
def old_assets_only(old_assets: Dataset, new_assets: Dataset) -> Dataset:
"""Returns old assets and ignores newly invested assets."""
return old_assets
@register_final_asset_transform(name="merge")
def merge_assets(old_assets: Dataset, new_assets: Dataset) -> Dataset:
"""Adds new assets to old along asset dimension.
New assets are assumed to be nonequivalent to any old_assets. Indeed,
it is expected that the asset dimension does not have coordinates (i.e.
it is a combination of coordinates, such as technology and installation
year).
After merging the new assets, quantities are back-filled along the year
dimension. Further missing values (i.e. future years the old_assets
did not take into account) are set to zero.
"""
from logging import getLogger
from muse.utilities import merge_assets
assert "asset" not in old_assets
if "asset" not in new_assets.dims and "replacement" in new_assets.dims:
new_assets = new_assets.rename(replacement="asset")
if len(new_assets.capacity) == 0:
getLogger(__name__).critical("there are no new assets")
return old_assets
return merge_assets(old_assets, new_assets)