dwind 0.3__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/resource.py ADDED
@@ -0,0 +1,166 @@
1
+ import h5py as h5
2
+ import pandas as pd
3
+
4
+ from dwind import Configuration
5
+
6
+
7
+ class ResourcePotential:
8
+ def __init__(
9
+ self, parcels, model_config: Configuration, tech="wind", application="fom", year="2018"
10
+ ):
11
+ self.df = parcels
12
+ self.tech = tech
13
+ self.application = application
14
+ self.year = year
15
+ self.config = model_config
16
+
17
+ if self.tech not in ("wind", "solar"):
18
+ raise ValueError("`tech` must be one of 'solar' or 'wind'.")
19
+
20
+ def create_rev_gid_to_summary_lkup(self, configs, save_csv=True):
21
+ config_dfs = []
22
+ for c in configs:
23
+ file_str = self.config.rev.DIR / f"rev_{c}_generation_{self.year}.h5"
24
+
25
+ with h5.File(file_str, "r") as hf:
26
+ rev_index = pd.DataFrame(hf["meta"][...]).index.to_series()
27
+ gids = pd.DataFrame(hf["meta"][...])[["gid"]]
28
+ annual_energy = pd.DataFrame(hf["annual_energy"][...])
29
+ cf_mean = pd.DataFrame(hf["cf_mean"][...])
30
+
31
+ config_df = pd.concat([rev_index, gids, annual_energy, cf_mean], axis=1)
32
+ 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",
37
+ ]
38
+
39
+ config_df["config"] = c
40
+ config_dfs.append(config_df)
41
+
42
+ summary_df = pd.concat(config_dfs)
43
+
44
+ if save_csv:
45
+ save_name = (
46
+ self.config.rev.generation[f"{self.tech}_DIR"]
47
+ / f"lkup_rev_gid_to_summary_{self.tech}_{self.year}.csv"
48
+ )
49
+ summary_df.to_csv(save_name, index=False)
50
+
51
+ return summary_df
52
+
53
+ def find_rev_summary_table(self):
54
+ if self.tech == "solar":
55
+ configs = self.config.rev.settings.solar
56
+ 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.application}"].map(
59
+ self.config.rev.settings.azimuth_direction_to_degree
60
+ )
61
+ self.df[config_col] = (
62
+ self.df[config_col].astype(str) + "_" + self.df[f"tilt_{self.tech}"].astype(str)
63
+ )
64
+ elif self.tech == "wind":
65
+ configs = self.rev.settings.wind
66
+ config_col = "turbine_class"
67
+ col_list = [
68
+ "gid",
69
+ f"rev_gid_{self.tech}",
70
+ config_col,
71
+ "turbine_height_m",
72
+ "wind_turbine_kw",
73
+ ]
74
+ self.df[config_col] = self.df["wind_turbine_kw"].map(self.config.rev.turbine_class_dict)
75
+
76
+ out_cols = [*col_list, f"rev_index_{self.tech}", f"{self.tech}_naep", f"{self.tech}_cf"]
77
+
78
+ f_gen = (
79
+ self.config.rev.generation[f"{self.tech}_DIR"]
80
+ / f"lkup_rev_gid_to_summary_{self.tech}_{self.year}.csv"
81
+ )
82
+
83
+ if f_gen.exists():
84
+ generation_summary = pd.read_csv(f_gen)
85
+ else:
86
+ generation_summary = self.create_rev_gid_to_summary_lkup(configs)
87
+
88
+ generation_summary = (
89
+ generation_summary.reset_index(drop=True)
90
+ .drop_duplicates(subset=[f"rev_index_{self.tech}", "config"])
91
+ .rename(columns={"config": config_col})
92
+ )
93
+ agents = self.df.merge(
94
+ generation_summary, how="left", on=[f"rev_index_{self.tech}", config_col]
95
+ )
96
+ return agents[out_cols]
97
+
98
+ def prepare_agents_for_gen(self):
99
+ # create lookup column based on each tech
100
+ if self.tech == "wind":
101
+ # drop wind turbine size duplicates
102
+ # SINCE WE ASSUME ANY TURBINE IN A GIVEN CLASS HAS THE SAME POWER CURVE
103
+ self.df.drop_duplicates(subset=["gid", "wind_size_kw"], keep="last", inplace=True)
104
+ # if running FOM application, only consider a single (largest) turbine size
105
+ if self.application == "fom":
106
+ self.df = self.df.loc[self.df["wind_size_kw"] == self.df["wind_size_kw_fom"]]
107
+
108
+ self.df["turbine_class"] = self.df["wind_turbine_kw"].map(
109
+ self.config.rev.turbine_class_dict
110
+ )
111
+
112
+ if self.tech == "solar":
113
+ # NOTE: tilt and azimuth are application-specific
114
+ self.df["solar_az_tilt"] = self.df[f"azimuth_{self.application}"].map(
115
+ self.config.rev.settings.azimuth_direction_to_degree
116
+ )
117
+ self.df["solar_az_tilt"] = self.df["solar_az_tilt"].astype(str)
118
+ self.df["solar_az_tilt"] = (
119
+ self.df["solar_az_tilt"] + "_" + self.df[f"tilt_{self.application}"].astype(str)
120
+ )
121
+
122
+ def merge_gen_to_agents(self, tech_agents):
123
+ if self.tech == "wind":
124
+ cols = ["turbine_height_m", "wind_turbine_kw", "turbine_class"]
125
+ else:
126
+ # NOTE: need to drop duplicates in solar agents
127
+ # since multiple rows exist due to multiple turbine configs for a given parcel
128
+ tech_agents = tech_agents.drop_duplicates(
129
+ subset=["gid", "rev_gid_solar", "solar_az_tilt"]
130
+ )
131
+ cols = ["solar_az_tilt"]
132
+
133
+ cols.extend(["gid", f"rev_index_{self.tech}"])
134
+
135
+ self.df = self.df.merge(tech_agents, how="left", on=cols)
136
+
137
+ def match_rev_summary_to_agents(self):
138
+ self.prepare_agents_for_gen()
139
+ tech_agents = self.find_rev_summary_table()
140
+ self.merge_gen_to_agents(tech_agents)
141
+
142
+ if self.tech == "wind":
143
+ # fill nan generation values
144
+ self.df = self.df.loc[
145
+ ~((self.df["wind_naep"].isnull()) & (self.df["turbine_class"] != "none"))
146
+ ]
147
+ self.df["wind_naep"] = self.df["wind_naep"].fillna(0.0)
148
+ self.df["wind_cf"] = self.df["wind_cf"].fillna(0.0)
149
+ # self.df['wind_cf_hourly'] = self.df['wind_cf_hourly'].fillna(0.)
150
+ # calculate annual energy production (aep)
151
+ self.df["wind_aep"] = self.df["wind_naep"] * self.df["wind_turbine_kw"]
152
+ # self.df = self.df.drop(columns="turbine_class")
153
+ else:
154
+ # fill nan generation values
155
+ self.df = self.df.loc[~(self.df["solar_naep"].isnull())]
156
+ # size groundmount system to equal wind aep
157
+ # self.df['solar_size_kw_fom'] = np.where(
158
+ # self.df['solar_groundmount'],
159
+ # self.df['wind_aep'] / (self.df['solar_cf'] * 8760),
160
+ # self.df['solar_size_kw_fom']
161
+ # )
162
+
163
+ # calculate annual energy production (aep)
164
+ self.df["solar_aep"] = self.df["solar_naep"] * self.df["solar_size_kw_fom"]
165
+
166
+ return self.df
dwind/run.py ADDED
@@ -0,0 +1,288 @@
1
+ """Enables running dwind as a CLI tool, the primary interface for working with dwind."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import tomllib
7
+ from enum import Enum
8
+ from typing import Annotated
9
+ from pathlib import Path
10
+
11
+ import typer
12
+ import pandas as pd
13
+
14
+
15
+ # from memory_profiler import profile
16
+
17
+
18
+ app = typer.Typer()
19
+
20
+ DWIND = Path("/projects/dwind/agents")
21
+
22
+
23
+ class Sector(str, Enum):
24
+ """Typer helper for validating the sector input.
25
+
26
+ - "fom": Front of meter
27
+ - "btm": Behind the meter
28
+ """
29
+
30
+ fom = "fom"
31
+ btm = "btm"
32
+
33
+
34
+ class Scenario(str, Enum):
35
+ """Scenario to run.
36
+
37
+ The only current option is "baseline".
38
+ """
39
+
40
+ baseline = "baseline"
41
+
42
+
43
+ def year_callback(ctx: typer.Context, param: typer.CallbackParam, value: int):
44
+ """Typer helper to validate the year input.
45
+
46
+ Parameters
47
+ ----------
48
+ ctx : typer.Context
49
+ The Typer context.
50
+ param : typer.CallbackParam
51
+ The Typer parameter.
52
+ value : int
53
+ User input for the analysis year basis, must be one of 2022, 2024, or 2025.
54
+
55
+ Returns:
56
+ -------
57
+ int
58
+ The input :py:param:`value`, if it is a valid input.
59
+
60
+ Raises:
61
+ ------
62
+ typer.BadParameter
63
+ Raised if the input is not one of 2022, 2024, or 2025.
64
+ """
65
+ valid_years = (2022, 2024, 2025, 2035, 2040)
66
+ if ctx.resilient_parsing:
67
+ return
68
+ if value not in valid_years:
69
+ raise typer.BadParameter(f"Only {valid_years} are valid options for `year`, not {value}.")
70
+ return value
71
+
72
+
73
+ def load_agents(
74
+ file_name: str | Path | None = None,
75
+ location: str | None = None,
76
+ sector: str | None = None,
77
+ ) -> pd.DataFrame:
78
+ """Load the agent file based on a filename or the location and sector to a Pandas DataFrame,
79
+ and return the data frame.
80
+
81
+ Args:
82
+ file_name (str | Path | None, optional): Name of the agent file, if not auto-generating from
83
+ the :py:attr:`location` and :py:attr:`sector` inputs. Defaults to None.
84
+ location (str | None, optional): The name of the location or grouping, such as
85
+ "colorado_larimer" or "priority1". Defaults to None.
86
+ sector (str | None, optional): The name of the section. Must be one of "btm" or "fom".
87
+ Defaults to None.
88
+
89
+ Returns:
90
+ pd.DataFrame: The agent DataFrame.
91
+ """
92
+ from dwind.model import Agents
93
+
94
+ if file_name is None and (location is None or sector is None):
95
+ raise ValueError("One of `file_name` or `location` and `sector` must be provided.")
96
+
97
+ f_agents = (
98
+ file_name if file_name is not None else DWIND / f"{location}/agents_dwind_{sector}.parquet"
99
+ )
100
+ if not f_agents.exists():
101
+ f_agents = f_agents.with_suffix(".pickle")
102
+ agents = Agents(agent_file=f_agents).agents
103
+ return agents
104
+
105
+
106
+ @app.command()
107
+ def run_hpc(
108
+ location: Annotated[
109
+ str, typer.Option(help="The state, state_county, or priority region to run.")
110
+ ],
111
+ sector: Annotated[
112
+ Sector, typer.Option(help="One of fom (front of meter) or btm (back-of-the-meter).")
113
+ ],
114
+ scenario: Annotated[
115
+ Scenario,
116
+ typer.Option(help="The scenario to run (baseline is the current only option)."),
117
+ ],
118
+ year: Annotated[
119
+ int,
120
+ typer.Option(
121
+ callback=year_callback,
122
+ help="The assumption year for the analysis. Options are 2022, 2024, and 2025.",
123
+ ),
124
+ ],
125
+ repository: Annotated[
126
+ str, typer.Option(help="Path to the dwind repository to use when running the model.")
127
+ ],
128
+ nodes: Annotated[
129
+ int,
130
+ typer.Option(
131
+ help="Number of HPC nodes or CPU nodes to run on. -1 indicates 75% of CPU limit."
132
+ ),
133
+ ],
134
+ allocation: Annotated[str, typer.Option(help="HPC allocation name.")],
135
+ memory: Annotated[int, typer.Option(help="Node memory, in GB (HPC only).")],
136
+ walltime: Annotated[int, typer.Option(help="Node walltime request, in hours.")],
137
+ feature: Annotated[
138
+ str,
139
+ typer.Option(
140
+ help=(
141
+ "Additional flags for the SLURM job, using formatting such as"
142
+ " --qos=high or --depend=[state:job_id]."
143
+ )
144
+ ),
145
+ ],
146
+ env: Annotated[
147
+ str,
148
+ typer.Option(
149
+ help="The path to the dwind Python environment that should be used to run the model."
150
+ ),
151
+ ],
152
+ model_config: Annotated[
153
+ str, typer.Option(help="Complete file name and path of the model configuration file")
154
+ ],
155
+ dir_out: Annotated[
156
+ str, typer.Option(help="Path to where the chunked outputs should be saved.")
157
+ ],
158
+ stdout_path: Annotated[str | None, typer.Option(help="The path to write stdout logs.")] = None,
159
+ ):
160
+ """Run dwind via the HPC multiprocessing interface."""
161
+ sys.path.append(repository)
162
+ from dwind.mp import MultiProcess
163
+
164
+ # NOTE: collect_by_priority has been removed but may need to be reinstated
165
+
166
+ mp = MultiProcess(
167
+ location=location,
168
+ sector=sector,
169
+ scenario=scenario,
170
+ year=year,
171
+ env=env,
172
+ n_nodes=nodes,
173
+ memory=memory,
174
+ walltime=walltime,
175
+ allocation=allocation,
176
+ feature=feature,
177
+ repository=repository,
178
+ model_config=model_config,
179
+ dir_out=dir_out,
180
+ stdout_path=stdout_path,
181
+ )
182
+
183
+ agent_df = load_agents(location=location, sector=sector)
184
+ mp.run_jobs(agent_df)
185
+
186
+
187
+ @app.command("run-config")
188
+ def run_hpc_from_config(
189
+ config_path: Annotated[
190
+ str, typer.Argument(help="Path to configuration TOML with run and model parameters.")
191
+ ],
192
+ ):
193
+ """Run dwind via the HPC multiprocessing interface from a configuration file."""
194
+ config_path = Path(config_path).resolve()
195
+ with config_path.open("rb") as f:
196
+ config = tomllib.load(f)
197
+ print(config)
198
+
199
+ run_hpc(**config)
200
+
201
+
202
+ @app.command()
203
+ def run_chunk(
204
+ # start: Annotated[int, typer.Option(help="chunk start index")],
205
+ # end: Annotated[int, typer.Option(help="chunk end index")],
206
+ location: Annotated[
207
+ str, typer.Option(help="The state, state_county, or priority region to run.")
208
+ ],
209
+ sector: Annotated[
210
+ Sector, typer.Option(help="One of fom (front of meter) or btm (back-of-the-meter).")
211
+ ],
212
+ scenario: Annotated[str, typer.Option(help="The scenario to run, such as baseline.")],
213
+ year: Annotated[
214
+ int, typer.Option(callback=year_callback, help="The year basis of the scenario.")
215
+ ],
216
+ chunk_ix: Annotated[int, typer.Option(help="Chunk number/index. Used for logging.")],
217
+ out_path: Annotated[str, typer.Option(help="save path")],
218
+ repository: Annotated[
219
+ str, typer.Option(help="Path to the dwind repository to use when running the model.")
220
+ ],
221
+ model_config: Annotated[
222
+ str, typer.Option(help="Complete file name and path of the model configuration file")
223
+ ],
224
+ ):
225
+ """Run a chunk of a dwind model. Internal only, do not run outside the context of a
226
+ chunked analysis.
227
+ """
228
+ # Import the correct version of the library
229
+ sys.path.append(repository)
230
+ from dwind.model import Model
231
+
232
+ agent_file = Path(out_path).resolve() / f"agents_{chunk_ix}.pqt"
233
+ agents = load_agents(file_name=agent_file)
234
+ agent_file.unlink()
235
+
236
+ model = Model(
237
+ agents=agents,
238
+ location=location,
239
+ sector=sector,
240
+ scenario=scenario,
241
+ year=year,
242
+ chunk_ix=chunk_ix,
243
+ out_path=out_path,
244
+ model_config=model_config,
245
+ )
246
+ model.run()
247
+
248
+
249
+ @app.command()
250
+ def run(
251
+ location: Annotated[
252
+ str, typer.Option(help="The state, state_county, or priority region to run.")
253
+ ],
254
+ sector: Annotated[
255
+ Sector, typer.Option(help="One of fom (front of meter) or btm (back-of-the-meter).")
256
+ ],
257
+ scenario: Annotated[str, typer.Option(help="The scenario to run, such as 'baseline'.")],
258
+ year: Annotated[int, typer.Option(help="The year basis of the scenario.")],
259
+ out_path: Annotated[str, typer.Option(help="save path")],
260
+ repository: Annotated[
261
+ str, typer.Option(help="Path to the dwind repository to use when running the model.")
262
+ ],
263
+ model_config: Annotated[
264
+ str, typer.Option(help="Complete file name and path of the model configuration file")
265
+ ],
266
+ ):
267
+ """Run the dwind model. Does not yet work, do not run unless dwind has been configured
268
+ to not rely on Kestrel usage.
269
+ """
270
+ # Import the correct version of the library
271
+ sys.path.append(repository)
272
+ from dwind.model import Model
273
+
274
+ agents = load_agents(location=location, sector=sector)
275
+ model = Model(
276
+ agents=agents,
277
+ location=location,
278
+ sector=sector,
279
+ scenario=scenario,
280
+ year=year,
281
+ out_path=out_path,
282
+ model_config=model_config,
283
+ )
284
+ model.run()
285
+
286
+
287
+ if __name__ == "__main__":
288
+ app()
dwind/scenarios.py ADDED
@@ -0,0 +1,139 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import pandas as pd
5
+
6
+
7
+ def config_nem(scenario, year):
8
+ # NEM_SCENARIO_CSV
9
+ nem_opt_scens = ["highrecost", "lowrecost", "re100"]
10
+ # nem_opt_scens = ['der_value_HighREcost', 'der_value_LowREcost', 're_100']
11
+ if scenario in nem_opt_scens:
12
+ nem_scenario_csv = "nem_optimistic_der_value_2035.csv"
13
+ elif scenario == "baseline" and year in (2022, 2025, 2035):
14
+ nem_scenario_csv = f"nem_baseline_{year}.csv"
15
+ else:
16
+ nem_scenario_csv = "nem_baseline_2035.csv"
17
+
18
+ return nem_scenario_csv
19
+
20
+
21
+ def config_cambium(scenario):
22
+ # CAMBIUM_SCENARIO
23
+ if scenario == "highrecost" or scenario == "re100":
24
+ cambium_scenario = "StdScen20_HighRECost"
25
+ elif scenario == "lowrecost":
26
+ cambium_scenario = "StdScen20_LowRECost"
27
+ else:
28
+ # cambium_scenario = "StdScen20_MidCase"
29
+ cambium_scenario = "Cambium23_MidCase"
30
+
31
+ return cambium_scenario
32
+
33
+
34
+ def config_costs(scenario, year):
35
+ # COST_INPUTS
36
+ f = Path(f"/projects/dwind/configs/costs/atb24/ATB24_costs_{scenario}_{year}.json").resolve()
37
+ with f.open("r") as f_in:
38
+ cost_inputs = json.load(f_in)
39
+
40
+ return cost_inputs
41
+
42
+
43
+ def config_performance(scenario, year):
44
+ # PERFORMANCE_INPUTS
45
+ if scenario == "baseline" and year == 2022:
46
+ performance_inputs = {
47
+ "solar": pd.DataFrame(
48
+ [
49
+ ["res", 0.017709659, 0.005],
50
+ ["com", 0.017709659, 0.005],
51
+ ["ind", 0.017709659, 0.00],
52
+ ],
53
+ columns=["sector_abbr", "pv_kw_per_sqft", "pv_degradation_factor"],
54
+ ),
55
+ "wind": pd.DataFrame(
56
+ [
57
+ [2.5, 0.083787756, 0.85],
58
+ [5.0, 0.083787756, 0.85],
59
+ [10.0, 0.083787756, 0.85],
60
+ [20.0, 0.083787756, 0.85],
61
+ [50.0, 0.116657183, 0.85],
62
+ [100.0, 0.116657183, 0.85],
63
+ [250.0, 0.106708234, 0.85],
64
+ [500.0, 0.106708234, 0.85],
65
+ [750.0, 0.106708234, 0.85],
66
+ [1000.0, 0.106708234, 0.85],
67
+ [1500.0, 0.106708234, 0.85],
68
+ ],
69
+ columns=["wind_turbine_kw_btm", "perf_improvement_factor", "wind_derate_factor"],
70
+ ),
71
+ }
72
+ else:
73
+ performance_inputs = {
74
+ "solar": {
75
+ "pv_kw_per_sqft": {"res": 0.021677397, "com": 0.021677397, "ind": 0.021677397},
76
+ "pv_degradation_factor": {"res": 0.005, "com": 0.005, "ind": 0.005},
77
+ },
78
+ "wind": {
79
+ "perf_improvement_factor": {
80
+ 2.5: 0.23136759,
81
+ 5.0: 0.23136759,
82
+ 10.0: 0.23136759,
83
+ 20.0: 0.23136759,
84
+ 50.0: 0.23713196,
85
+ 100.0: 0.23713196,
86
+ 250.0: 0.23617185,
87
+ 500.0: 0.23617185,
88
+ 750.0: 0.23617185,
89
+ 1000.0: 0.23617185,
90
+ 1500.0: 0.23617185,
91
+ },
92
+ "wind_derate_factor": {
93
+ 2.5: 0.85,
94
+ 5.0: 0.85,
95
+ 10.0: 0.85,
96
+ 20.0: 0.85,
97
+ 50.0: 0.85,
98
+ 100.0: 0.85,
99
+ 250.0: 0.85,
100
+ 500.0: 0.85,
101
+ 750.0: 0.85,
102
+ 1000.0: 0.85,
103
+ 1500.0: 0.85,
104
+ },
105
+ },
106
+ }
107
+
108
+ return performance_inputs
109
+
110
+
111
+ def config_financial(scenario, year):
112
+ # FINANCIAL_INPUTS
113
+ scenarios = ("baseline", "metering", "billing")
114
+ if scenario in scenarios and year == 2025:
115
+ f = f"/projects/dwind/configs/costs/atb24/ATB24_financing_baseline_{year}.json"
116
+ i = Path("/projects/dwind/data/incentives/2025_incentives.json").resolve()
117
+ with i.open("r") as i_in:
118
+ incentives = json.load(i_in)
119
+ elif scenario in scenarios and year in (2035, 2040):
120
+ f = "/projects/dwind/configs/costs/atb24/ATB24_financing_baseline_2035.json"
121
+ else:
122
+ # use old assumptions
123
+ f = "/projects/dwind/configs/costs/atb20/ATB20_financing_baseline_2035.json"
124
+ f = Path(f).resolve()
125
+
126
+ with f.open("r") as f_in:
127
+ financials = json.load(f_in)
128
+ if year == 2025:
129
+ financials["BTM"]["itc_fraction_of_capex"] = incentives
130
+ financials["FOM"]["itc_fraction_of_capex"] = incentives
131
+ financials["FOM"]["ptc_fed_dlrs_per_kwh"]["solar"] = 0.0
132
+ financials["FOM"]["ptc_fed_dlrs_per_kwh"]["wind"] = 0.0
133
+ else:
134
+ financials["BTM"]["itc_fraction_of_capex"] = 0.3
135
+ financials["FOM"]["itc_fraction_of_capex"] = 0.3
136
+ financials["FOM"]["ptc_fed_dlrs_per_kwh"]["solar"] = 0.0
137
+ financials["FOM"]["ptc_fed_dlrs_per_kwh"]["wind"] = 0.0
138
+
139
+ return financials