dwind 0.3.1__py3-none-any.whl → 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dwind/__init__.py +1 -1
- dwind/btm_sizing.py +1 -2
- dwind/cli/__init__.py +0 -0
- dwind/cli/collect.py +114 -0
- dwind/cli/debug.py +137 -0
- dwind/cli/run.py +288 -0
- dwind/cli/utils.py +166 -0
- dwind/config.py +147 -6
- dwind/main.py +20 -0
- dwind/model.py +128 -63
- dwind/mp.py +30 -35
- dwind/resource.py +120 -41
- dwind/scenarios.py +73 -36
- dwind/utils/array.py +16 -89
- dwind/utils/hpc.py +44 -2
- dwind/utils/loader.py +63 -0
- dwind/utils/progress.py +60 -0
- dwind/valuation.py +368 -239
- {dwind-0.3.1.dist-info → dwind-0.3.2.dist-info}/METADATA +2 -1
- dwind-0.3.2.dist-info/RECORD +28 -0
- dwind-0.3.2.dist-info/entry_points.txt +2 -0
- dwind-0.3.1.dist-info/RECORD +0 -20
- dwind-0.3.1.dist-info/entry_points.txt +0 -2
- {dwind-0.3.1.dist-info → dwind-0.3.2.dist-info}/WHEEL +0 -0
- {dwind-0.3.1.dist-info → dwind-0.3.2.dist-info}/licenses/LICENSE.txt +0 -0
- {dwind-0.3.1.dist-info → dwind-0.3.2.dist-info}/top_level.txt +0 -0
dwind/cli/utils.py
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
"""Provides shared utilities among the CLI subcommand modules."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
import typer
|
8
|
+
import pandas as pd
|
9
|
+
from rich.style import Style
|
10
|
+
from rich.table import Table
|
11
|
+
from rich.console import Console
|
12
|
+
|
13
|
+
from dwind.config import Year
|
14
|
+
|
15
|
+
|
16
|
+
DWIND = Path("/projects/dwind/agents")
|
17
|
+
|
18
|
+
console = Console()
|
19
|
+
|
20
|
+
|
21
|
+
def year_callback(ctx: typer.Context, param: typer.CallbackParam, value: int):
|
22
|
+
"""Typer helper to validate the year input.
|
23
|
+
|
24
|
+
Parameters
|
25
|
+
----------
|
26
|
+
ctx : typer.Context
|
27
|
+
The Typer context.
|
28
|
+
param : typer.CallbackParam
|
29
|
+
The Typer parameter.
|
30
|
+
value : int
|
31
|
+
User input for the analysis year basis, must be one of 2022, 2024, or 2025.
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
-------
|
35
|
+
int
|
36
|
+
The input :py:param:`value`, if it is a valid input.
|
37
|
+
|
38
|
+
Raises:
|
39
|
+
------
|
40
|
+
typer.BadParameter
|
41
|
+
Raised if the input is not one of 2022, 2024, or 2025.
|
42
|
+
"""
|
43
|
+
if ctx.resilient_parsing:
|
44
|
+
return
|
45
|
+
try:
|
46
|
+
year = Year(value)
|
47
|
+
except ValueError as e:
|
48
|
+
raise typer.BadParameter(
|
49
|
+
f"Only {Year.values()} are valid options for `year`, not {value}."
|
50
|
+
) from e
|
51
|
+
return year
|
52
|
+
|
53
|
+
|
54
|
+
def load_agents(
|
55
|
+
file_name: Path | None = None,
|
56
|
+
location: str | None = None,
|
57
|
+
sector: str | None = None,
|
58
|
+
model_config: str | Path | None = None,
|
59
|
+
*,
|
60
|
+
prepare: bool = False,
|
61
|
+
) -> pd.DataFrame:
|
62
|
+
"""Load the agent file based on a filename or the location and sector to a Pandas DataFrame,
|
63
|
+
and return the data frame.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
file_name (Path | None, optional): Name of the agent file, if not auto-generating from
|
67
|
+
the :py:attr:`location` and :py:attr:`sector` inputs. Defaults to None.
|
68
|
+
location (str | None, optional): The name of the location or grouping, such as
|
69
|
+
"colorado_larimer" or "priority1". Defaults to None.
|
70
|
+
sector (str | None, optional): The name of the section. Must be one of "btm" or "fom".
|
71
|
+
Defaults to None.
|
72
|
+
model_config: (str | Path, optional): The file name and path for the overarching model
|
73
|
+
configuration containing SQL and file references, scenario configurations, and etc.
|
74
|
+
Defaults to None.
|
75
|
+
prepare (bool, optional): True if loading pre-chunked and prepared agent data, which should
|
76
|
+
bypass the standard column checking for additional joins, by default False.
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
pd.DataFrame: The agent DataFrame.
|
80
|
+
"""
|
81
|
+
from dwind.model import Agents
|
82
|
+
|
83
|
+
if file_name is None and (location is None or sector is None):
|
84
|
+
raise ValueError("One of `file_name` or `location` and `sector` must be provided.")
|
85
|
+
|
86
|
+
f_agents = (
|
87
|
+
file_name if file_name is not None else DWIND / f"{location}/agents_dwind_{sector}.parquet"
|
88
|
+
)
|
89
|
+
if not isinstance(f_agents, Path):
|
90
|
+
f_agents = Path(f_agents).resolve()
|
91
|
+
|
92
|
+
alternative_suffix = (".pqt", ".parquet", ".pkl", ".pickle", ".csv")
|
93
|
+
base_style = Style.parse("cyan")
|
94
|
+
if not f_agents.exists():
|
95
|
+
for suffix in alternative_suffix:
|
96
|
+
if (new_fn := f_agents.with_suffix(suffix)).exists():
|
97
|
+
if new_fn != f_agents:
|
98
|
+
msg = (
|
99
|
+
f"Using alternative agent file: {new_fn}\n\t"
|
100
|
+
f"Requested agent file: {f_agents}"
|
101
|
+
)
|
102
|
+
console.print(msg, style=base_style)
|
103
|
+
f_agents = new_fn
|
104
|
+
break
|
105
|
+
|
106
|
+
if prepare:
|
107
|
+
return Agents.load_and_prepare_agents(
|
108
|
+
agent_file=f_agents, sector=sector, model_config=model_config
|
109
|
+
)
|
110
|
+
return Agents.load_agents(agent_file=f_agents)
|
111
|
+
|
112
|
+
|
113
|
+
def print_status_table(
|
114
|
+
summary: pd.DataFrame,
|
115
|
+
id_col: str = "job",
|
116
|
+
chunk_col: str | None = None,
|
117
|
+
status_col: str = "status",
|
118
|
+
) -> None:
|
119
|
+
"""Prints a nicely formatted pandas DataFrame to the console.
|
120
|
+
|
121
|
+
Args:
|
122
|
+
summary (pd.DataFrame): Summary DataFrame of the job (:py:attr:`id_col`), chunk
|
123
|
+
(:py:attr:`chunk_col`), and status (:py:attr:`status_col`).
|
124
|
+
id_col (str): Name of the job ID column.
|
125
|
+
chunk_col (str): Name of the chunk index column.
|
126
|
+
status_col (str): Name of the final job status column.
|
127
|
+
"""
|
128
|
+
table = Table()
|
129
|
+
table.add_column("Job ID")
|
130
|
+
if chunk_col is not None:
|
131
|
+
table.add_column("Agent Chunk")
|
132
|
+
table.add_column("Status")
|
133
|
+
|
134
|
+
if chunk_col is None:
|
135
|
+
cols = [id_col, status_col]
|
136
|
+
secondary_sort = status_col
|
137
|
+
else:
|
138
|
+
cols = [id_col, chunk_col, status_col]
|
139
|
+
secondary_sort = chunk_col
|
140
|
+
summary = summary[cols].astype({c: str for c in cols})
|
141
|
+
for _, row in summary.sort_values([status_col, secondary_sort]).iterrows():
|
142
|
+
table.add_row(*row.tolist())
|
143
|
+
|
144
|
+
console.print(table)
|
145
|
+
|
146
|
+
|
147
|
+
def cleanup_chunks(dir_out: str | None, which: str) -> None:
|
148
|
+
"""Deletes the temporary agent chunk files, typically in
|
149
|
+
``/path/to/analysis_scenario/chunk_files/agents``, and of the form ``agents_{ix}.pqt``.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
dir_out (str | None): The same :py:attr:`dir_out` passed to the run command. The
|
153
|
+
``chunk_files/agent_chunks`` will be automatically added to the path.
|
154
|
+
which (str): One of "agents" or "results" to indicate which chunked files should be deleted.
|
155
|
+
"""
|
156
|
+
dir_out = Path.cwd() if dir_out is None else Path(dir_out).resolve()
|
157
|
+
out_path = dir_out / "chunk_files"
|
158
|
+
if which == "agents":
|
159
|
+
out_path = out_path / "agent_chunks"
|
160
|
+
elif which != "results":
|
161
|
+
raise ValueError("`which` must be one 'agents' or 'results'.")
|
162
|
+
|
163
|
+
for f in out_path.iterdir():
|
164
|
+
if f.suffix == ".pqt":
|
165
|
+
f.unlink()
|
166
|
+
print(f"Removed: {f}")
|
dwind/config.py
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
"""Custom configuration data class to allow for dictionary style and dot notation calling of
|
2
|
-
attributes.
|
2
|
+
attributes and Enums for validating configuration data.
|
3
3
|
"""
|
4
4
|
|
5
5
|
from __future__ import annotations
|
6
6
|
|
7
7
|
import re
|
8
8
|
import sys
|
9
|
+
from enum import Enum, IntEnum
|
10
|
+
from typing import Any, Annotated
|
9
11
|
from pathlib import Path
|
10
12
|
|
11
13
|
|
@@ -17,54 +19,193 @@ else:
|
|
17
19
|
# fmt: on
|
18
20
|
|
19
21
|
|
22
|
+
class ValuesMixin:
|
23
|
+
"""Mixin class providing `.values()` for Enum classes."""
|
24
|
+
|
25
|
+
@staticmethod
|
26
|
+
def values() -> list[str]:
|
27
|
+
"""Generate a list of the valid input strings."""
|
28
|
+
return [*Sector._value2member_map_]
|
29
|
+
|
30
|
+
|
31
|
+
class Sector(str, ValuesMixin, Enum):
|
32
|
+
"""Enum validator for sector inputs."""
|
33
|
+
|
34
|
+
FOM: Annotated[str, "Front-of-meter"] = "fom"
|
35
|
+
BTM: Annotated[str, "Behind-the-meter"] = "btm"
|
36
|
+
|
37
|
+
|
38
|
+
class Technology(str, ValuesMixin, Enum):
|
39
|
+
"""Enum validator for technology inputs."""
|
40
|
+
|
41
|
+
WIND: Annotated[str, "Wind generation model"] = "wind"
|
42
|
+
SOLAR: Annotated[str, "Solar generation model"] = "solar"
|
43
|
+
|
44
|
+
|
45
|
+
class Scenario(str, ValuesMixin, Enum):
|
46
|
+
"""Enum validator for the scenario to run."""
|
47
|
+
|
48
|
+
BASELINE: Annotated[str, "Standard scenario to compare alternatives."] = "baseline"
|
49
|
+
METERING: Annotated[str, "TODO."] = "metering"
|
50
|
+
BILLING: Annotated[str, "TODO."] = "billing"
|
51
|
+
HIGHRECOST: Annotated[str, "High renewable adoption"] = "highrecost"
|
52
|
+
RE100: Annotated[str, "100% renewable adoption"] = "highrecost"
|
53
|
+
LOWRECOST: Annotated[str, "Low renewable adoption"] = "lowrecost"
|
54
|
+
|
55
|
+
|
56
|
+
class Year(ValuesMixin, IntEnum):
|
57
|
+
"""Enum validator for analysis year."""
|
58
|
+
|
59
|
+
_2022 = 2022
|
60
|
+
_2025 = 2025
|
61
|
+
_2035 = 2035
|
62
|
+
_2040 = 2040
|
63
|
+
|
64
|
+
|
65
|
+
class Optimization(str, ValuesMixin, Enum):
|
66
|
+
"""Enum validator for breakeven cost optimization strategies."""
|
67
|
+
|
68
|
+
BISECT = "bisect"
|
69
|
+
BRENTQ = "brentq"
|
70
|
+
GRID_SEARCH = "grid_search"
|
71
|
+
NEWTON = "newton"
|
72
|
+
|
73
|
+
|
74
|
+
class CRBModel(Enum):
|
75
|
+
"""Convert between integers and "crb_model" data for efficient storage and retrieval."""
|
76
|
+
|
77
|
+
full_service_restaurant = 0
|
78
|
+
hospital = 1
|
79
|
+
large_hotel = 2
|
80
|
+
large_office = 3
|
81
|
+
medium_office = 4
|
82
|
+
midrise_apartment = 5
|
83
|
+
out_patient = 6
|
84
|
+
primary_school = 7
|
85
|
+
quick_service_restaurant = 8
|
86
|
+
reference = 9
|
87
|
+
secondary_school = 10
|
88
|
+
small_hotel = 11
|
89
|
+
small_office = 12
|
90
|
+
stand_alone_retail = 13
|
91
|
+
strip_mall = 14
|
92
|
+
supermarket = 15
|
93
|
+
warehouse = 16
|
94
|
+
|
95
|
+
@staticmethod
|
96
|
+
def model_map() -> dict[str, int]:
|
97
|
+
"""Create a dictionary of name: int values for each crb model."""
|
98
|
+
return {el.name: el.value for el in CRBModel}
|
99
|
+
|
100
|
+
@staticmethod
|
101
|
+
def str_model_map() -> dict[str, str]:
|
102
|
+
"""Create a dictionary of name: str(int) values for each crb model."""
|
103
|
+
return {el.name: str(el.value) for el in CRBModel}
|
104
|
+
|
105
|
+
@staticmethod
|
106
|
+
def int_map() -> dict[str, int]:
|
107
|
+
"""Create a dictionary of int: name for each crb model."""
|
108
|
+
return {el.value: el.name for el in CRBModel}
|
109
|
+
|
110
|
+
|
20
111
|
class Mapping(dict):
|
21
112
|
"""Dict-like class that allows for the use of dictionary style attribute calls on class
|
22
113
|
attributes.
|
23
114
|
"""
|
24
115
|
|
25
|
-
def __setitem__(self, key, item):
|
116
|
+
def __setitem__(self, key: Any, item: Any):
|
117
|
+
"""Creates a new key, value pair in :py:attr:`__dict__`.
|
118
|
+
|
119
|
+
Args:
|
120
|
+
key (Any): A hashable dictionary key.
|
121
|
+
item (Any): A value to be retrieved when the :py:attr:`key` is called.
|
122
|
+
"""
|
26
123
|
self.__dict__[key] = item
|
27
124
|
|
28
|
-
def __getitem__(self, key):
|
125
|
+
def __getitem__(self, key: Any) -> Any:
|
126
|
+
"""Retrieve :py:attr:`key`'s value from :py:attr:`__dict__`.
|
127
|
+
|
128
|
+
Args:
|
129
|
+
key (Any): An existing key in :py:attr:`__dict__`.
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
Any: The value paired to :py:attr:`key`.
|
133
|
+
"""
|
29
134
|
return self.__dict__[key]
|
30
135
|
|
31
136
|
def __repr__(self):
|
137
|
+
"""Returns the ``repr(self.__dict__)``."""
|
32
138
|
return repr(self.__dict__)
|
33
139
|
|
34
|
-
def __len__(self):
|
140
|
+
def __len__(self) -> int:
|
141
|
+
"""Returns the number of keys in :py:attr:`__dict__`.
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
int: The number of keys in :py:attr:`__dict__`.
|
145
|
+
"""
|
35
146
|
return len(self.__dict__)
|
36
147
|
|
37
|
-
def __delitem__(self, key):
|
148
|
+
def __delitem__(self, key: Any):
|
149
|
+
"""Delete's :py:attr:`key` from :py:attr:`__dict__`."""
|
38
150
|
del self.__dict__[key]
|
39
151
|
|
40
152
|
def clear(self):
|
153
|
+
"""Deletes all entries in :py:attr:`__dict__`."""
|
41
154
|
return self.__dict__.clear()
|
42
155
|
|
43
|
-
def copy(self):
|
156
|
+
def copy(self) -> dict:
|
157
|
+
"""Returns an unlinked copy of :py:attr:`__dict__`."""
|
44
158
|
return self.__dict__.copy()
|
45
159
|
|
46
160
|
def update(self, *args, **kwargs):
|
161
|
+
"""Updates the provided args and keyword arguments of :py:attr:`__dict__`.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
*args: Variable length argument list.
|
165
|
+
**kwargs: Arbitrary keyword arguments.
|
166
|
+
"""
|
47
167
|
return self.__dict__.update(*args, **kwargs)
|
48
168
|
|
49
169
|
def keys(self):
|
170
|
+
"""Returns the keys of :py:attr:`__dict__`."""
|
50
171
|
return self.__dict__.keys()
|
51
172
|
|
52
173
|
def values(self):
|
174
|
+
"""Returns the :py:attr:`__dict__` values."""
|
53
175
|
return self.__dict__.values()
|
54
176
|
|
55
177
|
def items(self):
|
178
|
+
"""Returns the keys and values of :py:attr:`__dict__`."""
|
56
179
|
return self.__dict__.items()
|
57
180
|
|
58
181
|
def pop(self, *args):
|
182
|
+
"""Removes and returns the desired argments from :py:attr:`__dict__` if they exist.
|
183
|
+
|
184
|
+
Args:
|
185
|
+
*args: Variable length argument list.
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
Any: values of :py:attr:`__dict__` from keys :py:attr:`*args`.
|
189
|
+
"""
|
59
190
|
return self.__dict__.pop(*args)
|
60
191
|
|
61
192
|
def __cmp__(self, dict_):
|
193
|
+
"""Compares :py:attr:`_dict` to :py:attr:`__dict__`.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
dict_ (dict): Dictionary for object comparison.
|
197
|
+
|
198
|
+
Returns:
|
199
|
+
bool: Result of the comparison between :py:attr:`_dict` and :py:attr:`__dict__`.
|
200
|
+
"""
|
62
201
|
return self.__cmp__(self.__dict__, dict_)
|
63
202
|
|
64
203
|
def __contains__(self, item):
|
204
|
+
"""Checks if :py:attr:`item` is in :py:attr:`__dict__`."""
|
65
205
|
return item in self.__dict__
|
66
206
|
|
67
207
|
def __iter__(self):
|
208
|
+
"""Custom iterator return the :py:attr:`__dict__`."""
|
68
209
|
return iter(self.__dict__)
|
69
210
|
|
70
211
|
|
dwind/main.py
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
"""Enables running dwind as a CLI tool, the primary interface for working with dwind."""
|
2
|
+
|
3
|
+
import typer
|
4
|
+
|
5
|
+
from dwind.cli import run, debug, collect
|
6
|
+
|
7
|
+
|
8
|
+
app = typer.Typer()
|
9
|
+
|
10
|
+
app.add_typer(
|
11
|
+
run.app, name="run", help="Run a dwind analysis via sbatch or in an interactive session."
|
12
|
+
)
|
13
|
+
|
14
|
+
app.add_typer(debug.app, name="debug", help="Help identify issues with analyses that were run.")
|
15
|
+
|
16
|
+
app.add_typer(collect.app, name="collect", help="Gather results from a run or rerun analysis.")
|
17
|
+
|
18
|
+
|
19
|
+
if __name__ == "__main__":
|
20
|
+
app()
|