dwind 0.3__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 +2 -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 +159 -8
- dwind/loader.py +4 -1
- dwind/main.py +20 -0
- dwind/model.py +265 -99
- dwind/mp.py +61 -61
- dwind/resource.py +122 -40
- dwind/run.py +50 -17
- dwind/scenarios.py +75 -35
- dwind/utils/__init__.py +0 -0
- dwind/utils/array.py +99 -0
- dwind/utils/hpc.py +138 -0
- dwind/utils/loader.py +63 -0
- dwind/utils/progress.py +60 -0
- dwind/valuation.py +396 -290
- {dwind-0.3.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.dist-info/RECORD +0 -17
- dwind-0.3.dist-info/entry_points.txt +0 -2
- {dwind-0.3.dist-info → dwind-0.3.2.dist-info}/WHEEL +0 -0
- {dwind-0.3.dist-info → dwind-0.3.2.dist-info}/licenses/LICENSE.txt +0 -0
- {dwind-0.3.dist-info → dwind-0.3.2.dist-info}/top_level.txt +0 -0
dwind/mp.py
CHANGED
@@ -1,12 +1,19 @@
|
|
1
|
+
"""Provides the :py:class:`MultiProcess` class for running a model on `NREL's Kestrel HPC system`_.
|
2
|
+
|
3
|
+
.. NREL's Kestrel HPC system: https://nrel.github.io/HPC/Documentation/Systems/Kestrel/
|
4
|
+
"""
|
5
|
+
|
1
6
|
from __future__ import annotations
|
2
7
|
|
3
8
|
import time
|
4
9
|
from pathlib import Path
|
5
10
|
|
6
11
|
import pandas as pd
|
12
|
+
from rich.live import Live
|
7
13
|
from rex.utilities.hpc import SLURM
|
8
14
|
|
9
|
-
from dwind.
|
15
|
+
from dwind.utils import hpc
|
16
|
+
from dwind.utils.array import split_by_index
|
10
17
|
|
11
18
|
|
12
19
|
class MultiProcess:
|
@@ -116,7 +123,7 @@ class MultiProcess:
|
|
116
123
|
|
117
124
|
# Create the output directory if it doesn't already exist
|
118
125
|
self.dir_out = Path.cwd() if dir_out is None else Path(self.dir_out).resolve()
|
119
|
-
self.out_path = self.dir_out /
|
126
|
+
self.out_path = self.dir_out / "chunk_files"
|
120
127
|
if not self.out_path.exists():
|
121
128
|
self.out_path.mkdir()
|
122
129
|
|
@@ -127,78 +134,70 @@ class MultiProcess:
|
|
127
134
|
log_dir.mkdir()
|
128
135
|
self.stdout_path = log_dir
|
129
136
|
|
130
|
-
def check_status(self, job_ids: list[int]):
|
137
|
+
def check_status(self, job_ids: list[int], start_time: float):
|
131
138
|
"""Prints the status of all :py:attr:`jobs` submitted.
|
132
139
|
|
133
140
|
Parameters
|
134
141
|
----------
|
135
142
|
job_ids : list[int]
|
136
143
|
The list of HPC ``job_id``s to check on.
|
144
|
+
start_time : float
|
145
|
+
The results of initial ``time.perf_counter()``.
|
137
146
|
"""
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
def aggregate_outputs(self):
|
159
|
-
"""Collect the chunked results files, combine them into a single output parquet file, and
|
160
|
-
delete the chunked results files.
|
161
|
-
"""
|
162
|
-
result_files = [f for f in self.out_path.iterdir() if f.suffix in (".pickle", ".pkl")]
|
163
|
-
|
164
|
-
if len(result_files) > 0:
|
165
|
-
result_agents = pd.concat([pd.read_pickle(f) for f in result_files])
|
166
|
-
f_out = self.dir_out / f"run_{self.run_name}.pqt"
|
167
|
-
result_agents.to_parquet(f_out)
|
168
|
-
|
169
|
-
for f in result_files:
|
170
|
-
f.unlink()
|
171
|
-
|
172
|
-
def run_jobs(self, agent_df: pd.DataFrame) -> None:
|
147
|
+
slurm = SLURM()
|
148
|
+
job_status = {
|
149
|
+
j: {
|
150
|
+
"status": slurm.check_status(job_id=j),
|
151
|
+
"start": start_time,
|
152
|
+
"wait": time.perf_counter() - start_time,
|
153
|
+
"run": 0,
|
154
|
+
}
|
155
|
+
for j in job_ids
|
156
|
+
}
|
157
|
+
table, complete = hpc.generate_run_status_table(job_status)
|
158
|
+
with Live(table, refresh_per_second=1) as live:
|
159
|
+
while not complete:
|
160
|
+
time.sleep(5)
|
161
|
+
job_status |= hpc.update_status(job_status)
|
162
|
+
table, complete = hpc.generate_run_status_table(job_status)
|
163
|
+
live.update(table)
|
164
|
+
|
165
|
+
def run_jobs(self, agent_df: pd.DataFrame) -> dict[str, int]:
|
173
166
|
"""Run :py:attr:`n_jobs` number of jobs for the :py:attr:`agent_df`.
|
174
167
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
168
|
+
Args:
|
169
|
+
agent_df (pandas.DataFrame): The agent DataFrame to be chunked and analyzed.
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
dict[str, int]: Dictionary mapping of each SLURM job id to the chunk run in that job.
|
179
173
|
"""
|
180
174
|
agent_df = agent_df.reset_index(drop=True)
|
181
175
|
# chunks = np.array_split(agent_df, self.n_nodes)
|
182
176
|
starts, ends = split_by_index(agent_df, self.n_nodes)
|
183
|
-
|
184
|
-
|
185
|
-
base_cmd_str = f"module load conda; conda activate {self.env};
|
186
|
-
base_cmd_str += "dwind run
|
187
|
-
|
188
|
-
base_args = f"
|
189
|
-
base_args += f"
|
190
|
-
base_args += f"
|
191
|
-
base_args += f"
|
192
|
-
base_args += f"
|
193
|
-
base_args += f"
|
194
|
-
base_args += f"
|
195
|
-
|
196
|
-
|
197
|
-
|
177
|
+
job_chunk_map = {}
|
178
|
+
|
179
|
+
base_cmd_str = f"module load conda; conda activate {self.env};"
|
180
|
+
base_cmd_str += " dwind run chunk"
|
181
|
+
|
182
|
+
base_args = f" {self.location}"
|
183
|
+
base_args += f" {self.sector}"
|
184
|
+
base_args += f" {self.scenario}"
|
185
|
+
base_args += f" {self.year}"
|
186
|
+
base_args += f" {self.out_path}"
|
187
|
+
base_args += f" {self.repository}"
|
188
|
+
base_args += f" {self.model_config}"
|
189
|
+
|
190
|
+
if not (agent_path := self.out_path / "agent_chunks").is_dir():
|
191
|
+
agent_path.mkdir()
|
192
|
+
|
193
|
+
start_time = time.perf_counter()
|
194
|
+
# for i, (start, end) in enumerate(zip(starts, ends, strict=True)):
|
195
|
+
for i, (start, end) in enumerate(zip(starts, ends)): # noqa: B905
|
196
|
+
fn = self.out_path / "agent_chunks" / f"agents_{i}.pqt"
|
198
197
|
agent_df.iloc[start:end].to_parquet(fn)
|
199
198
|
|
200
199
|
job_name = f"{self.run_name}_{i}"
|
201
|
-
cmd_str = f"{base_cmd_str}
|
200
|
+
cmd_str = f"{base_cmd_str} {i} {base_args}"
|
202
201
|
print("cmd:", cmd_str)
|
203
202
|
|
204
203
|
slurm_manager = SLURM()
|
@@ -213,7 +212,7 @@ class MultiProcess:
|
|
213
212
|
)
|
214
213
|
|
215
214
|
if job_id:
|
216
|
-
|
215
|
+
job_chunk_map[job_id] = i
|
217
216
|
print(f"Kicked off job: {job_name}, with SLURM {job_id=} on Eagle.")
|
218
217
|
else:
|
219
218
|
print(
|
@@ -221,5 +220,6 @@ class MultiProcess:
|
|
221
220
|
)
|
222
221
|
|
223
222
|
# Check on the job statuses until they're complete, then aggregate the results
|
224
|
-
|
225
|
-
self.
|
223
|
+
jobs = [*job_chunk_map]
|
224
|
+
self.check_status(jobs, start_time)
|
225
|
+
return job_chunk_map
|
dwind/resource.py
CHANGED
@@ -1,23 +1,75 @@
|
|
1
|
+
"""Provides the :py:class:`ResourcePotential` class for gathering pre-calculated reV generation
|
2
|
+
data.
|
3
|
+
"""
|
4
|
+
|
1
5
|
import h5py as h5
|
2
6
|
import pandas as pd
|
3
7
|
|
4
|
-
from dwind import Configuration
|
8
|
+
from dwind.config import Sector, Technology, Configuration
|
5
9
|
|
6
10
|
|
7
11
|
class ResourcePotential:
|
12
|
+
"""Helper class designed to retrieve pre-calculated energy generation data from reV."""
|
13
|
+
|
8
14
|
def __init__(
|
9
|
-
self,
|
15
|
+
self,
|
16
|
+
parcels: pd.DataFrame,
|
17
|
+
model_config: Configuration,
|
18
|
+
sector: Sector,
|
19
|
+
tech: str = "wind",
|
20
|
+
year: int = 2018,
|
10
21
|
):
|
22
|
+
"""Initializes the :py:class:`ResourcePotential` instance.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
parcels (pd.DataFrame): The agent DataFrame containing at least the following columns:
|
26
|
+
"gid", "rev_gid_{tech}", "solar_az_tilt" (solar only), "azimuth_{sector}"
|
27
|
+
(solar only), "tilt_{tech}" (solar only), "turbine_class" (wind only),
|
28
|
+
"wind_turbine_kw" (wind only), and "turbine_height_m" (wind only).
|
29
|
+
model_config (Configuration): The pre-loaded model configuration data object containing
|
30
|
+
the requisite SQL, file, and configuration data.
|
31
|
+
sector (dwind.config.Sector): A valid sector instance.
|
32
|
+
tech (str, optional): One of "solar" or "wind". Defaults to "wind".
|
33
|
+
year (int, optional): Resource year for the reV lookup. Defaults to 2018.
|
34
|
+
|
35
|
+
Raises:
|
36
|
+
ValueError: Raised if :py:attr:`parcels:` is missing any of the required columns.
|
37
|
+
"""
|
11
38
|
self.df = parcels
|
12
|
-
self.tech = tech
|
13
|
-
self.
|
39
|
+
self.tech = Technology(tech)
|
40
|
+
self.sector = sector
|
14
41
|
self.year = year
|
15
42
|
self.config = model_config
|
16
43
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
44
|
+
solar_cols = ("solar_az_tilt", f"azimuth_{self.sector.value}", f"tilt_{self.tech.value}")
|
45
|
+
# wind_cols = ("turbine_class", "wind_turbine_kw", "turbine_height_m")
|
46
|
+
wind_cols = ("wind_turbine_kw", "turbine_height_m")
|
47
|
+
|
48
|
+
if self.tech is Technology.WIND:
|
49
|
+
cols = wind_cols
|
50
|
+
elif self.tech is Technology.SOLAR:
|
51
|
+
cols = solar_cols
|
52
|
+
|
53
|
+
missing = set(cols).difference(self.df.columns.tolist())
|
54
|
+
if missing:
|
55
|
+
raise ValueError(f"`parcels` is missing the following columns: {', '.join(missing)}")
|
56
|
+
|
57
|
+
def create_rev_gid_to_summary_lkup(
|
58
|
+
self, configs: list[str], *, save_csv: bool = True
|
59
|
+
) -> pd.DataFrame:
|
60
|
+
"""Creates the reV summary tables based on the "gid" mappings in :py:attr:`parcels`.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
configs (list[str]): The list of technology-specific configurations where the generation
|
64
|
+
data should be retrieved.
|
65
|
+
save_csv (bool, optional): If True, save the resulting lookup calculated from reV to the
|
66
|
+
reV folder definied in ``Configuration.rev.generation.{tech}_DIR``. Defaults to
|
67
|
+
True.
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
pd.DataFrame: reV generation lookup table for the technology-specific configurations in
|
71
|
+
:py:attr:`configs`.
|
72
|
+
"""
|
21
73
|
config_dfs = []
|
22
74
|
for c in configs:
|
23
75
|
file_str = self.config.rev.DIR / f"rev_{c}_generation_{self.year}.h5"
|
@@ -30,10 +82,10 @@ class ResourcePotential:
|
|
30
82
|
|
31
83
|
config_df = pd.concat([rev_index, gids, annual_energy, cf_mean], axis=1)
|
32
84
|
config_df.columns = [
|
33
|
-
f"rev_index_{self.tech}",
|
34
|
-
f"rev_gid_{self.tech}",
|
35
|
-
f"{self.tech}_naep",
|
36
|
-
f"{self.tech}_cf",
|
85
|
+
f"rev_index_{self.tech.value}",
|
86
|
+
f"rev_gid_{self.tech.value}",
|
87
|
+
f"{self.tech.value}_naep",
|
88
|
+
f"{self.tech.value}_cf",
|
37
89
|
]
|
38
90
|
|
39
91
|
config_df["config"] = c
|
@@ -43,84 +95,107 @@ class ResourcePotential:
|
|
43
95
|
|
44
96
|
if save_csv:
|
45
97
|
save_name = (
|
46
|
-
self.config.rev.generation[f"{self.tech}_DIR"]
|
47
|
-
/ f"lkup_rev_gid_to_summary_{self.tech}_{self.year}.csv"
|
98
|
+
self.config.rev.generation[f"{self.tech.value}_DIR"]
|
99
|
+
/ f"lkup_rev_gid_to_summary_{self.tech.value}_{self.year}.csv"
|
48
100
|
)
|
49
101
|
summary_df.to_csv(save_name, index=False)
|
50
102
|
|
51
103
|
return summary_df
|
52
104
|
|
53
105
|
def find_rev_summary_table(self):
|
54
|
-
|
106
|
+
"""Creates the generation summary data for each of the :py:attr:`tech`-specific
|
107
|
+
configurations specified in :py:attr:`config.rev.settings.{tech}`, then maps it to the
|
108
|
+
agent data (:py:attr:`parcels`), overwriting any previously computed data.
|
109
|
+
"""
|
110
|
+
if self.tech is Technology.SOLAR:
|
55
111
|
configs = self.config.rev.settings.solar
|
56
112
|
config_col = "solar_az_tilt"
|
57
|
-
col_list = ["gid", f"rev_gid_{self.tech}", config_col]
|
58
|
-
self.df[config_col] = self.df[f"azimuth_{self.
|
113
|
+
col_list = ["gid", f"rev_gid_{self.tech.value}", config_col]
|
114
|
+
self.df[config_col] = self.df[f"azimuth_{self.sector.value}"].map(
|
59
115
|
self.config.rev.settings.azimuth_direction_to_degree
|
60
116
|
)
|
61
117
|
self.df[config_col] = (
|
62
|
-
self.df[config_col].astype(str)
|
118
|
+
self.df[config_col].astype(str)
|
119
|
+
+ "_"
|
120
|
+
+ self.df[f"tilt_{self.tech.value}"].astype(str)
|
63
121
|
)
|
64
|
-
elif self.tech
|
65
|
-
configs = self.rev.settings.wind
|
122
|
+
elif self.tech is Technology.WIND:
|
123
|
+
configs = self.config.rev.settings.wind
|
66
124
|
config_col = "turbine_class"
|
67
125
|
col_list = [
|
68
126
|
"gid",
|
69
|
-
f"rev_gid_{self.tech}",
|
127
|
+
f"rev_gid_{self.tech.value}",
|
70
128
|
config_col,
|
71
129
|
"turbine_height_m",
|
72
130
|
"wind_turbine_kw",
|
73
131
|
]
|
74
132
|
self.df[config_col] = self.df["wind_turbine_kw"].map(self.config.rev.turbine_class_dict)
|
75
133
|
|
76
|
-
out_cols = [
|
134
|
+
out_cols = [
|
135
|
+
*col_list,
|
136
|
+
f"rev_index_{self.tech.value}",
|
137
|
+
f"{self.tech.value}_naep",
|
138
|
+
f"{self.tech.value}_cf",
|
139
|
+
]
|
140
|
+
|
141
|
+
drop_cols = [
|
142
|
+
f"rev_gid_{self.tech.value}",
|
143
|
+
f"{self.tech.value}_naep",
|
144
|
+
f"{self.tech.value}_cf",
|
145
|
+
]
|
146
|
+
self.df = self.df.drop(columns=[c for c in drop_cols if c in self.df])
|
77
147
|
|
78
148
|
f_gen = (
|
79
|
-
self.config.rev.generation[f"{self.tech}_DIR"]
|
80
|
-
/ f"lkup_rev_gid_to_summary_{self.tech}_{self.year}.csv"
|
149
|
+
self.config.rev.generation[f"{self.tech.value}_DIR"]
|
150
|
+
/ f"lkup_rev_gid_to_summary_{self.tech.value}_{self.year}.csv"
|
81
151
|
)
|
82
152
|
|
83
153
|
if f_gen.exists():
|
84
|
-
generation_summary = pd.read_csv(f_gen)
|
154
|
+
generation_summary = pd.read_csv(f_gen, dtype_backend="pyarrow")
|
85
155
|
else:
|
86
156
|
generation_summary = self.create_rev_gid_to_summary_lkup(configs)
|
87
157
|
|
88
158
|
generation_summary = (
|
89
159
|
generation_summary.reset_index(drop=True)
|
90
|
-
.drop_duplicates(subset=[f"rev_index_{self.tech}", "config"])
|
160
|
+
.drop_duplicates(subset=[f"rev_index_{self.tech.value}", "config"])
|
91
161
|
.rename(columns={"config": config_col})
|
92
162
|
)
|
93
163
|
agents = self.df.merge(
|
94
|
-
generation_summary, how="left", on=[f"rev_index_{self.tech}", config_col]
|
164
|
+
generation_summary, how="left", on=[f"rev_index_{self.tech.value}", config_col]
|
95
165
|
)
|
96
166
|
return agents[out_cols]
|
97
167
|
|
98
168
|
def prepare_agents_for_gen(self):
|
99
|
-
|
100
|
-
if self.tech
|
169
|
+
"""Create lookup column based on each technology."""
|
170
|
+
if self.tech is Technology.WIND:
|
101
171
|
# drop wind turbine size duplicates
|
102
172
|
# SINCE WE ASSUME ANY TURBINE IN A GIVEN CLASS HAS THE SAME POWER CURVE
|
103
173
|
self.df.drop_duplicates(subset=["gid", "wind_size_kw"], keep="last", inplace=True)
|
104
|
-
# if running FOM
|
105
|
-
if self.
|
174
|
+
# if running FOM sector, only consider a single (largest) turbine size
|
175
|
+
if self.sector is Sector.FOM:
|
106
176
|
self.df = self.df.loc[self.df["wind_size_kw"] == self.df["wind_size_kw_fom"]]
|
107
177
|
|
108
178
|
self.df["turbine_class"] = self.df["wind_turbine_kw"].map(
|
109
179
|
self.config.rev.turbine_class_dict
|
110
180
|
)
|
111
181
|
|
112
|
-
if self.tech
|
113
|
-
# NOTE: tilt and azimuth are
|
114
|
-
self.df["solar_az_tilt"] = self.df[f"azimuth_{self.
|
182
|
+
if self.tech is Technology.SOLAR:
|
183
|
+
# NOTE: tilt and azimuth are sector-specific
|
184
|
+
self.df["solar_az_tilt"] = self.df[f"azimuth_{self.sector.value}"].map(
|
115
185
|
self.config.rev.settings.azimuth_direction_to_degree
|
116
186
|
)
|
117
187
|
self.df["solar_az_tilt"] = self.df["solar_az_tilt"].astype(str)
|
118
188
|
self.df["solar_az_tilt"] = (
|
119
|
-
self.df["solar_az_tilt"] + "_" + self.df[f"tilt_{self.
|
189
|
+
self.df["solar_az_tilt"] + "_" + self.df[f"tilt_{self.sector.value}"].astype(str)
|
120
190
|
)
|
121
191
|
|
122
|
-
def merge_gen_to_agents(self, tech_agents):
|
123
|
-
|
192
|
+
def merge_gen_to_agents(self, tech_agents: pd.DataFrame):
|
193
|
+
"""Merges :py:attr:`tech_agents` to the parcel data :py:attr:`df`.
|
194
|
+
|
195
|
+
Args:
|
196
|
+
tech_agents (pd.DataFrame): The technology-specific energy generation data.
|
197
|
+
"""
|
198
|
+
if self.tech is Technology.WIND:
|
124
199
|
cols = ["turbine_height_m", "wind_turbine_kw", "turbine_class"]
|
125
200
|
else:
|
126
201
|
# NOTE: need to drop duplicates in solar agents
|
@@ -130,16 +205,23 @@ class ResourcePotential:
|
|
130
205
|
)
|
131
206
|
cols = ["solar_az_tilt"]
|
132
207
|
|
133
|
-
cols.extend(["gid", f"rev_index_{self.tech}"])
|
208
|
+
cols.extend(["gid", f"rev_index_{self.tech.value}"])
|
134
209
|
|
135
210
|
self.df = self.df.merge(tech_agents, how="left", on=cols)
|
136
211
|
|
137
212
|
def match_rev_summary_to_agents(self):
|
213
|
+
"""Runs the energy generation gathering and merging steps, and retursns back the updated
|
214
|
+
:py:attr:`df` agent/parcel data.
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
pd.DataFrame: Updated agent/parcel data with rec/alculated "wind_aep" or "solar_aep"
|
218
|
+
information for each agent.
|
219
|
+
"""
|
138
220
|
self.prepare_agents_for_gen()
|
139
221
|
tech_agents = self.find_rev_summary_table()
|
140
222
|
self.merge_gen_to_agents(tech_agents)
|
141
223
|
|
142
|
-
if self.tech
|
224
|
+
if self.tech is Technology.WIND:
|
143
225
|
# fill nan generation values
|
144
226
|
self.df = self.df.loc[
|
145
227
|
~((self.df["wind_naep"].isnull()) & (self.df["turbine_class"] != "none"))
|
@@ -150,7 +232,7 @@ class ResourcePotential:
|
|
150
232
|
# calculate annual energy production (aep)
|
151
233
|
self.df["wind_aep"] = self.df["wind_naep"] * self.df["wind_turbine_kw"]
|
152
234
|
# self.df = self.df.drop(columns="turbine_class")
|
153
|
-
|
235
|
+
elif self.tech is Technology.SOLAR:
|
154
236
|
# fill nan generation values
|
155
237
|
self.df = self.df.loc[~(self.df["solar_naep"].isnull())]
|
156
238
|
# size groundmount system to equal wind aep
|
dwind/run.py
CHANGED
@@ -3,19 +3,26 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import sys
|
6
|
-
import tomllib
|
7
6
|
from enum import Enum
|
8
|
-
from typing import Annotated
|
7
|
+
from typing import Optional, Annotated
|
9
8
|
from pathlib import Path
|
10
9
|
|
11
10
|
import typer
|
12
11
|
import pandas as pd
|
12
|
+
from rich.style import Style
|
13
|
+
from rich.pretty import pprint
|
14
|
+
from rich.console import Console
|
13
15
|
|
14
16
|
|
15
|
-
#
|
16
|
-
|
17
|
+
# fmt: off
|
18
|
+
if sys.version_info >= (3, 11): # noqa
|
19
|
+
import tomllib
|
20
|
+
else:
|
21
|
+
import tomli as tomllib
|
22
|
+
# fmt: on
|
17
23
|
|
18
24
|
app = typer.Typer()
|
25
|
+
console = Console()
|
19
26
|
|
20
27
|
DWIND = Path("/projects/dwind/agents")
|
21
28
|
|
@@ -71,20 +78,25 @@ def year_callback(ctx: typer.Context, param: typer.CallbackParam, value: int):
|
|
71
78
|
|
72
79
|
|
73
80
|
def load_agents(
|
74
|
-
file_name:
|
81
|
+
file_name: Path | None = None,
|
75
82
|
location: str | None = None,
|
76
83
|
sector: str | None = None,
|
84
|
+
model_config: str | Path | None = None,
|
85
|
+
*,
|
86
|
+
prepare: bool = False,
|
77
87
|
) -> pd.DataFrame:
|
78
88
|
"""Load the agent file based on a filename or the location and sector to a Pandas DataFrame,
|
79
89
|
and return the data frame.
|
80
90
|
|
81
91
|
Args:
|
82
|
-
file_name (
|
92
|
+
file_name (Path | None, optional): Name of the agent file, if not auto-generating from
|
83
93
|
the :py:attr:`location` and :py:attr:`sector` inputs. Defaults to None.
|
84
94
|
location (str | None, optional): The name of the location or grouping, such as
|
85
95
|
"colorado_larimer" or "priority1". Defaults to None.
|
86
96
|
sector (str | None, optional): The name of the section. Must be one of "btm" or "fom".
|
87
97
|
Defaults to None.
|
98
|
+
prepare (bool, optional): True if loading pre-chunked and prepared agent data, which should
|
99
|
+
bypass the standard column checking for additional joins, by default False.
|
88
100
|
|
89
101
|
Returns:
|
90
102
|
pd.DataFrame: The agent DataFrame.
|
@@ -97,10 +109,28 @@ def load_agents(
|
|
97
109
|
f_agents = (
|
98
110
|
file_name if file_name is not None else DWIND / f"{location}/agents_dwind_{sector}.parquet"
|
99
111
|
)
|
112
|
+
if not isinstance(f_agents, Path):
|
113
|
+
f_agents = Path(f_agents).resolve()
|
114
|
+
|
115
|
+
alternative_suffix = (".pqt", ".parquet", ".pkl", ".pickle", ".csv")
|
116
|
+
base_style = Style.parse("cyan")
|
100
117
|
if not f_agents.exists():
|
101
|
-
|
102
|
-
|
103
|
-
|
118
|
+
for suffix in alternative_suffix:
|
119
|
+
if (new_fn := f_agents.with_suffix(suffix)).exists():
|
120
|
+
if new_fn != f_agents:
|
121
|
+
msg = (
|
122
|
+
f"Using alternative agent file: {new_fn}\n\t"
|
123
|
+
f"Requested agent file: {f_agents}"
|
124
|
+
)
|
125
|
+
console.print(msg, style=base_style)
|
126
|
+
f_agents = new_fn
|
127
|
+
break
|
128
|
+
|
129
|
+
if prepare:
|
130
|
+
return Agents.load_and_prepare_agents(
|
131
|
+
agent_file=f_agents, sector=sector, model_config=model_config
|
132
|
+
)
|
133
|
+
return Agents.load_agents(agent_file=f_agents)
|
104
134
|
|
105
135
|
|
106
136
|
@app.command()
|
@@ -155,7 +185,9 @@ def run_hpc(
|
|
155
185
|
dir_out: Annotated[
|
156
186
|
str, typer.Option(help="Path to where the chunked outputs should be saved.")
|
157
187
|
],
|
158
|
-
stdout_path: Annotated[
|
188
|
+
stdout_path: Annotated[
|
189
|
+
Optional[str], typer.Option(help="The path to write stdout logs.") # noqa
|
190
|
+
] = None,
|
159
191
|
):
|
160
192
|
"""Run dwind via the HPC multiprocessing interface."""
|
161
193
|
sys.path.append(repository)
|
@@ -180,7 +212,9 @@ def run_hpc(
|
|
180
212
|
stdout_path=stdout_path,
|
181
213
|
)
|
182
214
|
|
183
|
-
agent_df = load_agents(
|
215
|
+
agent_df = load_agents(
|
216
|
+
location=location, sector=sector, model_config=model_config, prepare=True
|
217
|
+
)
|
184
218
|
mp.run_jobs(agent_df)
|
185
219
|
|
186
220
|
|
@@ -194,15 +228,15 @@ def run_hpc_from_config(
|
|
194
228
|
config_path = Path(config_path).resolve()
|
195
229
|
with config_path.open("rb") as f:
|
196
230
|
config = tomllib.load(f)
|
197
|
-
print(
|
231
|
+
print("Running the following configuration:")
|
232
|
+
pprint(config)
|
198
233
|
|
199
234
|
run_hpc(**config)
|
200
235
|
|
201
236
|
|
202
237
|
@app.command()
|
203
238
|
def run_chunk(
|
204
|
-
|
205
|
-
# end: Annotated[int, typer.Option(help="chunk end index")],
|
239
|
+
chunk_ix: Annotated[int, typer.Option(help="Chunk number/index. Used for logging.")],
|
206
240
|
location: Annotated[
|
207
241
|
str, typer.Option(help="The state, state_county, or priority region to run.")
|
208
242
|
],
|
@@ -213,7 +247,6 @@ def run_chunk(
|
|
213
247
|
year: Annotated[
|
214
248
|
int, typer.Option(callback=year_callback, help="The year basis of the scenario.")
|
215
249
|
],
|
216
|
-
chunk_ix: Annotated[int, typer.Option(help="Chunk number/index. Used for logging.")],
|
217
250
|
out_path: Annotated[str, typer.Option(help="save path")],
|
218
251
|
repository: Annotated[
|
219
252
|
str, typer.Option(help="Path to the dwind repository to use when running the model.")
|
@@ -230,7 +263,7 @@ def run_chunk(
|
|
230
263
|
from dwind.model import Model
|
231
264
|
|
232
265
|
agent_file = Path(out_path).resolve() / f"agents_{chunk_ix}.pqt"
|
233
|
-
agents = load_agents(file_name=agent_file)
|
266
|
+
agents = load_agents(file_name=agent_file, prepare=False)
|
234
267
|
agent_file.unlink()
|
235
268
|
|
236
269
|
model = Model(
|
@@ -271,7 +304,7 @@ def run(
|
|
271
304
|
sys.path.append(repository)
|
272
305
|
from dwind.model import Model
|
273
306
|
|
274
|
-
agents = load_agents(location=location, sector=sector)
|
307
|
+
agents = load_agents(location=location, sector=sector, model_config=model_config, prepare=True)
|
275
308
|
model = Model(
|
276
309
|
agents=agents,
|
277
310
|
location=location,
|