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/valuation.py
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
-
|
1
|
+
"""Provides the core value calculation methods."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
2
5
|
import time
|
3
6
|
import logging
|
7
|
+
import pathlib
|
4
8
|
import functools
|
5
9
|
import concurrent.futures as cf
|
6
10
|
|
@@ -14,32 +18,37 @@ import PySAM.Utilityrate5 as ur5
|
|
14
18
|
import PySAM.Merchantplant as mp
|
15
19
|
from scipy import optimize
|
16
20
|
|
17
|
-
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
|
18
24
|
|
19
25
|
|
20
26
|
log = logging.getLogger("dwfs")
|
21
27
|
|
22
28
|
|
23
29
|
class ValueFunctions:
|
30
|
+
"""Primary model calculation engine responsible for the computation of individual agents."""
|
31
|
+
|
24
32
|
def __init__(
|
25
33
|
self,
|
26
|
-
scenario: str,
|
27
|
-
year: int,
|
34
|
+
scenario: str | Scenario,
|
35
|
+
year: int | Year,
|
28
36
|
configuration: Configuration,
|
29
|
-
return_format="totals",
|
37
|
+
return_format: str = "totals",
|
30
38
|
):
|
31
|
-
"""
|
39
|
+
"""Creates an instance of the valuation class.
|
32
40
|
|
33
41
|
Args:
|
34
42
|
scenario (str): Only option is "baseline" currently.
|
35
43
|
year (int): Analysis year.
|
36
44
|
configuration (dwind.config.Configuration): Model configuration with universal settings.
|
37
|
-
return_format
|
38
|
-
|
39
|
-
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".
|
40
49
|
"""
|
41
|
-
self.scenario = scenario
|
42
|
-
self.year = year
|
50
|
+
self.scenario = Scenario(scenario)
|
51
|
+
self.year = Year(year)
|
43
52
|
self.config = configuration
|
44
53
|
self.return_format = return_format
|
45
54
|
|
@@ -51,6 +60,7 @@ class ValueFunctions:
|
|
51
60
|
self.load()
|
52
61
|
|
53
62
|
def load(self):
|
63
|
+
"""Loads all the core data from CSV and SQL for configuring PySAM."""
|
54
64
|
_load_csv = functools.partial(loader.load_df, year=self.year)
|
55
65
|
_load_sql = functools.partial(
|
56
66
|
loader.load_df,
|
@@ -121,9 +131,18 @@ class ValueFunctions:
|
|
121
131
|
costs["system_variable_om_per_kw"] = cost_inputs["system_variable_om_per_kw"][tech]
|
122
132
|
return costs
|
123
133
|
|
124
|
-
def _preprocess_btm(self, df, tech="wind"):
|
125
|
-
|
126
|
-
|
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)
|
127
146
|
df["county_id_int"] = df.county_id.astype(int)
|
128
147
|
|
129
148
|
# Get the electricity rates
|
@@ -144,8 +163,12 @@ class ValueFunctions:
|
|
144
163
|
df = df.drop(columns="county_id_int")
|
145
164
|
|
146
165
|
# Technology-specific factors
|
147
|
-
|
148
|
-
|
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)
|
149
172
|
df = pd.merge(
|
150
173
|
df,
|
151
174
|
cost_df,
|
@@ -153,15 +176,15 @@ class ValueFunctions:
|
|
153
176
|
left_on=tech_join,
|
154
177
|
right_on=tech_join,
|
155
178
|
)
|
156
|
-
if tech
|
179
|
+
if tech is Technology.SOLAR:
|
157
180
|
df = pd.merge(
|
158
181
|
df,
|
159
|
-
self.PERFORMANCE_INPUTS[tech],
|
182
|
+
self.PERFORMANCE_INPUTS[tech.value],
|
160
183
|
how="left",
|
161
184
|
left_on=tech_join,
|
162
185
|
right_on=tech_join,
|
163
186
|
)
|
164
|
-
|
187
|
+
elif tech is Technology.WIND:
|
165
188
|
df = pd.merge(
|
166
189
|
df,
|
167
190
|
self.wind_tech_inputs[["turbine_size_kw", "perf_improvement_factor"]],
|
@@ -240,9 +263,20 @@ class ValueFunctions:
|
|
240
263
|
return df
|
241
264
|
|
242
265
|
def _preprocess_fom(self, df, tech="wind"):
|
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".
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
pd.DataFrame: Updated agent data with key financial data attached.
|
275
|
+
"""
|
276
|
+
tech = Technology(tech)
|
243
277
|
itc_fraction_of_capex = self.FINANCIAL_INPUTS["FOM"]["itc_fraction_of_capex"]
|
244
278
|
df = df.assign(
|
245
|
-
yr=self.year,
|
279
|
+
yr=self.year.value,
|
246
280
|
cambium_scenario=self.CAMBIUM_SCENARIO,
|
247
281
|
analysis_period=self.FINANCIAL_INPUTS["FOM"]["system_lifetime"],
|
248
282
|
debt_option=self.FINANCIAL_INPUTS["FOM"]["debt_option"],
|
@@ -254,9 +288,13 @@ class ValueFunctions:
|
|
254
288
|
term_tenor=self.FINANCIAL_INPUTS["FOM"]["system_lifetime"],
|
255
289
|
itc_fed_pct=itc_fraction_of_capex if self.year != 2025 else 0.3,
|
256
290
|
deg=self.FINANCIAL_INPUTS["FOM"]["degradation"],
|
257
|
-
system_capex_per_kw=self.COST_INPUTS["FOM"]["system_capex_per_kw"][tech],
|
258
|
-
system_om_per_kw=self.COST_INPUTS["FOM"]["system_om_per_kw"][tech],
|
259
|
-
**{
|
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
|
+
},
|
260
298
|
)
|
261
299
|
# 2025 uses census-tract based applicable credit for the itc_fed_pct, so update accordingly
|
262
300
|
if self.year == 2025:
|
@@ -267,96 +305,42 @@ class ValueFunctions:
|
|
267
305
|
|
268
306
|
return df
|
269
307
|
|
270
|
-
def run(self, agents: pd.DataFrame, sector:
|
271
|
-
|
272
|
-
|
273
|
-
max_w = self.config.project.settings.THREAD_WORKERS
|
274
|
-
verb = self.config.project.settings.VERBOSITY
|
275
|
-
|
276
|
-
if max_w > 1:
|
277
|
-
results_list = []
|
278
|
-
|
279
|
-
# btw, multithreading is NOT multiprocessing
|
280
|
-
with cf.ThreadPoolExecutor(max_workers=max_w) as executor:
|
281
|
-
# log.info(
|
282
|
-
# f'....beginning multiprocess execution of valuation with {max_w} threads')
|
283
|
-
log.info(f"....beginning execution of valuation with {max_w} threads")
|
284
|
-
|
285
|
-
start = time.time()
|
286
|
-
checkpoint = max(1, int(len(agents) * verb))
|
287
|
-
|
288
|
-
# submit to worker
|
289
|
-
futures = [
|
290
|
-
executor.submit(self.worker, job, sector, self.config)
|
291
|
-
for _, job in agents.iterrows()
|
292
|
-
]
|
293
|
-
|
294
|
-
# return results *as completed* - not in same order as input
|
295
|
-
for f in cf.as_completed(futures):
|
296
|
-
results_list.append(f.result())
|
297
|
-
if len(results_list) % checkpoint == 0:
|
298
|
-
sec_per_agent = (time.time() - start) / len(results_list)
|
299
|
-
sec_per_agent = round(sec_per_agent, 3)
|
300
|
-
|
301
|
-
eta = (sec_per_agent * (len(agents) - len(results_list))) / 60 / 60
|
302
|
-
eta = round(eta, 2)
|
303
|
-
|
304
|
-
l_results = len(results_list)
|
305
|
-
l_agents = len(agents)
|
306
|
-
|
307
|
-
log.info(f"........finished job {l_results} / {l_agents}")
|
308
|
-
log.info(f"{sec_per_agent} seconds per agent")
|
309
|
-
log.info(f"ETA: {eta} hours")
|
310
|
-
else:
|
311
|
-
results_list = [self.worker(job, sector, self.config) for _, job in agents.iterrows()]
|
312
|
-
|
313
|
-
# create results df from workers
|
314
|
-
new_index = [r[0] for r in results_list]
|
315
|
-
new_dicts = [r[1] for r in results_list]
|
316
|
-
|
317
|
-
new_df = pd.DataFrame(new_dicts)
|
318
|
-
new_df["gid"] = new_index
|
319
|
-
new_df.set_index("gid", inplace=True)
|
320
|
-
|
321
|
-
# merge valuation results to agents dataframe
|
322
|
-
agents = agents.merge(new_df, on="gid", how="left")
|
308
|
+
def run(self, agents: pd.DataFrame, sector: Sector):
|
309
|
+
"""Run a multi-threaded PySAM analysis on each agent.
|
323
310
|
|
324
|
-
|
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).
|
325
315
|
|
326
|
-
|
327
|
-
|
328
|
-
|
316
|
+
Returns:
|
317
|
+
pd.DataFrame: An updated version of :py:attr:`agents` with PySAM results data.
|
318
|
+
"""
|
319
|
+
if sector is Sector.BTM:
|
329
320
|
agents = self._preprocess_btm(agents)
|
330
|
-
|
321
|
+
elif sector is Sector.FOM:
|
331
322
|
agents = self._preprocess_fom(agents)
|
332
323
|
|
333
324
|
max_w = self.config.project.settings.CORES
|
334
325
|
verb = self.config.project.settings.VERBOSITY
|
335
326
|
|
336
327
|
if max_w > 1:
|
337
|
-
# Override project-level setting to ensure memory intensive calculations don't
|
338
|
-
# cause jobs to silently fail
|
339
|
-
max_w = min(int(os.cpu_count() * 0.8), max_w)
|
340
328
|
results_list = []
|
341
|
-
|
329
|
+
# with cf.ProcessPoolExecutor(max_workers=max_w, mp_context=multiprocessing.get_context("spawn")) as executor: # noqa
|
342
330
|
with cf.ProcessPoolExecutor(max_workers=max_w) as executor:
|
343
|
-
|
344
|
-
|
345
|
-
start = time.time()
|
331
|
+
start = time.perf_counter()
|
346
332
|
checkpoint = max(1, int(len(agents) * verb))
|
347
333
|
|
348
|
-
# submit to worker
|
349
334
|
futures = [
|
350
|
-
executor.submit(worker,
|
351
|
-
for _,
|
335
|
+
executor.submit(worker, row, self.config, sector)
|
336
|
+
for _, row in agents.iterrows()
|
352
337
|
]
|
353
338
|
|
354
|
-
# return results *as completed* - not in same order as input
|
355
339
|
for f in cf.as_completed(futures):
|
356
340
|
results_list.append(f.result())
|
357
341
|
|
358
342
|
if len(results_list) % checkpoint == 0:
|
359
|
-
sec_per_agent = (time.
|
343
|
+
sec_per_agent = (time.perf_counter() - start) / len(results_list)
|
360
344
|
sec_per_agent = round(sec_per_agent, 3)
|
361
345
|
|
362
346
|
eta = (sec_per_agent * (len(agents) - len(results_list))) / 60 / 60
|
@@ -369,23 +353,33 @@ class ValueFunctions:
|
|
369
353
|
log.info(f"{sec_per_agent} seconds per agent")
|
370
354
|
log.info(f"ETA: {eta} hours")
|
371
355
|
else:
|
372
|
-
results_list = [worker(job,
|
356
|
+
results_list = [worker(job, self.config, sector) for _, job in agents.iterrows()]
|
373
357
|
|
374
358
|
# create results df from workers
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
new_df["gid"] = new_index
|
380
|
-
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"
|
381
363
|
|
382
364
|
# merge valuation results to agents dataframe
|
383
|
-
agents = agents.merge(
|
384
|
-
|
365
|
+
agents = agents.merge(results_df, on="gid", how="left")
|
385
366
|
return agents
|
386
367
|
|
387
368
|
|
388
|
-
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
|
+
"""
|
389
383
|
system_costs = capex_usd_p_kw * row["system_size_kw"]
|
390
384
|
|
391
385
|
# calculate system costs
|
@@ -399,7 +393,18 @@ def calc_financial_performance(capex_usd_p_kw, row, loan, batt_costs):
|
|
399
393
|
return loan.Outputs.npv
|
400
394
|
|
401
395
|
|
402
|
-
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
|
+
"""
|
403
408
|
system_costs = capex_usd_p_kw * row.loc["system_size_kw"]
|
404
409
|
|
405
410
|
financial.SystemCosts.total_installed_cost = system_costs
|
@@ -410,7 +415,27 @@ def calc_financial_performance_fom(capex_usd_p_kw, row, financial):
|
|
410
415
|
return financial.Outputs.project_return_aftertax_npv
|
411
416
|
|
412
417
|
|
413
|
-
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
|
+
"""
|
414
439
|
file_str = rev_dir / f"rev_{tech_config}_generation_{year}.h5"
|
415
440
|
|
416
441
|
with h5.File(file_str, "r") as hf:
|
@@ -424,14 +449,33 @@ def find_cf_from_rev_wind(rev_dir, generation_scale_offset, tech_config, rev_ind
|
|
424
449
|
scale_factor = generation_scale_offset
|
425
450
|
|
426
451
|
cf_prof /= scale_factor
|
427
|
-
|
428
452
|
return cf_prof
|
429
453
|
|
430
454
|
|
431
|
-
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
|
+
"""
|
432
476
|
# read processed cambium dataframe from pickle
|
433
477
|
cambium_f = cambium_dir / f"{row['cambium_scenario']}_pca_{row['yr']}_processed.pqt"
|
434
|
-
cambium_df = pd.read_parquet(cambium_f)
|
478
|
+
cambium_df = pd.read_parquet(cambium_f, dtype_backend="pyarrow")
|
435
479
|
|
436
480
|
cambium_df["year"] = cambium_df["year"].astype(str)
|
437
481
|
cambium_df["pca"] = cambium_df["pca"].astype(str)
|
@@ -462,24 +506,21 @@ def fetch_cambium_values(row, generation_hourly, cambium_dir, cambium_value, low
|
|
462
506
|
rev.loc[rev["cleared"] < rev["cleared"].max() * lower_thresh, "cleared"] = 0
|
463
507
|
rev["cleared"] = rev["cleared"].apply(np.floor)
|
464
508
|
|
465
|
-
|
466
|
-
tup = tuple(map(tuple, rev.values.tolist()))
|
467
|
-
|
468
|
-
return tup
|
509
|
+
return rev[["cleared", "value"]].values.tolist()
|
469
510
|
|
470
511
|
|
471
|
-
def process_tariff(utilityrate, row, net_billing_sell_rate):
|
512
|
+
def process_tariff(utilityrate: ur5, row: pd.Series, net_billing_sell_rate: float) -> ur5:
|
472
513
|
"""Instantiate the utilityrate5 PySAM model and process the agent's
|
473
514
|
rate information to conform with PySAM input formatting.
|
474
515
|
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
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.
|
479
521
|
|
480
522
|
Returns:
|
481
|
-
|
482
|
-
utilityrate: 'PySAM.Utilityrate5'
|
523
|
+
PySAM.Utilityrate5: Configured Utilityrate5 model.
|
483
524
|
"""
|
484
525
|
# Monthly fixed charge [$]
|
485
526
|
utilityrate.ElectricityRates.ur_monthly_fixed_charge = row["ur_monthly_fixed_charge"]
|
@@ -532,13 +573,48 @@ def process_tariff(utilityrate, row, net_billing_sell_rate):
|
|
532
573
|
|
533
574
|
|
534
575
|
def find_breakeven(
|
535
|
-
row,
|
536
|
-
loan,
|
537
|
-
batt_costs,
|
538
|
-
pysam_outputs,
|
539
|
-
|
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,
|
540
583
|
**kwargs,
|
541
|
-
):
|
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
|
+
|
542
618
|
# calculate theoretical min/max NPV values
|
543
619
|
min_npv = calc_financial_performance(1e9, row, loan, batt_costs)
|
544
620
|
|
@@ -580,7 +656,7 @@ def find_breakeven(
|
|
580
656
|
# pre-calculate tolerance as a proportion of capex value pre-calculated directly above
|
581
657
|
tol = 1e-6 * ((a + b) / 2)
|
582
658
|
|
583
|
-
if
|
659
|
+
if method is Optimization.GRID_SEARCH:
|
584
660
|
if kwargs["capex_array"] is None:
|
585
661
|
capex_array = np.arange(5000.0, -500.0, -500.0)
|
586
662
|
else:
|
@@ -598,7 +674,8 @@ def find_breakeven(
|
|
598
674
|
|
599
675
|
except Exception as e:
|
600
676
|
raise ValueError("Grid search failed.") from e
|
601
|
-
|
677
|
+
|
678
|
+
if method is Optimization.BISECT:
|
602
679
|
try:
|
603
680
|
# required args for 'bisect'
|
604
681
|
if not pre_calc_bounds_and_tolerances:
|
@@ -626,12 +703,13 @@ def find_breakeven(
|
|
626
703
|
full_output=full_output,
|
627
704
|
disp=disp,
|
628
705
|
)
|
629
|
-
results = loan.Outputs
|
630
|
-
return breakeven_cost_usd_p_kw,
|
706
|
+
results = {k: getattr(loan.Outputs, k) for k in pysam_outputs}
|
707
|
+
return breakeven_cost_usd_p_kw, results
|
631
708
|
|
632
709
|
except Exception as e:
|
633
710
|
raise ValueError("Root finding failed.") from e
|
634
|
-
|
711
|
+
|
712
|
+
if method is Optimization.BRENTQ:
|
635
713
|
try:
|
636
714
|
# required args for 'brentq'
|
637
715
|
if not pre_calc_bounds_and_tolerances:
|
@@ -660,13 +738,13 @@ def find_breakeven(
|
|
660
738
|
disp=disp,
|
661
739
|
)
|
662
740
|
|
663
|
-
results = loan.Outputs
|
664
|
-
return breakeven_cost_usd_p_kw,
|
741
|
+
results = {k: getattr(loan.Outputs, k) for k in pysam_outputs}
|
742
|
+
return breakeven_cost_usd_p_kw, results
|
665
743
|
|
666
744
|
except Exception as e:
|
667
745
|
raise ValueError("Root finding failed.") from e
|
668
746
|
|
669
|
-
|
747
|
+
if method is Optimization.NEWTON:
|
670
748
|
try:
|
671
749
|
# required args for 'newton'
|
672
750
|
x0 = kwargs["x0"]
|
@@ -700,23 +778,57 @@ def find_breakeven(
|
|
700
778
|
disp=disp,
|
701
779
|
)
|
702
780
|
|
703
|
-
results = loan.Outputs
|
704
|
-
return breakeven_cost_usd_p_kw,
|
781
|
+
results = {k: getattr(loan.Outputs, k) for k in pysam_outputs}
|
782
|
+
return breakeven_cost_usd_p_kw, results
|
705
783
|
|
706
784
|
except Exception as e:
|
707
785
|
raise ValueError("Root finding failed.") from e
|
708
786
|
|
709
|
-
|
710
|
-
raise ValueError("Invalid method passed to find_breakeven function")
|
787
|
+
raise ValueError(f"Invalid `method` ({method}) passed to `find_breakeven` function")
|
711
788
|
|
712
789
|
|
713
790
|
def find_breakeven_fom(
|
714
|
-
row,
|
715
|
-
financial,
|
716
|
-
pysam_outputs,
|
717
|
-
|
791
|
+
row: pd.Series,
|
792
|
+
financial: mp,
|
793
|
+
pysam_outputs: list[str],
|
794
|
+
method: str,
|
795
|
+
*,
|
796
|
+
pre_calc_bounds_and_tolerances: bool = True,
|
718
797
|
**kwargs,
|
719
|
-
):
|
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
|
+
|
720
832
|
# calculate theoretical min/max NPV values
|
721
833
|
min_npv = calc_financial_performance_fom(1e9, row, financial)
|
722
834
|
|
@@ -758,7 +870,7 @@ def find_breakeven_fom(
|
|
758
870
|
# capex value pre-calculated directly above
|
759
871
|
tol = 1e-6 * ((a + b) / 2)
|
760
872
|
|
761
|
-
if
|
873
|
+
if method is Optimization.GRID_SEARCH:
|
762
874
|
if kwargs["capex_array"] is None:
|
763
875
|
capex_array = np.arange(5000.0, -500.0, -500.0)
|
764
876
|
else:
|
@@ -776,7 +888,8 @@ def find_breakeven_fom(
|
|
776
888
|
|
777
889
|
except Exception as e:
|
778
890
|
raise ValueError("Grid search failed.") from e
|
779
|
-
|
891
|
+
|
892
|
+
if method is Optimization.BISECT:
|
780
893
|
try:
|
781
894
|
# required args for 'bisect
|
782
895
|
if not pre_calc_bounds_and_tolerances:
|
@@ -805,12 +918,13 @@ def find_breakeven_fom(
|
|
805
918
|
disp=disp,
|
806
919
|
)
|
807
920
|
|
808
|
-
results = financial.Outputs
|
809
|
-
return breakeven_cost_usd_p_kw,
|
921
|
+
results = {k: getattr(financial.Outputs, k) for k in pysam_outputs}
|
922
|
+
return breakeven_cost_usd_p_kw, results
|
810
923
|
|
811
924
|
except Exception as e:
|
812
925
|
raise ValueError("Root finding failed.") from e
|
813
|
-
|
926
|
+
|
927
|
+
if method is Optimization.BRENTQ:
|
814
928
|
try:
|
815
929
|
# required args for 'brentq'
|
816
930
|
if not pre_calc_bounds_and_tolerances:
|
@@ -839,12 +953,13 @@ def find_breakeven_fom(
|
|
839
953
|
disp=disp,
|
840
954
|
)
|
841
955
|
|
842
|
-
results = financial.Outputs
|
843
|
-
return breakeven_cost_usd_p_kw,
|
956
|
+
results = {k: getattr(financial.Outputs, k) for k in pysam_outputs}
|
957
|
+
return breakeven_cost_usd_p_kw, results
|
844
958
|
|
845
959
|
except Exception as e:
|
846
960
|
raise ValueError("Root finding failed.") from e
|
847
|
-
|
961
|
+
|
962
|
+
if method is Optimization.NEWTON:
|
848
963
|
try:
|
849
964
|
# required args for 'newton'
|
850
965
|
x0 = kwargs["x0"]
|
@@ -878,56 +993,46 @@ def find_breakeven_fom(
|
|
878
993
|
disp=disp,
|
879
994
|
)
|
880
995
|
|
881
|
-
results = financial.Outputs
|
882
|
-
return breakeven_cost_usd_p_kw,
|
996
|
+
results = {k: getattr(financial.Outputs, k) for k in pysam_outputs}
|
997
|
+
return breakeven_cost_usd_p_kw, results
|
883
998
|
|
884
999
|
except Exception as e:
|
885
1000
|
raise ValueError("Root finding failed") from e
|
886
|
-
|
887
|
-
|
1001
|
+
|
1002
|
+
raise ValueError("Invalid method passed to find_breakeven function")
|
888
1003
|
|
889
1004
|
|
890
1005
|
def process_btm(
|
891
|
-
row,
|
892
|
-
tech,
|
893
|
-
generation_hourly,
|
894
|
-
consumption_hourly,
|
895
|
-
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
|
+
*,
|
896
1013
|
en_batt=False,
|
897
|
-
batt_dispatch=None,
|
898
1014
|
):
|
899
|
-
"""Behind-the-meter
|
900
|
-
|
901
|
-
This function processes a BTM agent by:
|
902
|
-
1)
|
903
|
-
|
904
|
-
Parameters
|
905
|
-
----------
|
906
|
-
**row** : 'DataFrame row'
|
907
|
-
The row of the dataframe on which the function is performed
|
908
|
-
**exported_hourly** : ''
|
909
|
-
8760 of generation
|
910
|
-
**consumption_hourly** : ''
|
911
|
-
8760 of consumption
|
912
|
-
**tariff_dict** : 'dict'
|
913
|
-
Dictionary containing tariff parameters
|
914
|
-
**btm_nem** : 'bool'
|
915
|
-
Enable NEM for BTM parcels
|
916
|
-
**en_batt** : 'bool'
|
917
|
-
Enable battery modeling
|
918
|
-
**batt_dispatch** : 'string'
|
919
|
-
Specify battery dispatch strategy type
|
1015
|
+
"""Behind-the-meter calculation for a single agent.
|
920
1016
|
|
921
|
-
|
922
|
-
|
1017
|
+
TODO: list out the actual process
|
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.
|
923
1027
|
|
1028
|
+
Returns:
|
1029
|
+
pd.Series: Updated :py:attr:`row` with PySAM and breakeven cost results.
|
924
1030
|
"""
|
925
1031
|
# extract agent load and generation profiles
|
926
1032
|
generation_hourly = np.array(generation_hourly)
|
927
|
-
consumption_hourly = np.array(consumption_hourly, dtype=np.float32)
|
928
1033
|
|
929
1034
|
# specify tech-agnostic system size column
|
930
|
-
row["system_size_kw"] = row[f"{tech}_size_kw_btm"]
|
1035
|
+
row["system_size_kw"] = row[f"{tech.value}_size_kw_btm"]
|
931
1036
|
|
932
1037
|
# instantiate PySAM battery model based on agent sector
|
933
1038
|
if row.loc["sector_abbr"] == "res":
|
@@ -1289,8 +1394,7 @@ def process_btm(
|
|
1289
1394
|
|
1290
1395
|
_ = calc_financial_performance(row["system_capex_per_kw"], row, loan, batt_costs)
|
1291
1396
|
|
1292
|
-
|
1293
|
-
row["additional_pysam_outputs"] = {k: results.get(k) for k in pysam_outputs}
|
1397
|
+
row["additional_pysam_outputs"] = {k: getattr(loan.Outputs, k) for k in pysam_outputs}
|
1294
1398
|
|
1295
1399
|
# run root finding algorithm to find breakeven cost based on calculated NPV
|
1296
1400
|
out, _ = find_breakeven(
|
@@ -1298,8 +1402,9 @@ def process_btm(
|
|
1298
1402
|
loan=loan,
|
1299
1403
|
pysam_outputs=pysam_outputs,
|
1300
1404
|
batt_costs=batt_costs,
|
1405
|
+
method="newton",
|
1301
1406
|
pre_calc_bounds_and_tolerances=False,
|
1302
|
-
**{"
|
1407
|
+
**{"x0": 10000.0, "full_output": True},
|
1303
1408
|
)
|
1304
1409
|
|
1305
1410
|
row["breakeven_cost_usd_p_kw"] = out
|
@@ -1308,40 +1413,39 @@ def process_btm(
|
|
1308
1413
|
|
1309
1414
|
|
1310
1415
|
def process_fom(
|
1311
|
-
row,
|
1312
|
-
tech,
|
1313
|
-
generation_hourly,
|
1314
|
-
market_profile,
|
1315
|
-
pysam_outputs,
|
1316
|
-
|
1317
|
-
|
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,
|
1318
1424
|
):
|
1319
|
-
"""Front-of-
|
1425
|
+
"""Front-of-meter calculation for a single agent.
|
1320
1426
|
|
1321
|
-
|
1322
|
-
1)
|
1427
|
+
TODO: list out the actual process
|
1323
1428
|
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
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.
|
1332
1437
|
|
1333
1438
|
Returns:
|
1334
|
-
|
1335
|
-
|
1439
|
+
pd.Series: Updated :py:attr:`row` with PySAM and breakeven cost results.
|
1336
1440
|
"""
|
1337
1441
|
# extract generation profile
|
1338
1442
|
generation_hourly = np.array(generation_hourly)
|
1339
1443
|
|
1340
1444
|
# specify tech-agnostic system size column
|
1341
|
-
row["system_size_kw"] = row[f"{tech}_size_kw_fom"]
|
1445
|
+
row["system_size_kw"] = row[f"{tech.value}_size_kw_fom"]
|
1342
1446
|
|
1343
1447
|
inv_eff = 1.0 # required inverter efficiency for FOM systems
|
1344
|
-
gen =
|
1448
|
+
gen = (generation_hourly * inv_eff).tolist()
|
1345
1449
|
|
1346
1450
|
# set up battery, with system generation conditional
|
1347
1451
|
# on the battery generation being included
|
@@ -1350,15 +1454,15 @@ def process_fom(
|
|
1350
1454
|
pass
|
1351
1455
|
else:
|
1352
1456
|
# initialize PySAM model
|
1353
|
-
if tech
|
1457
|
+
if tech is Technology.SOLAR:
|
1354
1458
|
financial = mp.default("PVWattsMerchantPlant")
|
1355
|
-
elif tech
|
1459
|
+
elif tech is Technology.WIND:
|
1356
1460
|
financial = mp.default("WindPowerMerchantPlant")
|
1357
1461
|
else:
|
1358
1462
|
msg = "Please write a wrapper to account for the new technology type"
|
1359
|
-
raise NotImplementedError(f"{msg} {tech}")
|
1463
|
+
raise NotImplementedError(f"{msg} {tech.value}")
|
1360
1464
|
|
1361
|
-
ptc_fed_amt = row[f"ptc_fed_amt_{tech}"]
|
1465
|
+
ptc_fed_amt = row[f"ptc_fed_amt_{tech.value}"]
|
1362
1466
|
itc_fed_pct = row["itc_fed_pct"]
|
1363
1467
|
deg = row["deg"]
|
1364
1468
|
system_capex_per_kw = row["system_capex_per_kw"]
|
@@ -1426,10 +1530,12 @@ def process_fom(
|
|
1426
1530
|
financial.Revenue.mp_enable_ancserv2 = 0
|
1427
1531
|
financial.Revenue.mp_enable_ancserv3 = 0
|
1428
1532
|
financial.Revenue.mp_enable_ancserv4 = 0
|
1429
|
-
|
1430
|
-
|
1431
|
-
financial.Revenue.
|
1432
|
-
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
|
1433
1539
|
|
1434
1540
|
financial.CapacityPayments.cp_capacity_payment_type = 0
|
1435
1541
|
financial.CapacityPayments.cp_capacity_payment_amount = [0]
|
@@ -1447,8 +1553,7 @@ def process_fom(
|
|
1447
1553
|
|
1448
1554
|
log.info(f"row {row.loc['gid']} calculating financial performance")
|
1449
1555
|
_ = calc_financial_performance_fom(system_capex_per_kw, row, financial)
|
1450
|
-
|
1451
|
-
row["additional_pysam_outputs"] = {k: results.get(k) for k in pysam_outputs}
|
1556
|
+
row["additional_pysam_outputs"] = {k: getattr(financial.Outputs, k) for k in pysam_outputs}
|
1452
1557
|
|
1453
1558
|
# run root finding algorithm to find breakeven cost based on calculated NPV
|
1454
1559
|
log.info(f"row {row.loc['gid']} breakeven")
|
@@ -1465,46 +1570,70 @@ def process_fom(
|
|
1465
1570
|
return row
|
1466
1571
|
|
1467
1572
|
|
1468
|
-
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
|
+
"""
|
1469
1585
|
try:
|
1470
1586
|
results = {}
|
1471
1587
|
for tech in config.project.settings.TECHS:
|
1472
|
-
|
1588
|
+
tech = Technology(tech)
|
1589
|
+
if tech is Technology.WIND:
|
1473
1590
|
tech_config = row["turbine_class"]
|
1474
|
-
elif tech
|
1591
|
+
elif tech is Technology.SOLAR:
|
1475
1592
|
# TODO: Not updated for the actual configuration, so solar is likely out of date
|
1476
|
-
azimuth = config.rev.settings.azimuth_direction_to_degree[
|
1477
|
-
|
1478
|
-
|
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)
|
1479
1598
|
|
1480
|
-
if row[f"{tech}_size_kw_{sector}"] > 0:
|
1599
|
+
if row[f"{tech.value}_size_kw_{sector.value}"] > 0:
|
1481
1600
|
tech_config = row["turbine_class"]
|
1482
1601
|
cf_hourly = find_cf_from_rev_wind(
|
1483
|
-
config.rev.DIR,
|
1484
|
-
config.project.settings.GENERATION_SCALE_OFFSET["wind"],
|
1485
|
-
|
1486
|
-
|
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,
|
1487
1606
|
)
|
1488
|
-
generation_hourly = cf_hourly * row[f"{tech}_size_kw_{sector}"]
|
1607
|
+
generation_hourly = cf_hourly * row[f"{tech.value}_size_kw_{sector.value}"]
|
1489
1608
|
generation_hourly = generation_hourly.round(2)
|
1490
1609
|
generation_hourly = np.array(generation_hourly).astype(np.float32).round(2)
|
1491
1610
|
else:
|
1492
1611
|
# if system size is zero, return dummy values
|
1493
|
-
results[f"{tech}_breakeven_cost_{sector}"] = -1
|
1494
|
-
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
|
+
}
|
1495
1616
|
continue
|
1496
1617
|
|
1497
|
-
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
|
+
|
1498
1627
|
row = process_btm(
|
1499
1628
|
row=row,
|
1500
1629
|
tech=tech,
|
1501
1630
|
generation_hourly=generation_hourly,
|
1502
|
-
consumption_hourly=
|
1631
|
+
consumption_hourly=consumption_hourly,
|
1503
1632
|
pysam_outputs=config.pysam.outputs.btm,
|
1504
1633
|
en_batt=False,
|
1505
1634
|
batt_dispatch=None, # TODO: enable battery switch earlier
|
1506
1635
|
)
|
1507
|
-
|
1636
|
+
elif sector is Sector.FOM:
|
1508
1637
|
# fetch 8760 cambium values for FOM
|
1509
1638
|
market_profile = fetch_cambium_values(
|
1510
1639
|
row,
|
@@ -1524,10 +1653,10 @@ def worker(row: pd.Series, sector: str, config: Configuration):
|
|
1524
1653
|
)
|
1525
1654
|
|
1526
1655
|
# store results in dictionary
|
1527
|
-
results[f"{tech}_breakeven_cost_{sector}"] = row["breakeven_cost_usd_p_kw"]
|
1656
|
+
results[f"{tech.value}_breakeven_cost_{sector.value}"] = row["breakeven_cost_usd_p_kw"]
|
1528
1657
|
results |= row["additional_pysam_outputs"]
|
1529
1658
|
|
1530
|
-
return
|
1659
|
+
return {row["gid"]: results}
|
1531
1660
|
|
1532
1661
|
except Exception as e:
|
1533
1662
|
log.exception(e)
|