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/valuation.py
CHANGED
@@ -1,5 +1,10 @@
|
|
1
|
+
"""Provides the core value calculation methods."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
1
5
|
import time
|
2
6
|
import logging
|
7
|
+
import pathlib
|
3
8
|
import functools
|
4
9
|
import concurrent.futures as cf
|
5
10
|
|
@@ -13,32 +18,37 @@ import PySAM.Utilityrate5 as ur5
|
|
13
18
|
import PySAM.Merchantplant as mp
|
14
19
|
from scipy import optimize
|
15
20
|
|
16
|
-
from dwind import
|
21
|
+
from dwind import scenarios
|
22
|
+
from dwind.utils import loader
|
23
|
+
from dwind.config import Year, Sector, Scenario, Technology, Optimization, Configuration
|
17
24
|
|
18
25
|
|
19
26
|
log = logging.getLogger("dwfs")
|
20
27
|
|
21
28
|
|
22
29
|
class ValueFunctions:
|
30
|
+
"""Primary model calculation engine responsible for the computation of individual agents."""
|
31
|
+
|
23
32
|
def __init__(
|
24
33
|
self,
|
25
|
-
scenario: str,
|
26
|
-
year: int,
|
34
|
+
scenario: str | Scenario,
|
35
|
+
year: int | Year,
|
27
36
|
configuration: Configuration,
|
28
|
-
return_format="totals",
|
37
|
+
return_format: str = "totals",
|
29
38
|
):
|
30
|
-
"""
|
39
|
+
"""Creates an instance of the valuation class.
|
31
40
|
|
32
41
|
Args:
|
33
42
|
scenario (str): Only option is "baseline" currently.
|
34
43
|
year (int): Analysis year.
|
35
44
|
configuration (dwind.config.Configuration): Model configuration with universal settings.
|
36
|
-
return_format
|
37
|
-
|
38
|
-
annual totals for each value stream, or a single cumulative total
|
45
|
+
return_format (str, optional): One of "profiles", "total_profile", "totals", or "total"
|
46
|
+
to return individual value stream 8760s, a cumulative value stream 8760,
|
47
|
+
annual totals for each value stream, or a single cumulative total. Defaults to
|
48
|
+
"totals".
|
39
49
|
"""
|
40
|
-
self.scenario = scenario
|
41
|
-
self.year = year
|
50
|
+
self.scenario = Scenario(scenario)
|
51
|
+
self.year = Year(year)
|
42
52
|
self.config = configuration
|
43
53
|
self.return_format = return_format
|
44
54
|
|
@@ -50,19 +60,25 @@ class ValueFunctions:
|
|
50
60
|
self.load()
|
51
61
|
|
52
62
|
def load(self):
|
63
|
+
"""Loads all the core data from CSV and SQL for configuring PySAM."""
|
53
64
|
_load_csv = functools.partial(loader.load_df, year=self.year)
|
54
65
|
_load_sql = functools.partial(
|
55
66
|
loader.load_df,
|
56
67
|
year=self.year,
|
57
|
-
|
68
|
+
sql_constructor=self.config.sql.ATLAS_PG_CON_STR,
|
58
69
|
)
|
70
|
+
cost_dir = self.config.cost.DIR
|
59
71
|
|
60
|
-
self.retail_rate_inputs = _load_csv(self.config.cost.RETAIL_RATE_INPUT_TABLE)
|
61
|
-
self.wholesale_rate_inputs = _load_csv(
|
62
|
-
|
72
|
+
self.retail_rate_inputs = _load_csv(cost_dir / self.config.cost.RETAIL_RATE_INPUT_TABLE)
|
73
|
+
self.wholesale_rate_inputs = _load_csv(
|
74
|
+
cost_dir / self.config.cost.WHOLESALE_RATE_INPUT_TABLE
|
75
|
+
)
|
76
|
+
self.depreciation_schedule_inputs = _load_csv(
|
77
|
+
cost_dir / self.config.cost.DEPREC_INPUTS_TABLE
|
78
|
+
)
|
63
79
|
|
64
80
|
if "wind" in self.config.project.settings.TECHS:
|
65
|
-
self.wind_price_inputs =
|
81
|
+
self.wind_price_inputs = _load_csv(cost_dir / self.config.cost.WIND_PRICE_INPUT_TABLE)
|
66
82
|
self.wind_tech_inputs = _load_sql(self.config.cost.WIND_TECH_INPUT_TABLE)
|
67
83
|
self.wind_derate_inputs = _load_sql(self.config.cost.WIND_DERATE_INPUT_TABLE)
|
68
84
|
|
@@ -115,9 +131,18 @@ class ValueFunctions:
|
|
115
131
|
costs["system_variable_om_per_kw"] = cost_inputs["system_variable_om_per_kw"][tech]
|
116
132
|
return costs
|
117
133
|
|
118
|
-
def _preprocess_btm(self, df, tech="wind"):
|
119
|
-
|
120
|
-
|
134
|
+
def _preprocess_btm(self, df: pd.DataFrame, tech: str = "wind") -> pd.DataFrame:
|
135
|
+
"""Join the core behind-the-meter wind and solar financial data to the agent dataframe
|
136
|
+
(:py:attr:`df`) based on turbine/solar sizing for each parcel.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
df (pd.DataFrame): The agent data.
|
140
|
+
tech (str, optional): One of "solar" or "wind". Defaults to "wind".
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
pd.DataFrame: Updated agent data with key financial data attached.
|
144
|
+
"""
|
145
|
+
tech = Technology(tech)
|
121
146
|
df["county_id_int"] = df.county_id.astype(int)
|
122
147
|
|
123
148
|
# Get the electricity rates
|
@@ -138,8 +163,12 @@ class ValueFunctions:
|
|
138
163
|
df = df.drop(columns="county_id_int")
|
139
164
|
|
140
165
|
# Technology-specific factors
|
141
|
-
|
142
|
-
|
166
|
+
if tech is Technology.SOLAR:
|
167
|
+
tech_join = "sector_abbr"
|
168
|
+
elif tech is Technology.WIND:
|
169
|
+
tech_join = "wind_turbine_kw_btm"
|
170
|
+
|
171
|
+
cost_df = self._process_btm_costs(self.COST_INPUTS["BTM"], tech.value)
|
143
172
|
df = pd.merge(
|
144
173
|
df,
|
145
174
|
cost_df,
|
@@ -147,15 +176,15 @@ class ValueFunctions:
|
|
147
176
|
left_on=tech_join,
|
148
177
|
right_on=tech_join,
|
149
178
|
)
|
150
|
-
if tech
|
179
|
+
if tech is Technology.SOLAR:
|
151
180
|
df = pd.merge(
|
152
181
|
df,
|
153
|
-
self.PERFORMANCE_INPUTS[tech],
|
182
|
+
self.PERFORMANCE_INPUTS[tech.value],
|
154
183
|
how="left",
|
155
184
|
left_on=tech_join,
|
156
185
|
right_on=tech_join,
|
157
186
|
)
|
158
|
-
|
187
|
+
elif tech is Technology.WIND:
|
159
188
|
df = pd.merge(
|
160
189
|
df,
|
161
190
|
self.wind_tech_inputs[["turbine_size_kw", "perf_improvement_factor"]],
|
@@ -177,10 +206,7 @@ class ValueFunctions:
|
|
177
206
|
# field, and need to be removed, then rejoined with the appropriate column names
|
178
207
|
financial = self.FINANCIAL_INPUTS["BTM"].copy()
|
179
208
|
if self.year == 2025:
|
180
|
-
incentives =
|
181
|
-
self.FINANCIAL_INPUTS["BTM"].pop("itc_fraction_of_capex")
|
182
|
-
).T
|
183
|
-
incentives.index.name = "census_tract_id"
|
209
|
+
incentives = self.FINANCIAL_INPUTS["BTM"].pop("itc_fraction_of_capex")
|
184
210
|
|
185
211
|
deprec_sch = pd.DataFrame()
|
186
212
|
deprec_sch["sector_abbr"] = financial["deprec_sch"].keys()
|
@@ -237,116 +263,62 @@ class ValueFunctions:
|
|
237
263
|
return df
|
238
264
|
|
239
265
|
def _preprocess_fom(self, df, tech="wind"):
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
"
|
246
|
-
"inflation_rate",
|
247
|
-
"dscr",
|
248
|
-
"real_discount_rate",
|
249
|
-
"term_int_rate",
|
250
|
-
"term_tenor",
|
251
|
-
f"ptc_fed_amt_{tech}",
|
252
|
-
"itc_fed_pct",
|
253
|
-
"deg",
|
254
|
-
"system_capex_per_kw",
|
255
|
-
"system_om_per_kw",
|
256
|
-
]
|
266
|
+
"""Join the core front-of-meter wind and solar financial data to the agent dataframe
|
267
|
+
(:py:attr:`df`) based on turbine/solar sizing for each parcel.
|
268
|
+
|
269
|
+
Args:
|
270
|
+
df (pd.DataFrame): The agent data.
|
271
|
+
tech (str, optional): One of "solar" or "wind". Defaults to "wind".
|
257
272
|
|
273
|
+
Returns:
|
274
|
+
pd.DataFrame: Updated agent data with key financial data attached.
|
275
|
+
"""
|
276
|
+
tech = Technology(tech)
|
258
277
|
itc_fraction_of_capex = self.FINANCIAL_INPUTS["FOM"]["itc_fraction_of_capex"]
|
259
|
-
|
260
|
-
self.year,
|
261
|
-
self.CAMBIUM_SCENARIO,
|
262
|
-
self.FINANCIAL_INPUTS["FOM"]["system_lifetime"],
|
263
|
-
self.FINANCIAL_INPUTS["FOM"]["debt_option"],
|
264
|
-
self.FINANCIAL_INPUTS["FOM"]["debt_percent"] * 100,
|
265
|
-
self.FINANCIAL_INPUTS["FOM"]["inflation"] * 100,
|
266
|
-
self.FINANCIAL_INPUTS["FOM"]["dscr"],
|
267
|
-
self.FINANCIAL_INPUTS["FOM"]["discount_rate"] * 100,
|
268
|
-
self.FINANCIAL_INPUTS["FOM"]["interest_rate"] * 100,
|
269
|
-
self.FINANCIAL_INPUTS["FOM"]["system_lifetime"],
|
270
|
-
self.
|
271
|
-
|
272
|
-
self.
|
273
|
-
self.COST_INPUTS["FOM"]["
|
274
|
-
|
275
|
-
|
276
|
-
|
278
|
+
df = df.assign(
|
279
|
+
yr=self.year.value,
|
280
|
+
cambium_scenario=self.CAMBIUM_SCENARIO,
|
281
|
+
analysis_period=self.FINANCIAL_INPUTS["FOM"]["system_lifetime"],
|
282
|
+
debt_option=self.FINANCIAL_INPUTS["FOM"]["debt_option"],
|
283
|
+
debt_percent=self.FINANCIAL_INPUTS["FOM"]["debt_percent"] * 100,
|
284
|
+
inflation_rate=self.FINANCIAL_INPUTS["FOM"]["inflation"] * 100,
|
285
|
+
dscr=self.FINANCIAL_INPUTS["FOM"]["dscr"],
|
286
|
+
real_discount_rate=self.FINANCIAL_INPUTS["FOM"]["discount_rate"] * 100,
|
287
|
+
term_int_rate=self.FINANCIAL_INPUTS["FOM"]["interest_rate"] * 100,
|
288
|
+
term_tenor=self.FINANCIAL_INPUTS["FOM"]["system_lifetime"],
|
289
|
+
itc_fed_pct=itc_fraction_of_capex if self.year != 2025 else 0.3,
|
290
|
+
deg=self.FINANCIAL_INPUTS["FOM"]["degradation"],
|
291
|
+
system_capex_per_kw=self.COST_INPUTS["FOM"]["system_capex_per_kw"][tech.value],
|
292
|
+
system_om_per_kw=self.COST_INPUTS["FOM"]["system_om_per_kw"][tech.value],
|
293
|
+
**{
|
294
|
+
f"ptc_fed_amt_{tech.value}": self.FINANCIAL_INPUTS["FOM"]["ptc_fed_dlrs_per_kwh"][
|
295
|
+
tech.value
|
296
|
+
]
|
297
|
+
},
|
298
|
+
)
|
277
299
|
# 2025 uses census-tract based applicable credit for the itc_fed_pct, so update accordingly
|
278
300
|
if self.year == 2025:
|
279
|
-
incentives =
|
280
|
-
|
281
|
-
).T
|
282
|
-
incentives.index.name = "census_tract_id"
|
301
|
+
incentives = self.FINANCIAL_INPUTS["FOM"]["itc_fraction_of_capex"]
|
302
|
+
|
283
303
|
df = df.set_index("census_tract_id", drop=False).join(incentives).reset_index(drop=True)
|
284
|
-
df.itc_fed_pct = df.applicable_credit
|
285
|
-
df.itc_fed_pct = df.itc_fed_pct.fillna(0.3)
|
304
|
+
df.itc_fed_pct = df.applicable_credit.fillna(0.3)
|
286
305
|
|
287
306
|
return df
|
288
307
|
|
289
|
-
def run(self, agents: pd.DataFrame, sector:
|
290
|
-
|
308
|
+
def run(self, agents: pd.DataFrame, sector: Sector):
|
309
|
+
"""Run a multi-threaded PySAM analysis on each agent.
|
291
310
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
results_list = []
|
297
|
-
|
298
|
-
# btw, multithreading is NOT multiprocessing
|
299
|
-
with cf.ThreadPoolExecutor(max_workers=max_w) as executor:
|
300
|
-
# log.info(
|
301
|
-
# f'....beginning multiprocess execution of valuation with {max_w} threads')
|
302
|
-
log.info(f"....beginning execution of valuation with {max_w} threads")
|
303
|
-
|
304
|
-
start = time.time()
|
305
|
-
checkpoint = max(1, int(len(agents) * verb))
|
306
|
-
|
307
|
-
# submit to worker
|
308
|
-
futures = [
|
309
|
-
executor.submit(self.worker, job, sector, self.config)
|
310
|
-
for _, job in agents.iterrows()
|
311
|
-
]
|
312
|
-
|
313
|
-
# return results *as completed* - not in same order as input
|
314
|
-
for f in cf.as_completed(futures):
|
315
|
-
results_list.append(f.result())
|
316
|
-
if len(results_list) % checkpoint == 0:
|
317
|
-
sec_per_agent = (time.time() - start) / len(results_list)
|
318
|
-
sec_per_agent = round(sec_per_agent, 3)
|
319
|
-
|
320
|
-
eta = (sec_per_agent * (len(agents) - len(results_list))) / 60 / 60
|
321
|
-
eta = round(eta, 2)
|
322
|
-
|
323
|
-
l_results = len(results_list)
|
324
|
-
l_agents = len(agents)
|
325
|
-
|
326
|
-
log.info(f"........finished job {l_results} / {l_agents}")
|
327
|
-
log.info(f"{sec_per_agent} seconds per agent")
|
328
|
-
log.info(f"ETA: {eta} hours")
|
329
|
-
else:
|
330
|
-
results_list = [self.worker(job, sector, self.config) for _, job in agents.iterrows()]
|
331
|
-
|
332
|
-
# create results df from workers
|
333
|
-
new_index = [r[0] for r in results_list]
|
334
|
-
new_dicts = [r[1] for r in results_list]
|
335
|
-
|
336
|
-
new_df = pd.DataFrame(new_dicts)
|
337
|
-
new_df["gid"] = new_index
|
338
|
-
new_df.set_index("gid", inplace=True)
|
339
|
-
|
340
|
-
# merge valuation results to agents dataframe
|
341
|
-
agents = agents.merge(new_df, on="gid", how="left")
|
342
|
-
|
343
|
-
return agents
|
311
|
+
Args:
|
312
|
+
agents (pd.DataFrame): The fully prepared agent DataFrame.
|
313
|
+
sector (:py:class:`dwind.config.Sector`): One of "fom" (front-of-meter) or "btm"
|
314
|
+
(behind-the-meter).
|
344
315
|
|
345
|
-
|
346
|
-
|
347
|
-
|
316
|
+
Returns:
|
317
|
+
pd.DataFrame: An updated version of :py:attr:`agents` with PySAM results data.
|
318
|
+
"""
|
319
|
+
if sector is Sector.BTM:
|
348
320
|
agents = self._preprocess_btm(agents)
|
349
|
-
|
321
|
+
elif sector is Sector.FOM:
|
350
322
|
agents = self._preprocess_fom(agents)
|
351
323
|
|
352
324
|
max_w = self.config.project.settings.CORES
|
@@ -354,25 +326,21 @@ class ValueFunctions:
|
|
354
326
|
|
355
327
|
if max_w > 1:
|
356
328
|
results_list = []
|
357
|
-
|
329
|
+
# with cf.ProcessPoolExecutor(max_workers=max_w, mp_context=multiprocessing.get_context("spawn")) as executor: # noqa
|
358
330
|
with cf.ProcessPoolExecutor(max_workers=max_w) as executor:
|
359
|
-
|
360
|
-
|
361
|
-
start = time.time()
|
331
|
+
start = time.perf_counter()
|
362
332
|
checkpoint = max(1, int(len(agents) * verb))
|
363
333
|
|
364
|
-
# submit to worker
|
365
334
|
futures = [
|
366
|
-
executor.submit(worker,
|
367
|
-
for _,
|
335
|
+
executor.submit(worker, row, self.config, sector)
|
336
|
+
for _, row in agents.iterrows()
|
368
337
|
]
|
369
338
|
|
370
|
-
# return results *as completed* - not in same order as input
|
371
339
|
for f in cf.as_completed(futures):
|
372
340
|
results_list.append(f.result())
|
373
341
|
|
374
342
|
if len(results_list) % checkpoint == 0:
|
375
|
-
sec_per_agent = (time.
|
343
|
+
sec_per_agent = (time.perf_counter() - start) / len(results_list)
|
376
344
|
sec_per_agent = round(sec_per_agent, 3)
|
377
345
|
|
378
346
|
eta = (sec_per_agent * (len(agents) - len(results_list))) / 60 / 60
|
@@ -385,23 +353,33 @@ class ValueFunctions:
|
|
385
353
|
log.info(f"{sec_per_agent} seconds per agent")
|
386
354
|
log.info(f"ETA: {eta} hours")
|
387
355
|
else:
|
388
|
-
results_list = [worker(job,
|
356
|
+
results_list = [worker(job, self.config, sector) for _, job in agents.iterrows()]
|
389
357
|
|
390
358
|
# create results df from workers
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
new_df["gid"] = new_index
|
396
|
-
new_df.set_index("gid", inplace=True)
|
359
|
+
results_df = pd.DataFrame.from_dict(
|
360
|
+
{k: v for d in results_list for k, v in d.items()}, orient="index"
|
361
|
+
)
|
362
|
+
results_df.index.name = "gid"
|
397
363
|
|
398
364
|
# merge valuation results to agents dataframe
|
399
|
-
agents = agents.merge(
|
400
|
-
|
365
|
+
agents = agents.merge(results_df, on="gid", how="left")
|
401
366
|
return agents
|
402
367
|
|
403
368
|
|
404
|
-
def calc_financial_performance(
|
369
|
+
def calc_financial_performance(
|
370
|
+
capex_usd_p_kw: float, row: pd.Series, loan: cashloan, batt_costs: float
|
371
|
+
) -> float:
|
372
|
+
"""Calculates the net present value (NPV) for a single agent.
|
373
|
+
|
374
|
+
Args:
|
375
|
+
capex_usd_p_kw (float): Capital expenditures per kilowatt in USD (CapEx $/kW).
|
376
|
+
row (pd.Series): Single row of the agent DataFrame.
|
377
|
+
loan (:py:class:`PySAM.Cashloan`): Configured ``cashloan`` object.
|
378
|
+
batt_costs (float): Battery costs.
|
379
|
+
|
380
|
+
Returns:
|
381
|
+
float: The NPV of the agent.
|
382
|
+
"""
|
405
383
|
system_costs = capex_usd_p_kw * row["system_size_kw"]
|
406
384
|
|
407
385
|
# calculate system costs
|
@@ -415,7 +393,18 @@ def calc_financial_performance(capex_usd_p_kw, row, loan, batt_costs):
|
|
415
393
|
return loan.Outputs.npv
|
416
394
|
|
417
395
|
|
418
|
-
def calc_financial_performance_fom(capex_usd_p_kw, row, financial):
|
396
|
+
def calc_financial_performance_fom(capex_usd_p_kw: float, row: pd.Series, financial: mp) -> float:
|
397
|
+
"""Calculates the post-tax net present value (NPV) for a single agent.
|
398
|
+
|
399
|
+
Args:
|
400
|
+
capex_usd_p_kw (float): Capital expenditures per kilowatt in USD (CapEx $/kW).
|
401
|
+
row (pd.Series): Single row of the agent DataFrame.
|
402
|
+
financial (:py:class:`PySAM.Merchantplant`): Configured :py:class:`PVWattsMerchantplant``
|
403
|
+
or :py:class:`WindPowerMerchantplant`.
|
404
|
+
|
405
|
+
Returns:
|
406
|
+
float: The post-taxs NPV of the agent.
|
407
|
+
"""
|
419
408
|
system_costs = capex_usd_p_kw * row.loc["system_size_kw"]
|
420
409
|
|
421
410
|
financial.SystemCosts.total_installed_cost = system_costs
|
@@ -426,7 +415,27 @@ def calc_financial_performance_fom(capex_usd_p_kw, row, financial):
|
|
426
415
|
return financial.Outputs.project_return_aftertax_npv
|
427
416
|
|
428
417
|
|
429
|
-
def find_cf_from_rev_wind(
|
418
|
+
def find_cf_from_rev_wind(
|
419
|
+
rev_dir: pathlib.Path,
|
420
|
+
generation_scale_offset: float,
|
421
|
+
tech_config: str,
|
422
|
+
rev_index: str,
|
423
|
+
year: int = 2018,
|
424
|
+
) -> np.ndarray:
|
425
|
+
"""Calculate the reV capacity factor.
|
426
|
+
|
427
|
+
Args:
|
428
|
+
rev_dir (pathlib.Path): Location of the pre-calculated reV results found at
|
429
|
+
``Configuration.project.rev.DIR``
|
430
|
+
generation_scale_offset (float): Generation scaling offset found in the model configuration
|
431
|
+
at ``Configuration.project.settings.GENERATION_SCALE_OFFSET.wind``.
|
432
|
+
tech_config (str): Technology configuration string
|
433
|
+
rev_index (str): The "rev_index_wind" column value for the agent.
|
434
|
+
year (int, optional): reV generation year basis. Defaults to 2018.
|
435
|
+
|
436
|
+
Returns:
|
437
|
+
np.ndarray: Array of capacity factors.
|
438
|
+
"""
|
430
439
|
file_str = rev_dir / f"rev_{tech_config}_generation_{year}.h5"
|
431
440
|
|
432
441
|
with h5.File(file_str, "r") as hf:
|
@@ -440,14 +449,33 @@ def find_cf_from_rev_wind(rev_dir, generation_scale_offset, tech_config, rev_ind
|
|
440
449
|
scale_factor = generation_scale_offset
|
441
450
|
|
442
451
|
cf_prof /= scale_factor
|
443
|
-
|
444
452
|
return cf_prof
|
445
453
|
|
446
454
|
|
447
|
-
def fetch_cambium_values(
|
455
|
+
def fetch_cambium_values(
|
456
|
+
row: pd.Series,
|
457
|
+
generation_hourly: np.ndarray,
|
458
|
+
cambium_dir: pathlib.Path,
|
459
|
+
cambium_value: str,
|
460
|
+
lower_thresh: float = 0.01,
|
461
|
+
) -> list[list[float, float], ...]:
|
462
|
+
"""Retrieves the Cambium values as an 8760 x 2 tuple of tuples.
|
463
|
+
|
464
|
+
Args:
|
465
|
+
row (pd.Series): An individual row of the agent DataFrame.
|
466
|
+
generation_hourly (np.ndarray): Hourly generation data for a single year (length 8760).
|
467
|
+
cambium_dir (pathlib.Path): Full file path for the Cambium data.
|
468
|
+
cambium_value (str): Which Cambium value to retrieve.
|
469
|
+
lower_thresh (float, optional): Lower threshold for the MerchantPlant calculations.
|
470
|
+
Defaults to 0.01.
|
471
|
+
|
472
|
+
Returns:
|
473
|
+
list[list[float, float], ...]: Hourly, single year (8760) of the clipped generation, in MW,
|
474
|
+
and value, in $/MW.
|
475
|
+
"""
|
448
476
|
# read processed cambium dataframe from pickle
|
449
477
|
cambium_f = cambium_dir / f"{row['cambium_scenario']}_pca_{row['yr']}_processed.pqt"
|
450
|
-
cambium_df = pd.read_parquet(cambium_f)
|
478
|
+
cambium_df = pd.read_parquet(cambium_f, dtype_backend="pyarrow")
|
451
479
|
|
452
480
|
cambium_df["year"] = cambium_df["year"].astype(str)
|
453
481
|
cambium_df["pca"] = cambium_df["pca"].astype(str)
|
@@ -478,24 +506,21 @@ def fetch_cambium_values(row, generation_hourly, cambium_dir, cambium_value, low
|
|
478
506
|
rev.loc[rev["cleared"] < rev["cleared"].max() * lower_thresh, "cleared"] = 0
|
479
507
|
rev["cleared"] = rev["cleared"].apply(np.floor)
|
480
508
|
|
481
|
-
|
482
|
-
tup = tuple(map(tuple, rev.values))
|
483
|
-
|
484
|
-
return tup
|
509
|
+
return rev[["cleared", "value"]].values.tolist()
|
485
510
|
|
486
511
|
|
487
|
-
def process_tariff(utilityrate, row, net_billing_sell_rate):
|
512
|
+
def process_tariff(utilityrate: ur5, row: pd.Series, net_billing_sell_rate: float) -> ur5:
|
488
513
|
"""Instantiate the utilityrate5 PySAM model and process the agent's
|
489
514
|
rate information to conform with PySAM input formatting.
|
490
515
|
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
Individual agent
|
516
|
+
Args:
|
517
|
+
utilityrate (:py:class:`PySAM.Utilityrate5`): Utilityrate5 model to be configured based
|
518
|
+
on the agent's specifications.
|
519
|
+
row (pd.Series): Individual agent row from the agent DataFrame.
|
520
|
+
net_billing_sell_rate (float): Net billing sell rate.
|
495
521
|
|
496
522
|
Returns:
|
497
|
-
|
498
|
-
utilityrate: 'PySAM.Utilityrate5'
|
523
|
+
PySAM.Utilityrate5: Configured Utilityrate5 model.
|
499
524
|
"""
|
500
525
|
# Monthly fixed charge [$]
|
501
526
|
utilityrate.ElectricityRates.ur_monthly_fixed_charge = row["ur_monthly_fixed_charge"]
|
@@ -548,13 +573,48 @@ def process_tariff(utilityrate, row, net_billing_sell_rate):
|
|
548
573
|
|
549
574
|
|
550
575
|
def find_breakeven(
|
551
|
-
row,
|
552
|
-
loan,
|
553
|
-
batt_costs,
|
554
|
-
pysam_outputs,
|
555
|
-
|
576
|
+
row: pd.Series,
|
577
|
+
loan: cashloan,
|
578
|
+
batt_costs: float,
|
579
|
+
pysam_outputs: list[str],
|
580
|
+
method: str,
|
581
|
+
*,
|
582
|
+
pre_calc_bounds_and_tolerances: bool = True,
|
556
583
|
**kwargs,
|
557
|
-
):
|
584
|
+
) -> pd.DataFrame | tuple[float, dict]:
|
585
|
+
"""Calculates the breakeven cost and parameters for a distributed wind turbine in the agent's
|
586
|
+
location.
|
587
|
+
|
588
|
+
Args:
|
589
|
+
row (pd.Series): Single row of the agent DataFrame.
|
590
|
+
loan (:py:class:`PySAM.Cashloan`): Configured ``cashloan`` object.
|
591
|
+
batt_costs (float): Battery costs.
|
592
|
+
pysam_outputs (list[str]): List of PySAM output variables to return with the breakeven
|
593
|
+
cost.
|
594
|
+
method (str): Name of the optimization strategy. Must be on of "bisect", "brentq",
|
595
|
+
"grid_search", or "newton".
|
596
|
+
pre_calc_bounds_and_tolerances (bool, optional): Flag to pre-calculate the bounds for the
|
597
|
+
bisect adn brentq optimization methods.
|
598
|
+
**kwargs (dict): Inputs for each optimization strategy. For further parameterizations,
|
599
|
+
see the documentation for the "brentq", "bisect", and "newton" methods on the
|
600
|
+
`scipy optimize API reference site`_
|
601
|
+
|
602
|
+
# TODO: parameterize the default values.
|
603
|
+
|
604
|
+
Raises:
|
605
|
+
ValueError: Raised if the optimization was unable to find a viable solution
|
606
|
+
ValueError: Raised for an unknown :py:attr:`method`.
|
607
|
+
|
608
|
+
Returns:
|
609
|
+
pd.DataFrame: Returned when :py:attr:`method` is "grid_search"
|
610
|
+
tuple[float, dict]: Returns the breakeven cost and PySAM outputs specified in
|
611
|
+
:py:attr:`pysam_outputs` when :py:attr:`method` is "brentq", "bisect", or "newton".
|
612
|
+
|
613
|
+
.. _scipy optimize API reference site:
|
614
|
+
https://docs.scipy.org/doc/scipy/reference/optimize.html#scalar-functions
|
615
|
+
"""
|
616
|
+
method = Optimization(method)
|
617
|
+
|
558
618
|
# calculate theoretical min/max NPV values
|
559
619
|
min_npv = calc_financial_performance(1e9, row, loan, batt_costs)
|
560
620
|
|
@@ -596,7 +656,7 @@ def find_breakeven(
|
|
596
656
|
# pre-calculate tolerance as a proportion of capex value pre-calculated directly above
|
597
657
|
tol = 1e-6 * ((a + b) / 2)
|
598
658
|
|
599
|
-
if
|
659
|
+
if method is Optimization.GRID_SEARCH:
|
600
660
|
if kwargs["capex_array"] is None:
|
601
661
|
capex_array = np.arange(5000.0, -500.0, -500.0)
|
602
662
|
else:
|
@@ -614,7 +674,8 @@ def find_breakeven(
|
|
614
674
|
|
615
675
|
except Exception as e:
|
616
676
|
raise ValueError("Grid search failed.") from e
|
617
|
-
|
677
|
+
|
678
|
+
if method is Optimization.BISECT:
|
618
679
|
try:
|
619
680
|
# required args for 'bisect'
|
620
681
|
if not pre_calc_bounds_and_tolerances:
|
@@ -642,14 +703,13 @@ def find_breakeven(
|
|
642
703
|
full_output=full_output,
|
643
704
|
disp=disp,
|
644
705
|
)
|
645
|
-
|
646
|
-
return breakeven_cost_usd_p_kw,
|
647
|
-
k: loan.Outputs.export().get(k, None) for k in pysam_outputs
|
648
|
-
}
|
706
|
+
results = {k: getattr(loan.Outputs, k) for k in pysam_outputs}
|
707
|
+
return breakeven_cost_usd_p_kw, results
|
649
708
|
|
650
709
|
except Exception as e:
|
651
710
|
raise ValueError("Root finding failed.") from e
|
652
|
-
|
711
|
+
|
712
|
+
if method is Optimization.BRENTQ:
|
653
713
|
try:
|
654
714
|
# required args for 'brentq'
|
655
715
|
if not pre_calc_bounds_and_tolerances:
|
@@ -678,14 +738,13 @@ def find_breakeven(
|
|
678
738
|
disp=disp,
|
679
739
|
)
|
680
740
|
|
681
|
-
|
682
|
-
|
683
|
-
}
|
741
|
+
results = {k: getattr(loan.Outputs, k) for k in pysam_outputs}
|
742
|
+
return breakeven_cost_usd_p_kw, results
|
684
743
|
|
685
744
|
except Exception as e:
|
686
745
|
raise ValueError("Root finding failed.") from e
|
687
746
|
|
688
|
-
|
747
|
+
if method is Optimization.NEWTON:
|
689
748
|
try:
|
690
749
|
# required args for 'newton'
|
691
750
|
x0 = kwargs["x0"]
|
@@ -719,24 +778,57 @@ def find_breakeven(
|
|
719
778
|
disp=disp,
|
720
779
|
)
|
721
780
|
|
722
|
-
|
723
|
-
|
724
|
-
}
|
781
|
+
results = {k: getattr(loan.Outputs, k) for k in pysam_outputs}
|
782
|
+
return breakeven_cost_usd_p_kw, results
|
725
783
|
|
726
784
|
except Exception as e:
|
727
785
|
raise ValueError("Root finding failed.") from e
|
728
786
|
|
729
|
-
|
730
|
-
raise ValueError("Invalid method passed to find_breakeven function")
|
787
|
+
raise ValueError(f"Invalid `method` ({method}) passed to `find_breakeven` function")
|
731
788
|
|
732
789
|
|
733
790
|
def find_breakeven_fom(
|
734
|
-
row,
|
735
|
-
financial,
|
736
|
-
pysam_outputs,
|
737
|
-
|
791
|
+
row: pd.Series,
|
792
|
+
financial: mp,
|
793
|
+
pysam_outputs: list[str],
|
794
|
+
method: str,
|
795
|
+
*,
|
796
|
+
pre_calc_bounds_and_tolerances: bool = True,
|
738
797
|
**kwargs,
|
739
|
-
):
|
798
|
+
) -> pd.DataFrame | tuple[float, dict]:
|
799
|
+
"""Calculates the breakeven cost and parameters for a front-of-meter distributed wind turbine
|
800
|
+
in the agent's location.
|
801
|
+
|
802
|
+
Args:
|
803
|
+
row (pd.Series): Single row of the agent DataFrame.
|
804
|
+
financial (:py:class:`PySAM.Merchantplant`): Configured ``PVWattsMerchantPlant`` or
|
805
|
+
:py:class:`PySAM.WindPowerMerchantplant` object.
|
806
|
+
pysam_outputs (list[str]): List of PySAM output variables to return with the breakeven
|
807
|
+
cost.
|
808
|
+
method (str): Name of the optimization strategy. Must be on of "bisect", "brentq",
|
809
|
+
"grid_search", or "newton".
|
810
|
+
pre_calc_bounds_and_tolerances (bool, optional): Flag to pre-calculate the bounds for the
|
811
|
+
bisect adn brentq optimization methods.
|
812
|
+
**kwargs (dict): Inputs for each optimization strategy. For further parameterizations,
|
813
|
+
see the documentation for the "brentq", "bisect", and "newton" methods on the
|
814
|
+
`scipy optimize API reference site`_
|
815
|
+
|
816
|
+
# TODO: parameterize the default values.
|
817
|
+
|
818
|
+
Raises:
|
819
|
+
ValueError: Raised if the optimization was unable to find a viable solution
|
820
|
+
ValueError: Raised for an unknown :py:attr:`method`.
|
821
|
+
|
822
|
+
Returns:
|
823
|
+
pd.DataFrame: Returned when :py:attr:`method` is "grid_search"
|
824
|
+
tuple[float, dict]: Returns the breakeven cost and PySAM outputs specified in
|
825
|
+
:py:attr:`pysam_outputs` when :py:attr:`method` is "brentq", "bisect", or "newton".
|
826
|
+
|
827
|
+
.. _scipy optimize API reference site:
|
828
|
+
https://docs.scipy.org/doc/scipy/reference/optimize.html#scalar-functions
|
829
|
+
"""
|
830
|
+
method = Optimization(method)
|
831
|
+
|
740
832
|
# calculate theoretical min/max NPV values
|
741
833
|
min_npv = calc_financial_performance_fom(1e9, row, financial)
|
742
834
|
|
@@ -778,7 +870,7 @@ def find_breakeven_fom(
|
|
778
870
|
# capex value pre-calculated directly above
|
779
871
|
tol = 1e-6 * ((a + b) / 2)
|
780
872
|
|
781
|
-
if
|
873
|
+
if method is Optimization.GRID_SEARCH:
|
782
874
|
if kwargs["capex_array"] is None:
|
783
875
|
capex_array = np.arange(5000.0, -500.0, -500.0)
|
784
876
|
else:
|
@@ -796,7 +888,8 @@ def find_breakeven_fom(
|
|
796
888
|
|
797
889
|
except Exception as e:
|
798
890
|
raise ValueError("Grid search failed.") from e
|
799
|
-
|
891
|
+
|
892
|
+
if method is Optimization.BISECT:
|
800
893
|
try:
|
801
894
|
# required args for 'bisect
|
802
895
|
if not pre_calc_bounds_and_tolerances:
|
@@ -825,13 +918,13 @@ def find_breakeven_fom(
|
|
825
918
|
disp=disp,
|
826
919
|
)
|
827
920
|
|
828
|
-
|
829
|
-
|
830
|
-
}
|
921
|
+
results = {k: getattr(financial.Outputs, k) for k in pysam_outputs}
|
922
|
+
return breakeven_cost_usd_p_kw, results
|
831
923
|
|
832
924
|
except Exception as e:
|
833
925
|
raise ValueError("Root finding failed.") from e
|
834
|
-
|
926
|
+
|
927
|
+
if method is Optimization.BRENTQ:
|
835
928
|
try:
|
836
929
|
# required args for 'brentq'
|
837
930
|
if not pre_calc_bounds_and_tolerances:
|
@@ -860,13 +953,13 @@ def find_breakeven_fom(
|
|
860
953
|
disp=disp,
|
861
954
|
)
|
862
955
|
|
863
|
-
|
864
|
-
|
865
|
-
}
|
956
|
+
results = {k: getattr(financial.Outputs, k) for k in pysam_outputs}
|
957
|
+
return breakeven_cost_usd_p_kw, results
|
866
958
|
|
867
959
|
except Exception as e:
|
868
960
|
raise ValueError("Root finding failed.") from e
|
869
|
-
|
961
|
+
|
962
|
+
if method is Optimization.NEWTON:
|
870
963
|
try:
|
871
964
|
# required args for 'newton'
|
872
965
|
x0 = kwargs["x0"]
|
@@ -900,57 +993,46 @@ def find_breakeven_fom(
|
|
900
993
|
disp=disp,
|
901
994
|
)
|
902
995
|
|
903
|
-
|
904
|
-
|
905
|
-
}
|
996
|
+
results = {k: getattr(financial.Outputs, k) for k in pysam_outputs}
|
997
|
+
return breakeven_cost_usd_p_kw, results
|
906
998
|
|
907
999
|
except Exception as e:
|
908
1000
|
raise ValueError("Root finding failed") from e
|
909
|
-
|
910
|
-
|
1001
|
+
|
1002
|
+
raise ValueError("Invalid method passed to find_breakeven function")
|
911
1003
|
|
912
1004
|
|
913
1005
|
def process_btm(
|
914
|
-
row,
|
915
|
-
tech,
|
916
|
-
generation_hourly,
|
917
|
-
consumption_hourly,
|
918
|
-
pysam_outputs,
|
1006
|
+
row: pd.Series,
|
1007
|
+
tech: Technology,
|
1008
|
+
generation_hourly: np.ndarray,
|
1009
|
+
consumption_hourly: np.ndarray,
|
1010
|
+
pysam_outputs: list[str],
|
1011
|
+
batt_dispatch: str | None = None,
|
1012
|
+
*,
|
919
1013
|
en_batt=False,
|
920
|
-
batt_dispatch=None,
|
921
1014
|
):
|
922
|
-
"""Behind-the-meter
|
923
|
-
|
924
|
-
This function processes a BTM agent by:
|
925
|
-
1)
|
926
|
-
|
927
|
-
Parameters
|
928
|
-
----------
|
929
|
-
**row** : 'DataFrame row'
|
930
|
-
The row of the dataframe on which the function is performed
|
931
|
-
**exported_hourly** : ''
|
932
|
-
8760 of generation
|
933
|
-
**consumption_hourly** : ''
|
934
|
-
8760 of consumption
|
935
|
-
**tariff_dict** : 'dict'
|
936
|
-
Dictionary containing tariff parameters
|
937
|
-
**btm_nem** : 'bool'
|
938
|
-
Enable NEM for BTM parcels
|
939
|
-
**en_batt** : 'bool'
|
940
|
-
Enable battery modeling
|
941
|
-
**batt_dispatch** : 'string'
|
942
|
-
Specify battery dispatch strategy type
|
1015
|
+
"""Behind-the-meter calculation for a single agent.
|
943
1016
|
|
944
|
-
|
945
|
-
-------
|
1017
|
+
TODO: list out the actual process
|
946
1018
|
|
1019
|
+
Args:
|
1020
|
+
row (pandas.Series): The row of the dataframe on which the function is performed.
|
1021
|
+
tech (dwind.config.Technology): Validated :py:class:`dwind.config.Technology` object.
|
1022
|
+
generation_hourly (np.ndarray): Hourly generation for a single year (8760 hours).
|
1023
|
+
consumption_hourly (np.ndarray): Hourly consumption for a single year (8760 hours).
|
1024
|
+
pysam_outputs (list[str]): List of desired PySAM output data.
|
1025
|
+
batt_dispatch (str, optional): Battery dispatch strategy type.
|
1026
|
+
en_batt (bool, optional): Enable battery modeling if True. Defaults to False.
|
1027
|
+
|
1028
|
+
Returns:
|
1029
|
+
pd.Series: Updated :py:attr:`row` with PySAM and breakeven cost results.
|
947
1030
|
"""
|
948
1031
|
# extract agent load and generation profiles
|
949
1032
|
generation_hourly = np.array(generation_hourly)
|
950
|
-
consumption_hourly = np.array(consumption_hourly, dtype=np.float32)
|
951
1033
|
|
952
1034
|
# specify tech-agnostic system size column
|
953
|
-
row["system_size_kw"] = row[f"{tech}_size_kw_btm"]
|
1035
|
+
row["system_size_kw"] = row[f"{tech.value}_size_kw_btm"]
|
954
1036
|
|
955
1037
|
# instantiate PySAM battery model based on agent sector
|
956
1038
|
if row.loc["sector_abbr"] == "res":
|
@@ -1312,7 +1394,7 @@ def process_btm(
|
|
1312
1394
|
|
1313
1395
|
_ = calc_financial_performance(row["system_capex_per_kw"], row, loan, batt_costs)
|
1314
1396
|
|
1315
|
-
row["additional_pysam_outputs"] = {k: loan.Outputs
|
1397
|
+
row["additional_pysam_outputs"] = {k: getattr(loan.Outputs, k) for k in pysam_outputs}
|
1316
1398
|
|
1317
1399
|
# run root finding algorithm to find breakeven cost based on calculated NPV
|
1318
1400
|
out, _ = find_breakeven(
|
@@ -1320,8 +1402,9 @@ def process_btm(
|
|
1320
1402
|
loan=loan,
|
1321
1403
|
pysam_outputs=pysam_outputs,
|
1322
1404
|
batt_costs=batt_costs,
|
1405
|
+
method="newton",
|
1323
1406
|
pre_calc_bounds_and_tolerances=False,
|
1324
|
-
**{"
|
1407
|
+
**{"x0": 10000.0, "full_output": True},
|
1325
1408
|
)
|
1326
1409
|
|
1327
1410
|
row["breakeven_cost_usd_p_kw"] = out
|
@@ -1330,40 +1413,39 @@ def process_btm(
|
|
1330
1413
|
|
1331
1414
|
|
1332
1415
|
def process_fom(
|
1333
|
-
row,
|
1334
|
-
tech,
|
1335
|
-
generation_hourly,
|
1336
|
-
market_profile,
|
1337
|
-
pysam_outputs,
|
1338
|
-
|
1339
|
-
|
1416
|
+
row: pd.Series,
|
1417
|
+
tech: Technology,
|
1418
|
+
generation_hourly: np.ndarray,
|
1419
|
+
market_profile: list[list[float, float], ...],
|
1420
|
+
pysam_outputs: list[str],
|
1421
|
+
batt_dispatch: str | None = None,
|
1422
|
+
*,
|
1423
|
+
en_batt: bool = False,
|
1340
1424
|
):
|
1341
|
-
"""Front-of-
|
1425
|
+
"""Front-of-meter calculation for a single agent.
|
1342
1426
|
|
1343
|
-
|
1344
|
-
1)
|
1427
|
+
TODO: list out the actual process
|
1345
1428
|
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1429
|
+
Args:
|
1430
|
+
row (pandas.Series): The row of the dataframe on which the function is performed.
|
1431
|
+
tech (dwind.config.Technology): Validated :py:class:`dwind.config.Technology` object.
|
1432
|
+
generation_hourly (np.ndarray): Hourly generation for a single year (8760 hours).
|
1433
|
+
market_profile (list[list[float, float], ...]): Hourly Cambium values for a single year.
|
1434
|
+
pysam_outputs (list[str]): List of desired PySAM output data.
|
1435
|
+
batt_dispatch (str, optional): Battery dispatch strategy type.
|
1436
|
+
en_batt (bool, optional): Enable battery modeling if True. Defaults to False.
|
1354
1437
|
|
1355
1438
|
Returns:
|
1356
|
-
|
1357
|
-
|
1439
|
+
pd.Series: Updated :py:attr:`row` with PySAM and breakeven cost results.
|
1358
1440
|
"""
|
1359
1441
|
# extract generation profile
|
1360
1442
|
generation_hourly = np.array(generation_hourly)
|
1361
1443
|
|
1362
1444
|
# specify tech-agnostic system size column
|
1363
|
-
row["system_size_kw"] = row[f"{tech}_size_kw_fom"]
|
1445
|
+
row["system_size_kw"] = row[f"{tech.value}_size_kw_fom"]
|
1364
1446
|
|
1365
1447
|
inv_eff = 1.0 # required inverter efficiency for FOM systems
|
1366
|
-
gen =
|
1448
|
+
gen = (generation_hourly * inv_eff).tolist()
|
1367
1449
|
|
1368
1450
|
# set up battery, with system generation conditional
|
1369
1451
|
# on the battery generation being included
|
@@ -1372,15 +1454,15 @@ def process_fom(
|
|
1372
1454
|
pass
|
1373
1455
|
else:
|
1374
1456
|
# initialize PySAM model
|
1375
|
-
if tech
|
1457
|
+
if tech is Technology.SOLAR:
|
1376
1458
|
financial = mp.default("PVWattsMerchantPlant")
|
1377
|
-
elif tech
|
1459
|
+
elif tech is Technology.WIND:
|
1378
1460
|
financial = mp.default("WindPowerMerchantPlant")
|
1379
1461
|
else:
|
1380
1462
|
msg = "Please write a wrapper to account for the new technology type"
|
1381
|
-
raise NotImplementedError(f"{msg} {tech}")
|
1463
|
+
raise NotImplementedError(f"{msg} {tech.value}")
|
1382
1464
|
|
1383
|
-
ptc_fed_amt = row[f"ptc_fed_amt_{tech}"]
|
1465
|
+
ptc_fed_amt = row[f"ptc_fed_amt_{tech.value}"]
|
1384
1466
|
itc_fed_pct = row["itc_fed_pct"]
|
1385
1467
|
deg = row["deg"]
|
1386
1468
|
system_capex_per_kw = row["system_capex_per_kw"]
|
@@ -1448,10 +1530,12 @@ def process_fom(
|
|
1448
1530
|
financial.Revenue.mp_enable_ancserv2 = 0
|
1449
1531
|
financial.Revenue.mp_enable_ancserv3 = 0
|
1450
1532
|
financial.Revenue.mp_enable_ancserv4 = 0
|
1451
|
-
|
1452
|
-
|
1453
|
-
financial.Revenue.
|
1454
|
-
financial.Revenue.
|
1533
|
+
|
1534
|
+
N = len(market_profile)
|
1535
|
+
financial.Revenue.mp_ancserv1_revenue = [(0, 0)] * N
|
1536
|
+
financial.Revenue.mp_ancserv2_revenue = [(0, 0)] * N
|
1537
|
+
financial.Revenue.mp_ancserv3_revenue = [(0, 0)] * N
|
1538
|
+
financial.Revenue.mp_ancserv4_revenue = [(0, 0)] * N
|
1455
1539
|
|
1456
1540
|
financial.CapacityPayments.cp_capacity_payment_type = 0
|
1457
1541
|
financial.CapacityPayments.cp_capacity_payment_amount = [0]
|
@@ -1469,9 +1553,7 @@ def process_fom(
|
|
1469
1553
|
|
1470
1554
|
log.info(f"row {row.loc['gid']} calculating financial performance")
|
1471
1555
|
_ = calc_financial_performance_fom(system_capex_per_kw, row, financial)
|
1472
|
-
row["additional_pysam_outputs"] = {
|
1473
|
-
k: financial.Outputs.export().get(k, None) for k in pysam_outputs
|
1474
|
-
}
|
1556
|
+
row["additional_pysam_outputs"] = {k: getattr(financial.Outputs, k) for k in pysam_outputs}
|
1475
1557
|
|
1476
1558
|
# run root finding algorithm to find breakeven cost based on calculated NPV
|
1477
1559
|
log.info(f"row {row.loc['gid']} breakeven")
|
@@ -1488,52 +1570,76 @@ def process_fom(
|
|
1488
1570
|
return row
|
1489
1571
|
|
1490
1572
|
|
1491
|
-
def worker(row: pd.Series,
|
1573
|
+
def worker(row: pd.Series, config: Configuration, sector: Sector) -> tuple[str, dict]:
|
1574
|
+
"""Individual calculation process meant to be run in parallel.
|
1575
|
+
|
1576
|
+
Args:
|
1577
|
+
row (pd.Series): Individual row of from the agent DataFraem to analyze.
|
1578
|
+
config (Configuration): The overarching dwind configuration.
|
1579
|
+
sector (Sector): One of "fom" or "btm".
|
1580
|
+
|
1581
|
+
Returns:
|
1582
|
+
tuple[str, dict]: "gid" column of :py:attr:`row` and the dictionary
|
1583
|
+
of results for that agent.
|
1584
|
+
"""
|
1492
1585
|
try:
|
1493
1586
|
results = {}
|
1494
1587
|
for tech in config.project.settings.TECHS:
|
1495
|
-
|
1588
|
+
tech = Technology(tech)
|
1589
|
+
if tech is Technology.WIND:
|
1496
1590
|
tech_config = row["turbine_class"]
|
1497
|
-
elif tech
|
1591
|
+
elif tech is Technology.SOLAR:
|
1498
1592
|
# TODO: Not updated for the actual configuration, so solar is likely out of date
|
1499
|
-
azimuth = config.rev.settings.azimuth_direction_to_degree[
|
1500
|
-
|
1501
|
-
|
1593
|
+
azimuth = config.rev.settings.azimuth_direction_to_degree[
|
1594
|
+
row[f"azimuth_{sector.value}"]
|
1595
|
+
]
|
1596
|
+
tilt = row[f"tilt_{sector.value}"]
|
1597
|
+
str(azimuth) + "_" + str(tilt)
|
1502
1598
|
|
1503
|
-
if row[f"{tech}_size_kw_{sector}"] > 0:
|
1599
|
+
if row[f"{tech.value}_size_kw_{sector.value}"] > 0:
|
1504
1600
|
tech_config = row["turbine_class"]
|
1505
1601
|
cf_hourly = find_cf_from_rev_wind(
|
1506
|
-
config.rev.DIR,
|
1507
|
-
config.project.settings.GENERATION_SCALE_OFFSET["wind"],
|
1508
|
-
|
1509
|
-
|
1602
|
+
rev_dir=config.rev.DIR,
|
1603
|
+
generation_scale_offset=config.project.settings.GENERATION_SCALE_OFFSET["wind"],
|
1604
|
+
rev_index=row[f"rev_index_{tech.value}"],
|
1605
|
+
tech_config=tech_config,
|
1510
1606
|
)
|
1511
|
-
generation_hourly = cf_hourly * row[f"{tech}_size_kw_{sector}"]
|
1607
|
+
generation_hourly = cf_hourly * row[f"{tech.value}_size_kw_{sector.value}"]
|
1512
1608
|
generation_hourly = generation_hourly.round(2)
|
1513
1609
|
generation_hourly = np.array(generation_hourly).astype(np.float32).round(2)
|
1514
1610
|
else:
|
1515
1611
|
# if system size is zero, return dummy values
|
1516
|
-
results[f"{tech}_breakeven_cost_{sector}"] = -1
|
1517
|
-
results[f"{tech}_pysam_outputs_{sector}"] = {
|
1612
|
+
results[f"{tech.value}_breakeven_cost_{sector.value}"] = -1
|
1613
|
+
results[f"{tech.value}_pysam_outputs_{sector.value}"] = {
|
1614
|
+
"msg": "System size is zero"
|
1615
|
+
}
|
1518
1616
|
continue
|
1519
1617
|
|
1520
|
-
if sector
|
1618
|
+
if sector is Sector.BTM:
|
1619
|
+
with h5.File("/projects/dwind/data/crb_consumption_hourly.h5") as hf:
|
1620
|
+
c, h = row.crb_model_index, row.hdf_index
|
1621
|
+
consumption_hourly = hf["consumption"][c, h, :].astype(np.float32)
|
1622
|
+
consumption_hourly /= 1e8
|
1623
|
+
consumption_hourly = (
|
1624
|
+
consumption_hourly / consumption_hourly.sum() * row.load_kwh
|
1625
|
+
)
|
1626
|
+
|
1521
1627
|
row = process_btm(
|
1522
1628
|
row=row,
|
1523
1629
|
tech=tech,
|
1524
1630
|
generation_hourly=generation_hourly,
|
1525
|
-
consumption_hourly=
|
1631
|
+
consumption_hourly=consumption_hourly,
|
1526
1632
|
pysam_outputs=config.pysam.outputs.btm,
|
1527
1633
|
en_batt=False,
|
1528
1634
|
batt_dispatch=None, # TODO: enable battery switch earlier
|
1529
1635
|
)
|
1530
|
-
|
1636
|
+
elif sector is Sector.FOM:
|
1531
1637
|
# fetch 8760 cambium values for FOM
|
1532
1638
|
market_profile = fetch_cambium_values(
|
1533
1639
|
row,
|
1534
1640
|
generation_hourly,
|
1535
1641
|
config.project.settings.CAMBIUM_DATA_DIR,
|
1536
|
-
config.CAMBIUM_VALUE,
|
1642
|
+
config.project.settings.CAMBIUM_VALUE,
|
1537
1643
|
)
|
1538
1644
|
|
1539
1645
|
row = process_fom(
|
@@ -1547,10 +1653,10 @@ def worker(row: pd.Series, sector: str, config: Configuration):
|
|
1547
1653
|
)
|
1548
1654
|
|
1549
1655
|
# store results in dictionary
|
1550
|
-
results[f"{tech}_breakeven_cost_{sector}"] = row["breakeven_cost_usd_p_kw"]
|
1551
|
-
results
|
1656
|
+
results[f"{tech.value}_breakeven_cost_{sector.value}"] = row["breakeven_cost_usd_p_kw"]
|
1657
|
+
results |= row["additional_pysam_outputs"]
|
1552
1658
|
|
1553
|
-
return
|
1659
|
+
return {row["gid"]: results}
|
1554
1660
|
|
1555
1661
|
except Exception as e:
|
1556
1662
|
log.exception(e)
|