Skip to content

Commit 38bf5d2

Browse files
committed
Synchronize updates v3.2
Changes are described in the version 2.2 release notes.
1 parent 5d2f6df commit 38bf5d2

File tree

15 files changed

+2314
-768
lines changed

15 files changed

+2314
-768
lines changed

src/pownet/core/builder.py

Lines changed: 576 additions & 175 deletions
Large diffs are not rendered by default.

src/pownet/core/data_processor.py

Lines changed: 173 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,25 @@ def __init__(
2020
is stored in the model_library/model_name folder. The required files are:
2121
1. transmission.csv: A file that contains the transmission data.
2222
2. thermal_unit.csv: A file that contains the thermal unit data.
23-
3. unit_marginal_cost.csv: A file that contains the marginal cost of non-thermal units.
24-
4. (Optional) solar.csv, wind.csv, hydropower.csv, import.csv: Files that contain the renewable unit data.
23+
3. solar.csv, wind.csv, hydropower.csv, import.csv: Files that contain the renewable unit data.
24+
4. energy_storage.csv: A file that contains the energy storage system data.
2525
"""
2626
self.input_folder = input_folder
2727
self.model_name = model_name
2828
self.year = year
2929
self.frequency = frequency
3030

3131
# Values that will be calculated
32-
self.cycle_map: dict = {}
33-
self.thermal_derate_factors: pd.DataFrame = get_dates(year)
34-
self.marginal_costs: pd.DataFrame = get_dates(year)
32+
self.cycle_map: dict = json.loads("{}")
33+
self.thermal_derate_factors: pd.DataFrame = pd.DataFrame()
34+
self.thermal_derated_capacity: pd.DataFrame = pd.DataFrame()
35+
36+
self.transmission_data: pd.DataFrame = pd.DataFrame()
37+
self.transmission_params: dict = {} # Default PowNet parameters
38+
self.user_transmission: pd.DataFrame = pd.DataFrame()
39+
40+
self.ess_derate_factors: pd.DataFrame = pd.DataFrame()
41+
self.ess_derated_capacity: pd.DataFrame = pd.DataFrame()
3542

3643
# Maps frequency to wavelength
3744
wavelengths = {50: 6000, 60: 5000}
@@ -40,7 +47,7 @@ def __init__(
4047
# Note that we will modify the original file
4148
self.model_folder = os.path.join(self.input_folder, model_name)
4249

43-
def load_data(self) -> None:
50+
def load_transmission_data(self) -> None:
4451
# User inputs of transmission data
4552
self.user_transmission = pd.read_csv(
4653
os.path.join(self.model_folder, "transmission.csv"),
@@ -141,6 +148,9 @@ def calc_line_capacity(self) -> None:
141148
"""Calculate the capacity of line segments. The unit is in MW.
142149
Line capacity is the minimum of the thermal limit and the steady-state
143150
stability limit (a function of distance).
151+
152+
Note the calculated values are overwritten by user provided values
153+
in the transmission.csv file.
144154
"""
145155
self.transmission_data["stability_limit"] = self.user_transmission.apply(
146156
lambda x: self.calc_stability_limit(
@@ -166,6 +176,19 @@ def calc_line_capacity(self) -> None:
166176
["thermal_limit", "stability_limit"]
167177
].min(axis=1)
168178

179+
# Overwrite calculated values with user provided values
180+
excluded_list = [-1, None]
181+
user_specified_capacity = self.user_transmission.loc[
182+
~self.user_transmission["user_line_cap"].isin(excluded_list)
183+
]
184+
user_specified_capacity = user_specified_capacity.set_index(
185+
["source", "sink"]
186+
).rename(columns={"user_line_cap": "line_capacity"})
187+
188+
self.transmission_data = self.transmission_data.set_index(["source", "sink"])
189+
self.transmission_data.update(user_specified_capacity)
190+
self.transmission_data = self.transmission_data.reset_index()
191+
169192
def calc_line_susceptance(self) -> None:
170193
"""Calculate the susceptance of line segments. The unit is in Siemens (S)."""
171194
# Assume reactance based on the maximum voltage level of the two buses
@@ -178,16 +201,34 @@ def calc_line_susceptance(self) -> None:
178201
axis=1,
179202
)
180203

181-
self.transmission_data["reactance_pu"] = (
204+
self.transmission_data["reactance"] = (
182205
self.transmission_data["reactance_per_km"]
183206
* self.user_transmission["distance"]
184207
)
185208

186209
self.transmission_data["susceptance"] = self.transmission_data.apply(
187-
lambda x: int(x["source_kv"] * x["sink_kv"] / x["reactance_pu"]),
210+
lambda x: int(x["source_kv"] * x["sink_kv"] / x["reactance"]),
188211
axis=1,
189212
)
190213

214+
# Replace with user-specified values
215+
excluded_values = [-1, None]
216+
user_specified_susceptance = self.user_transmission.loc[
217+
~self.user_transmission["susceptance"].isin(excluded_values),
218+
["source", "sink", "susceptance"],
219+
]
220+
user_specified_susceptance = user_specified_susceptance.set_index(
221+
["source", "sink"]
222+
)
223+
# Change from float to int
224+
user_specified_susceptance = user_specified_susceptance.astype(
225+
{"susceptance": int}
226+
)
227+
228+
self.transmission_data = self.transmission_data.set_index(["source", "sink"])
229+
self.transmission_data.update(user_specified_susceptance)
230+
self.transmission_data = self.transmission_data.reset_index()
231+
191232
def write_transmission_data(self) -> None:
192233
self.transmission_data.to_csv(
193234
os.path.join(self.model_folder, "pownet_transmission.csv"), index=False
@@ -205,7 +246,7 @@ def create_cycle_map(self) -> None:
205246
target="sink",
206247
)
207248
cycles = nx.cycle_basis(graph)
208-
# We save this map to use by the ModelBuilder
249+
# Save this map to be uses by ModelBuilder
209250
self.cycle_map = {f"cycle_{idx+1}": cycle for idx, cycle in enumerate(cycles)}
210251

211252
def write_cycle_map(self) -> None:
@@ -216,99 +257,128 @@ def write_cycle_map(self) -> None:
216257
with open(os.path.join(self.model_folder, "pownet_cycle_map.json"), "w") as f:
217258
json.dump(self.cycle_map, f)
218259

219-
def create_thermal_derate_factors(self, derate_factor: float = 1.00) -> None:
220-
"""Assumes a constant derate factor for all thermal units. The derate factor is applied
221-
to the nameplate capacity of thermal units.
260+
def _create_derate_factors(
261+
self, unit_type: str, derate_factor: float = 1.00
262+
) -> None:
263+
"""Creates derate factors for a given unit type (thermal or ess).
264+
265+
Args:
266+
unit_type (str): The type of unit ('thermal' or 'ess').
267+
derate_factor (float): The derate factor to apply. Defaults to 1.00.
222268
"""
223-
# Get the thermal units
269+
224270
model_dir = os.path.join(self.input_folder, self.model_name)
225-
thermal_units = pd.read_csv(os.path.join(model_dir, "thermal_unit.csv"))[
226-
"name"
227-
].values
271+
272+
if unit_type == "thermal":
273+
filename = "thermal_unit.csv"
274+
attribute_name = "thermal_derate_factors"
275+
elif unit_type == "ess":
276+
filename = "energy_storage.csv"
277+
attribute_name = "ess_derate_factors"
278+
else:
279+
raise ValueError(
280+
f"Invalid unit type: {unit_type}. Must be 'thermal' or 'ess'."
281+
)
282+
283+
if os.path.exists(os.path.join(model_dir, filename)):
284+
units = pd.read_csv(os.path.join(model_dir, filename))["name"].values
285+
else:
286+
return
287+
228288
temp_df = pd.DataFrame(
229289
derate_factor,
230-
index=self.thermal_derate_factors.index,
231-
columns=thermal_units,
290+
index=range(0, 8760), # Consider making 8760 a constant or parameter
291+
columns=units,
232292
)
233-
self.thermal_derate_factors = pd.concat(
234-
[get_dates(year=self.year), temp_df], axis=1
293+
setattr(
294+
self,
295+
attribute_name,
296+
pd.concat([get_dates(year=self.year), temp_df], axis=1),
235297
)
236298

299+
def create_thermal_derate_factors(self, derate_factor: float = 1.00) -> None:
300+
"""Creates derate factors for thermal units."""
301+
self._create_derate_factors("thermal", derate_factor)
302+
303+
def create_ess_derate_factors(self, derate_factor: float = 1.00) -> None:
304+
"""Creates derate factors for ESS units."""
305+
self._create_derate_factors("ess", derate_factor)
306+
237307
def write_thermal_derate_factors(self) -> None:
238308
self.thermal_derate_factors.to_csv(
239309
os.path.join(self.model_folder, "pownet_derate_factor.csv"), index=False
240310
)
241311

242-
def create_derated_capacity(self) -> None:
243-
"""Create a dataframe of hourly derated capacity of thermal units. The columns are names of thermal units."""
244-
# Get the nameplate capacity of each thermal unit
245-
max_cap = pd.read_csv(
246-
os.path.join(self.model_folder, "thermal_unit.csv"),
247-
header=0,
248-
index_col="name",
249-
usecols=["name", "max_capacity"],
250-
).to_dict()["max_capacity"]
251-
252-
self.derated_max_cap = pd.DataFrame(
253-
0,
254-
columns=max_cap.keys(),
255-
index=range(0, 8760), # match index with get_dates
256-
)
257-
for thermal_unit in max_cap.keys():
258-
self.derated_max_cap[thermal_unit] = (
259-
self.thermal_derate_factors[thermal_unit] * max_cap[thermal_unit]
312+
def _create_derated_capacity(self, unit_type: str) -> None:
313+
"""Creates a dataframe of hourly derated capacity for a given unit type.
314+
315+
Args:
316+
unit_type (str): The type of unit ('thermal' or 'ess').
317+
"""
318+
319+
if unit_type == "thermal":
320+
filename = "thermal_unit.csv"
321+
derate_factor_attr = "thermal_derate_factors"
322+
derated_capacity_attr = "thermal_derated_capacity"
323+
elif unit_type == "ess":
324+
filename = "energy_storage.csv"
325+
derate_factor_attr = "ess_derate_factors"
326+
derated_capacity_attr = "ess_derated_capacity"
327+
else:
328+
raise ValueError(
329+
f"Invalid unit type: {unit_type}. Must be 'thermal' or 'ess'."
260330
)
261331

262-
self.derated_max_cap = pd.concat(
263-
[get_dates(year=self.year), self.derated_max_cap], axis=1
332+
# Get the nameplate capacity of each unit
333+
filepath = os.path.join(self.model_folder, filename)
334+
if os.path.exists(filepath):
335+
max_cap = pd.read_csv(
336+
filepath,
337+
index_col="name",
338+
usecols=["name", "max_capacity"],
339+
)[
340+
"max_capacity"
341+
] # Directly get the Series
342+
else:
343+
return
344+
345+
# Get the derate factors for the units
346+
derate_factors = getattr(self, derate_factor_attr)
347+
348+
# Efficiently calculate derated capacity using vectorized operations
349+
derated_capacity = derate_factors.drop(columns=["date", "hour"]).mul(
350+
max_cap, axis=1
264351
)
265-
self.derated_max_cap.index += 1
266352

267-
def write_derated_capacity(self) -> None:
268-
self.derated_max_cap.to_csv(
269-
os.path.join(self.model_folder, "pownet_derated_capacity.csv"), index=False
353+
# Concatenate with dates and set the index
354+
derated_capacity = pd.concat(
355+
[get_dates(year=self.year), derated_capacity], axis=1
270356
)
357+
derated_capacity.index += 1
271358

272-
def create_marginal_costs(self) -> None:
273-
"""Create a dataframe of hourly fuel prices (or marginal costs) of non-thermal units.
274-
The columns are names of renewable and import units. The marginal cost is in $/MWh.
275-
"""
276-
# unit_marginal_cost.csv has three columns: name, fuel_type, and marginal_cost.
277-
# Now, we create a fuel_type to marginal_cost mapping
278-
constant_prices = pd.read_csv(
279-
os.path.join(self.model_folder, "unit_marginal_cost.csv"),
280-
header=0,
281-
index_col="fuel_type",
282-
).to_dict()["marginal_cost"]
283-
284-
# If there are solar.csv, hydropower.csv, and wind.csv files, then we need to
285-
# include them in the fuel price file.
286-
hours_in_year = range(8760)
287-
unit_types = ["solar", "wind", "hydropower", "import"]
288-
for unit_type in unit_types:
289-
filename = os.path.join(self.model_folder, f"{unit_type}.csv")
290-
if os.path.exists(filename):
291-
units = pd.read_csv(filename, header=0).columns
292-
units = units.drop(
293-
["year", "month", "day", "hour"], errors="ignore"
294-
).to_list()
295-
temp_df = pd.DataFrame(
296-
constant_prices[unit_type],
297-
index=hours_in_year,
298-
columns=units,
299-
)
300-
self.marginal_costs = pd.concat(
301-
[
302-
self.marginal_costs,
303-
temp_df,
304-
],
305-
axis=1,
306-
)
307-
308-
def write_marginal_costs(self) -> None:
309-
self.marginal_costs.to_csv(
310-
os.path.join(self.model_folder, "pownet_marginal_cost.csv"), index=False
311-
)
359+
setattr(self, derated_capacity_attr, derated_capacity)
360+
361+
def create_thermal_derated_capacity(self) -> None:
362+
"""Creates a dataframe of hourly derated capacity of thermal units."""
363+
self._create_derated_capacity("thermal")
364+
365+
def create_ess_derated_capacity(self) -> None:
366+
"""Creates a dataframe of hourly derated capacity of ess units."""
367+
self._create_derated_capacity("ess")
368+
369+
def write_thermal_derated_capacity(self) -> None:
370+
if not self.thermal_derate_factors.empty:
371+
self.thermal_derated_capacity.to_csv(
372+
os.path.join(self.model_folder, "pownet_thermal_derated_capacity.csv"),
373+
index=False,
374+
)
375+
376+
def write_ess_derated_capacity(self) -> None:
377+
if not self.ess_derated_capacity.empty:
378+
self.ess_derated_capacity.to_csv(
379+
os.path.join(self.model_folder, "pownet_ess_derated_capacity.csv"),
380+
index=False,
381+
)
312382

313383
def check_user_line_capacities(self) -> None:
314384
"""The user can provide their own line capacities under user_line_cap column
@@ -319,22 +389,30 @@ def check_user_line_capacities(self) -> None:
319389

320390
def run_all_processing_steps(self) -> None:
321391
"""Run all the data processing steps"""
322-
self.calc_line_capacity()
323-
self.calc_line_susceptance()
324-
self.create_cycle_map()
392+
if not self.user_transmission.empty:
393+
self.calc_line_capacity()
394+
self.calc_line_susceptance()
395+
self.create_cycle_map()
396+
325397
self.create_thermal_derate_factors()
326-
self.create_derated_capacity()
327-
self.create_marginal_costs()
398+
self.create_thermal_derated_capacity()
399+
400+
self.create_ess_derate_factors()
401+
self.create_ess_derated_capacity()
328402

329403
def write_data(self) -> None:
330404
"""Write the processed data as csv files sharing a prefix "pownet_" to the model folder"""
331-
self.write_transmission_data()
332-
self.write_cycle_map()
333-
self.write_thermal_derate_factors()
334-
self.write_derated_capacity()
335-
self.write_marginal_costs()
405+
406+
if not self.transmission_data.empty:
407+
self.write_transmission_data()
408+
self.write_cycle_map()
409+
410+
self.write_thermal_derated_capacity()
411+
self.write_ess_derated_capacity()
336412

337413
def execute_data_pipeline(self) -> None:
338-
self.load_data()
414+
if os.path.exists(os.path.join(self.model_folder, "transmission.csv")):
415+
self.load_transmission_data()
416+
339417
self.run_all_processing_steps()
340418
self.write_data()

0 commit comments

Comments
 (0)