Skip to content

Commit 8c02fa6

Browse files
committed
Fix minor issue with processing flow variables
1 parent 6cdc298 commit 8c02fa6

File tree

3 files changed

+89
-33
lines changed

3 files changed

+89
-33
lines changed

src/pownet/core/output.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -390,42 +390,76 @@ def get_max_line_usage(
390390
line_locations: pd.DataFrame,
391391
rated_line_capacities: dict[tuple[str, str], int],
392392
) -> pd.DataFrame:
393+
"""Calculates the maximum utilization for each transmission line.
394+
395+
This function takes the flow results from an optimization model,
396+
determines the peak flow on each line over the entire simulation horizon,
397+
and then calculates the utilization of each line as a percentage of its
398+
rated capacity. It also merges location data for the lines.
399+
400+
Args:
401+
flow_variables (pd.DataFrame): DataFrame containing flow values for each
402+
line at each timestep. Expected columns: 'node_a', 'node_b',
403+
'value' (flow magnitude), and 'hour'.
404+
line_locations (pd.DataFrame): DataFrame containing location or other
405+
metadata for each line. Expected to be indexed by a
406+
MultiIndex ('source', 'sink').
407+
rated_line_capacities (dict[tuple[str, str], int]): Dictionary mapping
408+
line tuples (source_node, sink_node) to their rated
409+
power capacity (e.g., in MW).
410+
411+
Returns:
412+
pd.DataFrame: A DataFrame indexed by ('source', 'sink') with columns
413+
including 'max_line_usage' (peak flow / rated capacity),
414+
columns from `line_locations`, and 'rated_capacity'.
415+
"""
393416

394417
# Prevent unintentional modification to the original dataframe
395-
flow_variables = flow_variables.copy()
418+
flow_vars = flow_variables.copy()
396419

397-
flow_variables = flow_variables.rename(
420+
# Standardize column names and remove unnecessary columns
421+
flow_vars = flow_vars.rename(
398422
columns={"node_a": "source", "node_b": "sink"}
399-
).drop("hour", axis=1)
400-
# Flow can be negative due to flow being directional
401-
flow_variables["value"] = flow_variables["value"].abs()
423+
).drop(
424+
"hour", axis=1
425+
) # Assuming 'hour' is not needed for max usage across all time
402426

403427
# Find the max_value for each line segment across the whole time horizon
404-
flow_variables["max_value"] = flow_variables.groupby(["source", "sink"])[
428+
# Flow variables are non-negative, so we can use max() to find the peak flow.
429+
flow_vars["max_value"] = flow_vars.groupby(["source", "sink"])[
405430
"value"
406431
].transform("max")
432+
407433
# Drop duplicates because we are only interested in the maximum flow
408-
# over the whole simulation
409-
flow_variables = flow_variables.drop_duplicates(subset=["source", "sink"])
434+
# over the whole simulation for each unique line
435+
flow_vars = flow_vars.drop_duplicates(subset=["source", "sink"])
410436

411-
# Max utilization rate
412-
flow_variables["max_line_usage"] = flow_variables.apply(
437+
# Calculate maximum utilization rate
438+
# Ensure that the (row["source"], row["sink"]) tuple exactly matches the keys in rated_line_capacities
439+
flow_vars["max_line_usage"] = flow_vars.apply(
413440
lambda row: row["max_value"]
414441
/ rated_line_capacities[(row["source"], row["sink"])],
415442
axis=1,
416443
).round(4)
417444

418-
flow_variables = flow_variables[["source", "sink", "max_line_usage"]].set_index(
419-
["source", "sink"]
420-
)
421-
flow_variables = flow_variables.merge(
445+
# Select and re-index the DataFrame
446+
flow_vars = flow_vars[
447+
["source", "sink", "max_value", "max_line_usage"]
448+
].set_index(["source", "sink"])
449+
450+
# Merge with line location data
451+
# The index of flow_vars is now (source, sink)
452+
# line_locations should also be indexed by (source, sink) for a clean merge
453+
flow_vars = flow_vars.merge(
422454
line_locations, how="left", left_index=True, right_index=True
423455
)
424-
# Append rated capacities
425-
flow_variables["rated_capacity"] = [
426-
rated_line_capacities[idx] for idx in flow_variables.index
456+
457+
# Ensure that the index of flow_vars (which is (source, sink))
458+
# correctly aligns with the keys in rated_line_capacities
459+
flow_vars["rated_capacity"] = [
460+
rated_line_capacities[idx] for idx in flow_vars.index
427461
]
428-
return flow_variables
462+
return flow_vars
429463

430464
def get_fuel_mix(self, hourly_generation: pd.DataFrame) -> pd.DataFrame:
431465
"""Return the fuel mix (%) for the whole simulation period."""

src/pownet/core/visualizer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def plot_lmp(
256256
def plot_line_usage(
257257
self,
258258
max_line_usage: pd.DataFrame,
259-
output_folder: str,
259+
output_folder: str = None,
260260
) -> None:
261261
"""Flow variables must have the max_line_usage column"""
262262
max_line_usage = create_geoseries_columns(max_line_usage)
@@ -273,7 +273,7 @@ def get_linewidth(capacity):
273273
max_linewidth = 6
274274
# Scale capacity to between 1 and 10 to avoid log(0) errors
275275
scaled_capacity = 1 + 9 * (capacity - min_capacity) / (
276-
max_capacity - min_capacity
276+
max_capacity - min_capacity + 0.0001 # to avoid division by zero
277277
)
278278
log_capacity = np.log10(scaled_capacity)
279279
# Scale the log value to the desired linewidth range.
@@ -324,7 +324,7 @@ def get_linewidth(capacity):
324324
plt.tight_layout()
325325
plt.subplots_adjust(bottom=0.2)
326326

327-
if output_folder is not None:
327+
if output_folder:
328328
figure_name = f"{self.model_id}_line_usage.png"
329329
fig = ax.get_figure()
330330
fig.savefig(

src/pownet/data_utils.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -327,27 +327,49 @@ def parse_flow_variables(
327327
solution: pd.DataFrame, sim_horizon: int, step_k: int
328328
) -> pd.DataFrame:
329329
"""
330-
The flow variables are in the (node, node, t) format.
330+
Parses flow variables from the solution DataFrame.
331+
The flow variables are expected in the format:
332+
flow_fwd[node_a,node_b,t] or flow_bwd[node_a,node_b,t].
331333
332334
Args:
333-
solution: The solution DataFrame.
334-
sim_horizon: The length of the simulation horizon.
335-
step_k: The current simulation period.
335+
solution: The solution DataFrame with a 'varname' column.
336+
sim_horizon: The length of the simulation horizon for a single step_k (e.g., 24 hours).
337+
step_k: The current simulation period (1-indexed).
336338
337339
Returns:
338-
pd.DataFrame: The flow variables DataFrame
340+
pd.DataFrame: A DataFrame with parsed flow variables, including
341+
columns for 'node_a', 'node_b', 'type' (fwd/bwd),
342+
'value', 'timestep' (relative to step_k), and 'hour' (absolute).
339343
"""
340-
flow_var_pattern = r"flow\[(.+),(.+),(\d+)\]"
341-
cur_flow_vars = solution[solution["varname"].str.match(flow_var_pattern)].copy()
344+
# Matches flow_fwd[node_a,node_b,t] or flow_bwd[node_a,node_b,t]
345+
# It captures the type (fwd or bwd), node_a, node_b, and t.
346+
flow_var_pattern = r"flow_(fwd|bwd)\[([^,]+),([^,]+),(\d+)\]"
342347

343-
cur_flow_vars[["node_a", "node_b", "timestep"]] = cur_flow_vars[
344-
"varname"
345-
].str.extract(flow_var_pattern, expand=True)
348+
# Filter rows that match the flow variable pattern
349+
flow_vars_mask = solution["varname"].str.contains(
350+
r"flow_(?:fwd|bwd)\[.+,.+,\d+\]", regex=True
351+
)
352+
cur_flow_vars = solution[flow_vars_mask].copy()
346353

354+
if cur_flow_vars.empty:
355+
return pd.DataFrame(
356+
columns=["node_a", "node_b", "type", "value", "timestep", "hour"]
357+
)
358+
359+
# Extract components from varname
360+
extracted_data = cur_flow_vars["varname"].str.extract(flow_var_pattern, expand=True)
361+
cur_flow_vars[["type", "node_a", "node_b", "timestep"]] = extracted_data
362+
363+
# Convert timestep to integer
347364
cur_flow_vars["timestep"] = cur_flow_vars["timestep"].astype(int)
365+
366+
# Calculate absolute hour
367+
# Assuming sim_horizon is the number of timesteps within one step_k
368+
# and step_k is 1-indexed.
348369
cur_flow_vars["hour"] = cur_flow_vars["timestep"] + sim_horizon * (step_k - 1)
349-
cur_flow_vars = cur_flow_vars.drop("varname", axis=1)
350-
return cur_flow_vars
370+
371+
final_columns = ["node_a", "node_b", "value", "type", "timestep", "hour"]
372+
return cur_flow_vars[final_columns]
351373

352374

353375
def parse_syswide_variables(

0 commit comments

Comments
 (0)