diff --git a/emf/common/helpers/loadflow.py b/emf/common/helpers/loadflow.py index 314505ca..3e5ca7e7 100644 --- a/emf/common/helpers/loadflow.py +++ b/emf/common/helpers/loadflow.py @@ -81,8 +81,8 @@ def get_network_elements(network: pypowsybl.network, elements = elements.merge(_voltage_levels, left_on='voltage_level_id', right_index=True, suffixes=(None, '_voltage_level')) elements = elements.merge(_substations, left_on='substation_id', right_index=True, suffixes=(None, '_substation')) - # Need to ensure that column 'isHvdc' is present if DANGLING_LINE type is requested - if element_type is pypowsybl.network.ElementType.DANGLING_LINE: + # Need to ensure that column 'isHvdc' is present if BOUNDARY_LINE type is requested + if element_type is pypowsybl.network.ElementType.BOUNDARY_LINE: if 'isHvdc' not in elements.columns: elements['isHvdc'] = '' @@ -121,8 +121,8 @@ def get_model_outages(network: pypowsybl.network): lines = lines.merge(_substations, left_on='substation_id', right_index=True, suffixes=(None, '_substation')) lines['element_type'] = 'LINE' - dlines = get_network_elements(network, pypowsybl.network.ElementType.DANGLING_LINE).reset_index(names=['grid_id']) - dlines['element_type'] = 'DANGLING_LINE' + dlines = get_network_elements(network, pypowsybl.network.ElementType.BOUNDARY_LINE).reset_index(names=['grid_id']) + dlines['element_type'] = 'BOUNDARY_LINE' gens = get_network_elements(network, pypowsybl.network.ElementType.GENERATOR).reset_index(names=['grid_id']) gens['element_type'] = 'GENERATOR' diff --git a/emf/model_merger/merge_functions.py b/emf/model_merger/merge_functions.py index e7a9f9a8..94b8956a 100644 --- a/emf/model_merger/merge_functions.py +++ b/emf/model_merger/merge_functions.py @@ -300,50 +300,50 @@ def ensure_paired_equivalent_injection_compatibility(network: pypowsybl.network) Set P and Q to 0 - so that no additional consumption or production is on tie line """ logger.info("Configuring paired boundary points equivalent injections: p0/q0 = 0.0") - dangling_lines = network.get_dangling_lines(all_attributes=True) - paired_dangling_lines = dangling_lines[dangling_lines['paired'] == True] - if paired_dangling_lines.empty: - logger.warning(f"No paired dangling lines found in network model") + boundary_lines = network.get_boundary_lines(all_attributes=True) + paired_boundary_lines = boundary_lines[boundary_lines['paired'] == True] + if paired_boundary_lines.empty: + logger.warning(f"No paired boundary lines found in network model") return network - # Set p0/q0 to 0 for all paired dangling lines - _updated_p0 = pd.Series(0, index=paired_dangling_lines.index) - _updated_q0 = pd.Series(0, index=paired_dangling_lines.index) - network.update_dangling_lines(id=paired_dangling_lines.index, p0=_updated_p0, q0=_updated_q0) + # Set p0/q0 to 0 for all paired boundary lines + _updated_p0 = pd.Series(0, index=paired_boundary_lines.index) + _updated_q0 = pd.Series(0, index=paired_boundary_lines.index) + network.update_boundary_lines(id=paired_boundary_lines.index, p0=_updated_p0, q0=_updated_q0) return network def ensure_paired_boundary_line_connectivity(network: pypowsybl.network): logger.info("Aligning paired boundary lines connection status") - dangling_lines = network.get_dangling_lines(all_attributes=True) - # Add cim:Tieflow attribute to dangling lines - dangling_lines['isTieflow'] = dangling_lines.index.isin(network.get_areas_boundaries()["element"]) - paired_dangling_lines = dangling_lines[dangling_lines['paired'] == True] - if paired_dangling_lines.empty: - logger.warning(f"No paired dangling lines found in network model") + boundary_lines = network.get_boundary_lines(all_attributes=True) + # Add cim:Tieflow attribute to boundary lines + boundary_lines['isTieflow'] = boundary_lines.index.isin(network.get_areas_boundaries()["element"]) + paired_boundary_lines = boundary_lines[boundary_lines['paired'] == True] + if paired_boundary_lines.empty: + logger.warning(f"No paired boundary lines found in network model") return network - # Identify dangling line pairs where the 'connected' status is inconsistent within each pairing_key group - group = paired_dangling_lines.groupby('pairing_key') + # Identify boundary lines pairs where the 'connected' status is inconsistent within each pairing_key group + group = paired_boundary_lines.groupby('pairing_key') mask_connected = group['connected'].transform(lambda s: s.nunique() > 1) mask_tieflow = group['isTieflow'].transform( lambda s: s.nunique() > 1) - mismatched_dangling_lines_con = paired_dangling_lines[mask_connected] - logger.info(f"Boundary lines with non-matching connection status: {mismatched_dangling_lines_con['pairing_key'].unique().tolist()}") + mismatched_boundary_lines_con = paired_boundary_lines[mask_connected] + logger.info(f"Boundary lines with non-matching connection status: {mismatched_boundary_lines_con['pairing_key'].unique().tolist()}") - mismatched_dangling_lines_tie = paired_dangling_lines[mask_tieflow] - logger.info(f"Boundary lines with non-matching cim:Tieflow: {mismatched_dangling_lines_tie['pairing_key'].unique().tolist()}") + mismatched_boundary_lines_tie = paired_boundary_lines[mask_tieflow] + logger.info(f"Boundary lines with non-matching cim:Tieflow: {mismatched_boundary_lines_tie['pairing_key'].unique().tolist()}") - mismatched_dangling_lines = pd.concat([mismatched_dangling_lines_con, mismatched_dangling_lines_tie]) + mismatched_boundary_lines = pd.concat([mismatched_boundary_lines_con, mismatched_boundary_lines_tie]) # Set all mismatched lines to disconnected (False) - _connected = pd.Series(data=False, index=mismatched_dangling_lines.index) - network.update_dangling_lines(id=mismatched_dangling_lines.index, connected=_connected) + _connected = pd.Series(data=False, index=mismatched_boundary_lines.index) + network.update_boundary_lines(id=mismatched_boundary_lines.index, connected=_connected) # Log each change - for i, row in mismatched_dangling_lines.iterrows(): - logger.info(f"Changed status of dangling line {row['name']}: {row['connected']} -> False") + for i, row in mismatched_boundary_lines.iterrows(): + logger.info(f"Changed status of boundary line {row['name']}: {row['connected']} -> False") return network @@ -598,17 +598,17 @@ def update_model_outages(merged_model: object, tso_list: list, scenario_datetime model_outage_areas = [model_area_map.get(item, item) for item in tso_list] filtered_model_outages = mapped_model_outages[mapped_model_outages['country'].isin(model_outage_areas)] - # Include cross-border lines for reconnection (both dangling lines) - dangling_lines = get_network_elements(network=merged_model.network, - element_type=pypowsybl.network.ElementType.DANGLING_LINE).reset_index(names=['grid_id']) - border_lines = dangling_lines[dangling_lines['pairing_key'].isin(model_outages['pairing_key'])] + # Include cross-border lines for reconnection (both boundary lines) + boundary_lines = get_network_elements(network=merged_model.network, + element_type=pypowsybl.network.ElementType.BOUNDARY_LINE).reset_index(names=['grid_id']) + border_lines = boundary_lines[boundary_lines['pairing_key'].isin(model_outages['pairing_key'])] relevant_border_lines = border_lines[border_lines['country'].isin(model_outage_areas)] # Removing any BRELL lines relevant_border_lines = relevant_border_lines[~relevant_border_lines['lineEnergyIdentificationCodeEIC'].str.contains('RU')] - additional_dangling_lines = dangling_lines[dangling_lines['pairing_key'].isin(relevant_border_lines['pairing_key'])] + additional_boundary_lines = boundary_lines[boundary_lines['pairing_key'].isin(relevant_border_lines['pairing_key'])] # Merged dataframe of network elements to be reconnected - filtered_model_outages = pd.concat([filtered_model_outages, additional_dangling_lines]).drop_duplicates(subset='grid_id') + filtered_model_outages = pd.concat([filtered_model_outages, additional_boundary_lines]).drop_duplicates(subset='grid_id') filtered_model_outages = filtered_model_outages.where(pd.notnull(filtered_model_outages), None) # rename columns diff --git a/emf/model_merger/model_merger.py b/emf/model_merger/model_merger.py index 8ad7cb62..6bb6def5 100644 --- a/emf/model_merger/model_merger.py +++ b/emf/model_merger/model_merger.py @@ -623,14 +623,12 @@ def handle(self, task_object: dict, properties: dict, **kwargs): "job_period_start": "2024-05-24T22:00:00+00:00", "job_period_end": "2024-05-25T06:00:00+00:00", "task_properties": { - "timestamp_utc": "2026-07-01T15:30:00+00:00", + "timestamp_utc": "2026-07-13T15:30:00+00:00", "merge_type": "BA", "merging_entity": "BALTICRCC", - # "included": ["PSE", "LITGRID", "ELERING", "AST"], - "included": ["PSE", "LITGRID", "AST"], - # "included": [], + "included": ["PSE", "LITGRID", "ELERING", "AST"], "excluded": [], - "local_import": ["ELERING"], + "local_import": [], "replace_tso": [], "time_horizon": "1D", "version": "000", @@ -641,11 +639,15 @@ def handle(self, task_object: dict, properties: dict, **kwargs): "outage_update": "True", "force_outage_fix": "False", "upload_to_opdm": "False", - "upload_to_minio": "False", - "send_merge_report": "False", + "upload_to_minio": "True", + "send_merge_report": "True", "lvl8_reporting": "False" } } + class Properties(dict): + pass + properties = Properties() + properties.headers = {} worker = HandlerMergeModels() - finished_task = worker.handle(sample_task, {}) + finished_task = worker.handle(sample_task, {}) \ No newline at end of file diff --git a/emf/model_merger/scaler.py b/emf/model_merger/scaler.py index a548a009..41070b29 100644 --- a/emf/model_merger/scaler.py +++ b/emf/model_merger/scaler.py @@ -26,7 +26,7 @@ - power factor sign defines whether P and Q values has opposite sign. This needs to be ensured because new Q values are calculated from P values, then the power factors sign defines what sign should be for new Q value. - current algorithm is using subnetworks, therefore at network import parameters it should be set to True. -It is possible to use older solution but that causes problems with TTN IGM where some dangling lines does not have +It is possible to use older solution but that causes problems with TTN IGM where some boundary lines does not have substation assigned. Current algorithm defines element area from subnetworks identifiables instead of substations dataframe. """ @@ -71,14 +71,14 @@ def validate_loadflow_status(results: List, components: Dict): def get_areas_losses(network: pp.network.Network, buses: pd.DataFrame, components: Dict): # Calculate ACNP with losses (from cross-border lines) - dangling_lines = get_network_elements(network, pp.network.ElementType.DANGLING_LINE, all_attributes=True) - dangling_lines = dangling_lines.merge(buses.connected_component, how='left', left_on='bus_id', right_index=True) - dangling_lines = dangling_lines[dangling_lines.connected_component.isin(components.keys())] - dangling_lines.connected_component = dangling_lines.connected_component.astype(int) - dangling_lines['boundary_p'] = dangling_lines['boundary_p'] * -1 # invert boundary_p sign to match flow direction - ac_dangling_lines = dangling_lines[dangling_lines.isHvdc == ''] - dc_dangling_lines = dangling_lines[dangling_lines.isHvdc == 'true'] - acnp_with_losses = _get_series_from_df(df=ac_dangling_lines, value_col='boundary_p').groupby(level=0).sum() + boundary_lines = get_network_elements(network, pp.network.ElementType.BOUNDARY_LINE, all_attributes=True) + boundary_lines = boundary_lines.merge(buses.connected_component, how='left', left_on='bus_id', right_index=True) + boundary_lines = boundary_lines[boundary_lines.connected_component.isin(components.keys())] + boundary_lines.connected_component = boundary_lines.connected_component.astype(int) + boundary_lines['boundary_p'] = boundary_lines['boundary_p'] * -1 # invert boundary_p sign to match flow direction + ac_boundary_lines = boundary_lines[boundary_lines.isHvdc == ''] + dc_boundary_lines = boundary_lines[boundary_lines.isHvdc == 'true'] + acnp_with_losses = _get_series_from_df(df=ac_boundary_lines, value_col='boundary_p').groupby(level=0).sum() # Calculate ACNP without losses (from generation and consumption) generation = get_areas_metrics(network=network, buses=buses, components=components, metric='GENERATOR') @@ -86,7 +86,7 @@ def get_areas_losses(network: pp.network.Network, buses: pd.DataFrame, component ## Need to ensure that all series in substraction has same index values. For example when area does not have HVDC connections ## Otherwise we will get NaN values for areas without HVDC after regular substraction present_areas = generation.index.union(consumption.index) - dcnp = _get_series_from_df(df=dc_dangling_lines, value_col='boundary_p').groupby(level=0).sum().reindex(present_areas, fill_value=0) + dcnp = _get_series_from_df(df=dc_boundary_lines, value_col='boundary_p').groupby(level=0).sum().reindex(present_areas, fill_value=0) acnp_without_losses = generation - consumption - dcnp # Calculate losses by regions @@ -105,15 +105,15 @@ def get_areas_metrics(network: pp.network.Network, buses: pd.DataFrame, componen return series.groupby(series.index).sum() * sign -def validate_converged_components(dangling_lines: pd.DataFrame, converged_components: Dict): +def validate_converged_components(boundary_lines: pd.DataFrame, converged_components: Dict): logger.info(f"Validating converged islands") for k, v in list(converged_components.items()): v['state'] = 'valid' # In case of internal island it should contain only one area if len(v['countries']) == 1: - component_dangling_lines = dangling_lines[dangling_lines['connected_component'] == k] + component_boundary_lines = boundary_lines[boundary_lines['connected_component'] == k] # Check if there are any boundary lines which belongs to component - if component_dangling_lines.empty: + if component_boundary_lines.empty: v['state'] = 'internal' logger.warning(f"Network component {k} considered as internal area island, excluding from scaling: {v}") @@ -146,13 +146,13 @@ def get_countries_to_components(components: Dict): return country_to_keys -def get_fragmented_areas_participation(unpaired_dangling_lines: pd.DataFrame, areas_to_components: Dict): +def get_fragmented_areas_participation(unpaired_boundary_lines: pd.DataFrame, areas_to_components: Dict): fragmented_areas = [] for area, comps in areas_to_components.items(): if len(comps) > 1: logger.warning(f"Fragmented area identified: {area} in components {list(comps)}") - area_dangling_lines = unpaired_dangling_lines[unpaired_dangling_lines[_country_col] == area] - fragments_acnp = {comp: area_dangling_lines[area_dangling_lines.connected_component == comp].boundary_p.sum() for comp in comps} + area_boundary_lines = unpaired_boundary_lines[unpaired_boundary_lines[_country_col] == area] + fragments_acnp = {comp: area_boundary_lines[area_boundary_lines.connected_component == comp].boundary_p.sum() for comp in comps} total_fragments_acnp = abs(sum(fragments_acnp.values())) or 1 # removing zero division warning participation = {k: abs(v) / total_fragments_acnp for k, v in fragments_acnp.items()} fragmented_areas.append(pd.DataFrame({'connected_component': list(participation.keys()), @@ -172,7 +172,7 @@ def _get_series_from_df(df: pd.DataFrame, value_col: str, area_col: str = _count index=df[area_col].astype(str) + "-" + df.connected_component.astype(str)).sort_index().round(1) -def _set_power_ratio_to_dangling_lines(df: pd.DataFrame): +def _set_power_ratio_to_boundary_lines(df: pd.DataFrame): df['power_factor'] = df['boundary_q'] / df['boundary_p'] # estimate the power factor df['power_factor'] = df['power_factor'].fillna(0) # handle zero division df['power_factor'] = df['power_factor'].clip(-float(POWER_FACTOR_THRESHOLD), float(POWER_FACTOR_THRESHOLD)) @@ -219,13 +219,13 @@ def scale_balance(model: object, # Get buses buses = network.get_buses() - # Get all dangling lines and define power factor - # dangling_lines = get_network_elements(network, pp.network.ElementType.DANGLING_LINE, all_attributes=True) - dangling_lines = network.get_dangling_lines(all_attributes=True) - dangling_lines[_country_col] = dangling_lines.index.map(_elements_to_areas_map[_country_col]) - dangling_lines = _set_power_ratio_to_dangling_lines(dangling_lines) - dangling_lines['boundary_p'] = dangling_lines['boundary_p'] * -1 # invert boundary_p sign to match flow direction - dangling_lines['boundary_q'] = dangling_lines['boundary_q'] * -1 # invert boundary_q sign to match flow direction (just used for printing) + # Get all boundary lines and define power factor + # boundary_lines = get_network_elements(network, pp.network.ElementType.BOUNDARY_LINE, all_attributes=True) + boundary_lines = network.get_boundary_lines(all_attributes=True) + boundary_lines[_country_col] = boundary_lines.index.map(_elements_to_areas_map[_country_col]) + boundary_lines = _set_power_ratio_to_boundary_lines(boundary_lines) + boundary_lines['boundary_p'] = boundary_lines['boundary_p'] * -1 # invert boundary_p sign to match flow direction + boundary_lines['boundary_q'] = boundary_lines['boundary_q'] * -1 # invert boundary_q sign to match flow direction (just used for printing) # Target HVDC setpoints target_hvdc_sp_df = pd.DataFrame(dc_schedules) @@ -239,7 +239,7 @@ def scale_balance(model: object, target_acnp_df['value'] = np.where(mask, target_acnp_df['value'] * -1, target_acnp_df['value']) # Validate presence of target AC net position by areas in network model - present_areas = dangling_lines[_country_col].drop_duplicates() + present_areas = boundary_lines[_country_col].drop_duplicates() missing_ac_schedule = present_areas[~present_areas.isin(target_acnp_df.registered_resource)].to_list() if missing_ac_schedule: # TODO consider exit scaling here if some schedules are missing @@ -247,7 +247,7 @@ def scale_balance(model: object, # Get pre-scale HVDC setpoints logger.info(f"Scaling HVDC network part") - prescale_hvdc_sp = dangling_lines[dangling_lines.isHvdc == 'true'][['lineEnergyIdentificationCodeEIC', 'boundary_p', 'boundary_q']] + prescale_hvdc_sp = boundary_lines[boundary_lines.isHvdc == 'true'][['lineEnergyIdentificationCodeEIC', 'boundary_p', 'boundary_q']] prescale_hvdc_sp = prescale_hvdc_sp.rename(columns={'boundary_p': 'value', 'boundary_q': 'value_q'}) _hvdc_results.append(pd.concat([prescale_hvdc_sp.set_index('lineEnergyIdentificationCodeEIC').value, pd.Series({'KEY': 'prescale-setpoint'})]).to_dict()) @@ -257,7 +257,7 @@ def scale_balance(model: object, # Mapping HVDC schedules to network _cols_to_keep = ['lineEnergyIdentificationCodeEIC', _country_col, 'ucte_xnode_code', 'power_factor'] - scalable_hvdc = dangling_lines[dangling_lines.isHvdc == 'true'] + scalable_hvdc = boundary_lines[boundary_lines.isHvdc == 'true'] # Ignore HVDC elements in update of setpoint which are disconnected by network model scalable_hvdc = scalable_hvdc[scalable_hvdc.connected][_cols_to_keep] scalable_hvdc.reset_index(inplace=True) @@ -277,8 +277,8 @@ def scale_balance(model: object, if _CONSTANT_POWER_FACTOR: scalable_hvdc_target['value_q'] = scalable_hvdc_target.value * scalable_hvdc_target.power_factor # ensure power factor is kept else: - scalable_hvdc_target['value_q'] = dangling_lines.loc[scalable_hvdc_target.index].q0 - network.update_dangling_lines(id=scalable_hvdc_target.index, p0=scalable_hvdc_target.value, q0=scalable_hvdc_target.value_q) + scalable_hvdc_target['value_q'] = boundary_lines.loc[scalable_hvdc_target.index].q0 + network.update_boundary_lines(id=scalable_hvdc_target.index, p0=scalable_hvdc_target.value, q0=scalable_hvdc_target.value_q) _hvdc_results.append(pd.concat([scalable_hvdc_target.set_index('lineEnergyIdentificationCodeEIC').value, pd.Series({'KEY': 'postscale-setpoint'})]).to_dict()) logger.info(f"[INITIAL] HVDC elements updated to target values: {scalable_hvdc_target['lineEnergyIdentificationCodeEIC'].values}") @@ -311,33 +311,33 @@ def scale_balance(model: object, logger.error(f"Terminating network scaling due to divergence in main island") return model - # Get dangling lines after HVDC scaling and loadflow - # dangling_lines = get_network_elements(network, pp.network.ElementType.DANGLING_LINE, all_attributes=True) - dangling_lines = network.get_dangling_lines(all_attributes=True) - dangling_lines[_country_col] = dangling_lines.index.map(_elements_to_areas_map[_country_col]) - ## Merge buses to dangling lines in order to know dangling lines network component - dangling_lines = dangling_lines.merge(buses.connected_component, how='left', left_on='bus_id', right_index=True) - dangling_lines = _set_power_ratio_to_dangling_lines(dangling_lines) - dangling_lines['boundary_p'] = dangling_lines['boundary_p'] * -1 # invert boundary_p sign to match flow direction + # Get boundary lines after HVDC scaling and loadflow + # boundary_lines = get_network_elements(network, pp.network.ElementType.BOUNDARY_LINE, all_attributes=True) + boundary_lines = network.get_boundary_lines(all_attributes=True) + boundary_lines[_country_col] = boundary_lines.index.map(_elements_to_areas_map[_country_col]) + ## Merge buses to boundary lines in order to know boundary lines network component + boundary_lines = boundary_lines.merge(buses.connected_component, how='left', left_on='bus_id', right_index=True) + boundary_lines = _set_power_ratio_to_boundary_lines(boundary_lines) + boundary_lines['boundary_p'] = boundary_lines['boundary_p'] * -1 # invert boundary_p sign to match flow direction # Validate existence of internal islands and exclude them - converged_components = validate_converged_components(dangling_lines=dangling_lines, converged_components=converged_components) + converged_components = validate_converged_components(boundary_lines=boundary_lines, converged_components=converged_components) valid_components = {k: copy.deepcopy(v) for k, v in converged_components.items() if v['state'] == 'valid'} # Get pre-scale total network balance by each component -> AC+DC net position - prescale_network_np = {k: round(dangling_lines[dangling_lines.connected_component == k].boundary_p.sum()) for k, v in valid_components.items()} + prescale_network_np = {k: round(boundary_lines[boundary_lines.connected_component == k].boundary_p.sum()) for k, v in valid_components.items()} _scaling_results.append({'KEY': 'prescale-network-np', 'GLOBAL': prescale_network_np, 'ITER': _iteration}) logger.info(f"[ITER {_iteration}] PRE-SCALE NETWORK NP by component: {prescale_network_np}") # Get pre-scale total network balance by each component -> AC net position - unpaired_dangling_lines = (dangling_lines.isHvdc == '') & (dangling_lines.tie_line_id == '') - prescale_network_acnp = {k: round(dangling_lines[unpaired_dangling_lines].query("connected_component == @k").boundary_p.sum()) for k, v in valid_components.items()} + unpaired_boundary_lines = (boundary_lines.isHvdc == '') & (boundary_lines.tie_line_id == '') + prescale_network_acnp = {k: round(boundary_lines[unpaired_boundary_lines].query("connected_component == @k").boundary_p.sum()) for k, v in valid_components.items()} _scaling_results.append({'KEY': 'prescale-network-acnp', 'GLOBAL': prescale_network_acnp, 'ITER': _iteration}) logger.info(f"[ITER {_iteration}] PRE-SCALE NETWORK ACNP by component: {prescale_network_acnp}") # Identify fragmented IGMs - where some part of network model with boundary belongs other component areas_to_components = get_countries_to_components(components=valid_components) - fragments_participation = get_fragmented_areas_participation(unpaired_dangling_lines=dangling_lines[dangling_lines.isHvdc == ''], + fragments_participation = get_fragmented_areas_participation(unpaired_boundary_lines=boundary_lines[boundary_lines.isHvdc == ''], areas_to_components=areas_to_components) # Map fragmented models to target ACNP schedules and recalculate values by participation @@ -349,7 +349,7 @@ def scale_balance(model: object, target_acnp_df['value'] = target_acnp_df['value'].round(1) # Validate total network AC net position from schedules to network model and scale to meet scheduled (per each component) - # Scaling is done through unpaired AC dangling lines + # Scaling is done through unpaired AC boundary lines # From target_acnp variable need to take only areas which are present in network model # TODO discuss whether to scale only converged islands or try on all. Currently scales converged higher than 5 buses logger.info(f"Scaling each existing island external injections to meet total island ACNP target schedule") @@ -357,18 +357,18 @@ def scale_balance(model: object, for component_key, v in valid_components.items(): scheduled_component_acnp = float(target_acnp_df[target_acnp_df.connected_component == component_key]['value'].sum().round(1)) target_network_acnp[component_key] = round(scheduled_component_acnp) # preserve for scaling report - relevant_dangling_lines = dangling_lines[unpaired_dangling_lines].query("connected_component == @component_key") - relevant_dangling_lines['participation'] = relevant_dangling_lines.boundary_p.abs() / relevant_dangling_lines.boundary_p.abs().sum() + relevant_boundary_lines = boundary_lines[unpaired_boundary_lines].query("connected_component == @component_key") + relevant_boundary_lines['participation'] = relevant_boundary_lines.boundary_p.abs() / relevant_boundary_lines.boundary_p.abs().sum() offset_network_acnp = prescale_network_acnp.get(component_key) - scheduled_component_acnp - prescale_network_acnp_diff = offset_network_acnp * relevant_dangling_lines.participation - prescale_network_acnp_target = relevant_dangling_lines.p0 - prescale_network_acnp_diff + prescale_network_acnp_diff = offset_network_acnp * relevant_boundary_lines.participation + prescale_network_acnp_target = relevant_boundary_lines.p0 - prescale_network_acnp_diff prescale_network_acnp_target.dropna(inplace=True) if _CONSTANT_POWER_FACTOR: - _component_dl_q_values = prescale_network_acnp_target * relevant_dangling_lines.power_factor + _component_dl_q_values = prescale_network_acnp_target * relevant_boundary_lines.power_factor else: - _component_dl_q_values = relevant_dangling_lines.q0 + _component_dl_q_values = relevant_boundary_lines.q0 logger.info(f"[ITER {_iteration}] Scaling network component {component_key} {v['countries']} ACNP to scheduled: {scheduled_component_acnp}") - network.update_dangling_lines(id=prescale_network_acnp_target.index, + network.update_boundary_lines(id=prescale_network_acnp_target.index, p0=prescale_network_acnp_target.to_list(), q0=_component_dl_q_values.to_list()) _scaling_results.append({'KEY': 'target-network-acnp', 'GLOBAL': target_network_acnp, 'ITER': _iteration}) @@ -388,12 +388,12 @@ def scale_balance(model: object, return model # Validate total network AC net position alignment - # dangling_lines = get_network_elements(network, pp.network.ElementType.DANGLING_LINE, all_attributes=True) - dangling_lines = network.get_dangling_lines(all_attributes=True) - dangling_lines[_country_col] = dangling_lines.index.map(_elements_to_areas_map[_country_col]) - dangling_lines = dangling_lines.merge(buses.connected_component, how='left', left_on='bus_id', right_index=True) - dangling_lines['boundary_p'] = dangling_lines['boundary_p'] * -1 # invert boundary_p sign to match flow direction - postscale_network_acnp = {k: round(dangling_lines[unpaired_dangling_lines].query("connected_component == @k").boundary_p.sum()) for k, v in valid_components.items()} + # boundary_lines = get_network_elements(network, pp.network.ElementType.BOUNDARY_LINE, all_attributes=True) + boundary_lines = network.get_boundary_lines(all_attributes=True) + boundary_lines[_country_col] = boundary_lines.index.map(_elements_to_areas_map[_country_col]) + boundary_lines = boundary_lines.merge(buses.connected_component, how='left', left_on='bus_id', right_index=True) + boundary_lines['boundary_p'] = boundary_lines['boundary_p'] * -1 # invert boundary_p sign to match flow direction + postscale_network_acnp = {k: round(boundary_lines[unpaired_boundary_lines].query("connected_component == @k").boundary_p.sum()) for k, v in valid_components.items()} _scaling_results.append({'KEY': 'postscale-network-acnp', 'GLOBAL': postscale_network_acnp, 'ITER': _iteration}) logger.info(f"[ITER {_iteration}] POST-SCALE NETWORK ACNP by component: {postscale_network_acnp}") @@ -405,8 +405,8 @@ def scale_balance(model: object, _scaling_results.append(pd.concat([prescale_consumption, pd.Series({'KEY': 'consumption', 'ITER': _iteration})]).to_dict()) # Get pre-scale AC net positions for each control area - dangling_lines = dangling_lines[dangling_lines.connected_component.isin(valid_components.keys())] - prescale_acnp = dangling_lines[dangling_lines.isHvdc == ''].groupby([_country_col, 'connected_component']).boundary_p.sum().reset_index() + boundary_lines = boundary_lines[boundary_lines.connected_component.isin(valid_components.keys())] + prescale_acnp = boundary_lines[boundary_lines.isHvdc == ''].groupby([_country_col, 'connected_component']).boundary_p.sum().reset_index() prescale_acnp.connected_component = prescale_acnp.connected_component.astype(int) _pre_scale_acnp_series = _get_series_from_df(df=prescale_acnp, value_col='boundary_p') _scaling_results.append(pd.concat([_pre_scale_acnp_series, pd.Series({'KEY': 'prescale-acnp', 'ITER': _iteration})]).to_dict()) @@ -485,20 +485,20 @@ def scale_balance(model: object, logger.debug(f"[ITER {_iteration}] POST-SCALE LOSSES: {postscale_losses.to_dict()}") # Get post-scale AC net position - # dangling_lines = get_network_elements(network, pp.network.ElementType.DANGLING_LINE, all_attributes=True) - dangling_lines = network.get_dangling_lines(all_attributes=True) - dangling_lines[_country_col] = dangling_lines.index.map(_elements_to_areas_map[_country_col]) - dangling_lines = dangling_lines.merge(buses.connected_component, how='left', left_on='bus_id', right_index=True) - dangling_lines['boundary_p'] = dangling_lines['boundary_p'] * -1 # invert boundary_p sign to match flow direction - dangling_lines = dangling_lines[dangling_lines.connected_component.isin(valid_components.keys())] - postscale_acnp = dangling_lines[dangling_lines.isHvdc == ''].groupby([_country_col, 'connected_component']).boundary_p.sum().reset_index() + # boundary_lines = get_network_elements(network, pp.network.ElementType.BOUNDARY_LINE, all_attributes=True) + boundary_lines = network.get_boundary_lines(all_attributes=True) + boundary_lines[_country_col] = boundary_lines.index.map(_elements_to_areas_map[_country_col]) + boundary_lines = boundary_lines.merge(buses.connected_component, how='left', left_on='bus_id', right_index=True) + boundary_lines['boundary_p'] = boundary_lines['boundary_p'] * -1 # invert boundary_p sign to match flow direction + boundary_lines = boundary_lines[boundary_lines.connected_component.isin(valid_components.keys())] + postscale_acnp = boundary_lines[boundary_lines.isHvdc == ''].groupby([_country_col, 'connected_component']).boundary_p.sum().reset_index() postscale_acnp.connected_component = postscale_acnp.connected_component.astype(int) _post_scale_acnp_series = _get_series_from_df(df=postscale_acnp, value_col='boundary_p') _scaling_results.append(pd.concat([_post_scale_acnp_series, pd.Series({'KEY': 'postscale-acnp', 'ITER': _iteration})]).to_dict()) logger.info(f"[ITER {_iteration}] POST-SCALE ACNP: {_post_scale_acnp_series.to_dict()}") # Get post-scale total network balance - prescale_total_np = dangling_lines[dangling_lines['paired'] == False].boundary_p.sum() + prescale_total_np = boundary_lines[boundary_lines['paired'] == False].boundary_p.sum() logger.info(f"[ITER {_iteration}] POST-SCALE TOTAL NP: {round(prescale_total_np, 2)}") # Get offset between target and post-scale AC net position diff --git a/pyproject.toml b/pyproject.toml index e6642bb2..03bfddb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "emf" -version = "0.1.0" +version = "1.8.0" description = "Open source tool for European Merging Function" readme = "README.md" requires-python = ">=3.11" dependencies = [ "pandas", "numpy", - "pypowsybl==1.11.2", + "pypowsybl==1.15.0", "aniso8601", "lxml", "edx", diff --git a/uv.lock b/uv.lock index cef8b72f..69068cb7 100644 --- a/uv.lock +++ b/uv.lock @@ -258,7 +258,7 @@ requires-dist = [ { name = "pandas" }, { name = "pika" }, { name = "pyarrow", specifier = ">=24.0.0" }, - { name = "pypowsybl", specifier = "==1.11.2" }, + { name = "pypowsybl", specifier = "==1.15.0" }, { name = "pytz" }, { name = "requests" }, { name = "saxonche" }, @@ -669,7 +669,7 @@ wheels = [ [[package]] name = "pypowsybl" -version = "1.11.2" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "networkx" }, @@ -678,14 +678,22 @@ dependencies = [ { name = "prettytable" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/9e/236816a412fc0f45efe14f6083179eb0bb47569c759c79167f3c6334e286/pypowsybl-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23e628b62521dd46aa8e4b93aee5a58c366ace971d69bbd09775d1294c4d1f85", size = 65053577, upload-time = "2025-06-25T08:55:08.142Z" }, - { url = "https://files.pythonhosted.org/packages/29/b7/5b0ff764cf6e74199069900cf41dd85cc794882007589037f58f55ad12f5/pypowsybl-1.11.2-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:255c9e72a09c89fd0d99f98f02d34ca10f1d64108bd57059d4c7917c541db8b2", size = 67943747, upload-time = "2025-06-25T08:55:27.039Z" }, - { url = "https://files.pythonhosted.org/packages/a8/09/6e67b719b75221ef67cb04f25ba5a702f0ebef2e6c38bec5d6ba7d37d4b0/pypowsybl-1.11.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f52b7d01fb9b99245dc50a89322549eb953e74c3ad1ae95842cab648adc87a", size = 81806619, upload-time = "2025-06-25T08:55:46.774Z" }, - { url = "https://files.pythonhosted.org/packages/76/b2/56d3d887b2d7d74f6251244a9caffa2b17fabf25e063bbe47e94871dea1d/pypowsybl-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:eaf992676631769b0dd1b009cfa041f001eacf1142ea4c087a6928942ba1fdb0", size = 63429009, upload-time = "2025-06-25T08:56:07.142Z" }, - { url = "https://files.pythonhosted.org/packages/b1/57/abfc3da95528a3dc32624c395a3c10cce8e99e2b06f5cf9851b1ca42f0aa/pypowsybl-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:54cda0f1b8115c01587a2fa73ecb76ce7b74435b60f017bed412da02bae7d75f", size = 65060473, upload-time = "2025-06-25T08:55:12.028Z" }, - { url = "https://files.pythonhosted.org/packages/98/23/ed4122a8806420764d1bc4af6ae3f29e1f96df96c374db0bb59b243fe291/pypowsybl-1.11.2-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:a192d50800e4b08c5a04d72835243ffb64318b7652713fd4875ea318e51b6174", size = 67961967, upload-time = "2025-06-25T08:55:30.862Z" }, - { url = "https://files.pythonhosted.org/packages/78/8c/565eed9603adbcf46842c2c4d3b463b4d38d59407faeb16aac18fe31ae90/pypowsybl-1.11.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a03ef2b6244388bdd869a2486646119ec2aa3efa49fc236603ab1ab161eb5ef9", size = 81778331, upload-time = "2025-06-25T08:55:51.303Z" }, - { url = "https://files.pythonhosted.org/packages/01/9f/ecd83ed29870ccc8647f9f0b4b43e630f5430b7262cf158052fdfc6b9a0a/pypowsybl-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:f8afb4255eedc060e81637fdc8943a3be60e52da241367030e2923632a586a08", size = 63423904, upload-time = "2025-06-25T08:56:11.036Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/1a57e7274c35ad1445f9b6950adaafba328112cfbbaefaf53130879e0661/pypowsybl-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b5878246c4ae8ea020841b2423c957dbe331d9a021f53ed9187f3c7b1f8a4dd2", size = 70071991, upload-time = "2026-04-16T15:29:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/bb/98/aef7c8d4c6ff9bd21d5cfae801cb80b12788613acef6c1f2c27e6e2d9465/pypowsybl-1.15.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:25feaf84790e0d3cc05c1828b2a8c7e3776e842352159ca14654f508141e1de5", size = 71256864, upload-time = "2026-04-16T15:29:24.36Z" }, + { url = "https://files.pythonhosted.org/packages/26/bf/36c0c02c47aa96f983f99a2af6e20145e297d14ca79a5dbfcd08ba91190b/pypowsybl-1.15.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:904660e7cf54ab83a7eee5a4c7400107cc559b08da52920321d27b728adb7b94", size = 84735526, upload-time = "2026-04-16T15:29:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/cee08cbd7f80ce988ca803cfbbfbd0a042e50b962466bc1549a793747955/pypowsybl-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:979af9fc896ff8cd0e7beff77e58cf517a0e9b03ab897a49299367eba8188ae2", size = 66921392, upload-time = "2026-04-16T15:30:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a4/eff2576b133e994121ab7ac1e4538f2a6ffb4293493b18e21835c82fb1b8/pypowsybl-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58f309b3400002b199dc744f59661e0b676a37b65cb3ccbb7cf4da1d5f7fc7fb", size = 70075965, upload-time = "2026-04-16T15:29:04.204Z" }, + { url = "https://files.pythonhosted.org/packages/32/64/b5289fbd4f2095272d974e30a7881e342d09545786366dbf70d4b4e300ec/pypowsybl-1.15.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:d9ddee2b9c9be81f13ddb878a1ccaae08177b8837d05890c775b794fb97a6394", size = 71274336, upload-time = "2026-04-16T15:29:28.762Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f2/b14a81cbe875163e4744ac88147644537b34fa77057e9bf12bb8004e52d8/pypowsybl-1.15.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f45e90f2cacb10b6f09c12ffcec3d42d69cb45fd58a460974aaec52039f9472", size = 84741953, upload-time = "2026-04-16T15:29:53.423Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/9e145670d04f5a6da43d891abce9c40b0fb33866bcc2ec488d75b9edcd9a/pypowsybl-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:86c67643d5f3fdb947ec0c7bbaeb78a9aa41c42b44f11b25eb6451a8fb29368d", size = 66922263, upload-time = "2026-04-16T15:30:17.167Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a1/9bd38f1b05db2c96d5d2ac9a82d9010bacc2d433cc4751276a112a2090b7/pypowsybl-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:60539ee319cba8b0f2546bd90c2f53f9686a90aafc3ef80f4579db47b2d13ba2", size = 70075882, upload-time = "2026-04-16T15:29:11.891Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f1/eaa9d2b9dbadec492ac0261f1dee62dd78854195c776c91eca3e27cdc9ed/pypowsybl-1.15.0-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:d16ea51ff4c20e664bcd7ec29013806ea395c4be5c3eda7429f5834a7ba74eb5", size = 71274237, upload-time = "2026-04-16T15:29:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/09/fd0e709efdb3aefe1f98bf1363845099e4d441e537e050e14bf6ce11a087/pypowsybl-1.15.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78fed685f212fa50d6948e6698bc9ce0d2a6d4544b3021d23627c98cb6bbd3ef", size = 84741912, upload-time = "2026-04-16T15:29:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/73/6b/9aaa6ed82db5620d9a5c99725393ba20c2f9ba1fb42874ba3f0d7214b9c1/pypowsybl-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:a93e17ec5ae6a5696060bace089efa939e85c03383973fa2389baacec2a4b509", size = 66922225, upload-time = "2026-04-16T15:30:21.771Z" }, + { url = "https://files.pythonhosted.org/packages/46/1f/1a26f7a2d87ff11906c7058cd0fce05e7b9297ef0bde2e1cfcb68e472099/pypowsybl-1.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8fda7822b730f60664f41616b1608cb620b39f214572690340baf0c459433ea2", size = 70078159, upload-time = "2026-04-16T15:29:15.704Z" }, + { url = "https://files.pythonhosted.org/packages/89/7f/173662a64355b07ce0b13289aaed3c90d86ee202d93b95fa8f77d916a906/pypowsybl-1.15.0-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:b067867301085a45563f879169830d88a8ea77281f86b37d973048868c800e87", size = 71273860, upload-time = "2026-04-16T15:29:37.664Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f3/ba7363846bff9f545ce258d58d5fe9d7964bd225bd6bd4d4a70986db3423/pypowsybl-1.15.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43b905776d575ebc0187f24231722aaa98a60a8dc4e1134d476237d0fd0e541e", size = 84744720, upload-time = "2026-04-16T15:30:03.489Z" }, + { url = "https://files.pythonhosted.org/packages/8f/23/d1507033ac845562d93df3aa0a142a6d7d4868d4a4a132a96407d84d1876/pypowsybl-1.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:a0c4ee76d7c3c752cac7b0c300a3e761ea1d64c41858d4fef90d1a75d78c441f", size = 68604931, upload-time = "2026-04-16T15:30:26.634Z" }, ] [[package]]