Skip to content

Area and AreaBorder Model-DF Generators

AreaModelGenerator

Bases: GeoModelGeneratorBase

Generates comprehensive area model DataFrames from node-to-area mappings.

This class creates detailed area model DataFrames that aggregate node-level data into area-level representations for energy system analysis. It supports automatic area discovery, node counting, and geographic representative point calculation for visualization and spatial analysis.

The generator processes node model data with area assignments to create comprehensive area models suitable for energy system aggregation, market analysis, and spatial visualization workflows.

Key Features
  • Automatic area discovery from node-to-area mappings
  • Representative geographic point calculation for visualization
  • Integration with geometric area data (polygons, boundaries)
  • Support for different area granularities (countries, bidding zones, regions)
  • Robust handling of missing or incomplete area assignments
MESQUAL Integration

Designed to work with MESQUAL's area accounting system, providing area model building capabilities that support spatial energy system analysis, capacity aggregation, and visualization workflows.

Attributes:

Name Type Description
node_model_df DataFrame

Node-level data with area assignments

area_column str

Column name containing area identifiers

geo_location_column str

Column name containing geographic Point objects

Examples:

Basic area model generation:
>>> import pandas as pd
>>> from shapely.geometry import Point
>>>
>>> # Create node model with area assignments
>>> node_data = pd.DataFrame({
>>>     'voltage': [380, 380, 220, 380, 220],
>>>     'country': ['DE', 'DE', 'FR', 'FR', 'BE'],
>>>     'capacity_mw': [2000, 1500, 800, 1200, 600],
>>>     'location': [Point(10, 52), Point(11, 53), Point(2, 48),
>>>                  Point(3, 49), Point(4, 50)]
>>> }, index=['DE1', 'DE2', 'FR1', 'FR2', 'BE1'])
>>>
>>> # Generate area model
>>> generator = AreaModelGenerator(node_data, 'country')
>>> area_model = generator.generate_area_model()
>>> print(area_model)
              node_count projection_point
    country
    DE               2    POINT (10.5 52.5)
    FR               2    POINT (2.5 48.5)
    BE               1    POINT (4 50)

Enhanced area model with geometry:
>>> import geopandas as gpd
>>> from shapely.geometry import Polygon
>>>
>>> # Create area geometries
>>> area_polygons = gpd.GeoDataFrame({
>>>     'geometry': [
>>>         Polygon([(9, 51), (12, 51), (12, 54), (9, 54)]),  # DE
>>>         Polygon([(1, 47), (4, 47), (4, 50), (1, 50)]),   # FR
>>>         Polygon([(3, 49), (5, 49), (5, 51), (3, 51)])    # BE
>>>     ]
>>> }, index=['DE', 'FR', 'BE'])
>>>
>>> # Enhance with geometry
>>> area_model_geo = generator.enhance_with_geometry(area_model, area_polygons)
>>> print(f"Enhanced model has geometry: {'geometry' in area_model_geo.columns}")

Custom enhancement workflow:
>>> # Step-by-step area model building
>>> base_model = generator.generate_base_area_model_from_area_names_in_node_model_df()
>>> enhanced_model = generator.enhance_area_model_df_by_adding_node_count_per_area(base_model)
>>> final_model = generator.enhance_area_model_df_by_adding_representative_geo_point(enhanced_model)
>>> print(f"Created area model with {len(final_model)} areas")
Energy Domain Context
  • Area models are fundamental for energy system analysis, enabling:
    • Projection of node-level data to area-level data (e.g. nodal prices -> area prices)
    • Market zone aggregation and analysis
    • Regional energy balance studies
    • ...
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/area_model_generator.py
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
class AreaModelGenerator(GeoModelGeneratorBase):
    """Generates comprehensive area model DataFrames from node-to-area mappings.

    This class creates detailed area model DataFrames that aggregate node-level data
    into area-level representations for energy system analysis. It supports
    automatic area discovery, node counting, and geographic representative point
    calculation for visualization and spatial analysis.

    The generator processes node model data with area assignments to create
    comprehensive area models suitable for energy system aggregation, market
    analysis, and spatial visualization workflows.

    Key Features:
        - Automatic area discovery from node-to-area mappings
        - Representative geographic point calculation for visualization
        - Integration with geometric area data (polygons, boundaries)
        - Support for different area granularities (countries, bidding zones, regions)
        - Robust handling of missing or incomplete area assignments

    MESQUAL Integration:
        Designed to work with MESQUAL's area accounting system, providing
        area model building capabilities that support spatial energy system analysis,
        capacity aggregation, and visualization workflows.

    Attributes:
        node_model_df (pd.DataFrame): Node-level data with area assignments
        area_column (str): Column name containing area identifiers
        geo_location_column (str): Column name containing geographic Point objects

    Examples:

        Basic area model generation:
        >>> import pandas as pd
        >>> from shapely.geometry import Point
        >>>
        >>> # Create node model with area assignments
        >>> node_data = pd.DataFrame({
        >>>     'voltage': [380, 380, 220, 380, 220],
        >>>     'country': ['DE', 'DE', 'FR', 'FR', 'BE'],
        >>>     'capacity_mw': [2000, 1500, 800, 1200, 600],
        >>>     'location': [Point(10, 52), Point(11, 53), Point(2, 48),
        >>>                  Point(3, 49), Point(4, 50)]
        >>> }, index=['DE1', 'DE2', 'FR1', 'FR2', 'BE1'])
        >>>
        >>> # Generate area model
        >>> generator = AreaModelGenerator(node_data, 'country')
        >>> area_model = generator.generate_area_model()
        >>> print(area_model)
                      node_count projection_point
            country
            DE               2    POINT (10.5 52.5)
            FR               2    POINT (2.5 48.5)
            BE               1    POINT (4 50)

        Enhanced area model with geometry:
        >>> import geopandas as gpd
        >>> from shapely.geometry import Polygon
        >>>
        >>> # Create area geometries
        >>> area_polygons = gpd.GeoDataFrame({
        >>>     'geometry': [
        >>>         Polygon([(9, 51), (12, 51), (12, 54), (9, 54)]),  # DE
        >>>         Polygon([(1, 47), (4, 47), (4, 50), (1, 50)]),   # FR
        >>>         Polygon([(3, 49), (5, 49), (5, 51), (3, 51)])    # BE
        >>>     ]
        >>> }, index=['DE', 'FR', 'BE'])
        >>>
        >>> # Enhance with geometry
        >>> area_model_geo = generator.enhance_with_geometry(area_model, area_polygons)
        >>> print(f"Enhanced model has geometry: {'geometry' in area_model_geo.columns}")

        Custom enhancement workflow:
        >>> # Step-by-step area model building
        >>> base_model = generator.generate_base_area_model_from_area_names_in_node_model_df()
        >>> enhanced_model = generator.enhance_area_model_df_by_adding_node_count_per_area(base_model)
        >>> final_model = generator.enhance_area_model_df_by_adding_representative_geo_point(enhanced_model)
        >>> print(f"Created area model with {len(final_model)} areas")

    Energy Domain Context:
        - Area models are fundamental for energy system analysis, enabling:
            - Projection of node-level data to area-level data (e.g. nodal prices -> area prices)
            - Market zone aggregation and analysis
            - Regional energy balance studies
            - ...
    """

    def __init__(
            self,
            node_model_df: pd.DataFrame,
            area_column: str,
            geo_location_column: str = None,
    ):
        """Initialize the area model generator.

        Args:
            node_model_df: DataFrame containing node-level data with area assignments.
                Must contain area_column with area identifiers for each node.
                May contain geographic Point objects for spatial analysis.
            area_column: Column name in node_model_df containing area assignments
                (e.g., 'country', 'bidding_zone', 'market_region', 'control_area').
            geo_location_column: Column name containing geographic Point objects
                for representative point calculation. If None, automatically
                detects column containing Point geometries.

        Raises:
            ValueError: If area_column is not found in node_model_df columns.

        Example:

            >>> import pandas as pd
            >>> from shapely.geometry import Point
            >>> 
            >>> # Node data with area assignments
            >>> nodes = pd.DataFrame({
            >>>     'voltage_kv': [380, 220, 380, 150],
            >>>     'country': ['DE', 'DE', 'FR', 'FR'], 
            >>>     'bidding_zone': ['DE_LU', 'DE_LU', 'FR', 'FR'],
            >>>     'coordinates': [Point(10, 52), Point(11, 53), Point(2, 48), Point(3, 49)]
            >>> }, index=['DE1', 'DE2', 'FR1', 'FR2'])
            >>> 
            >>> # Initialize for country-level analysis
            >>> generator = AreaModelGenerator(nodes, 'country', 'coordinates')
            >>> 
            >>> # Or let it auto-detect geographic column
            >>> generator = AreaModelGenerator(nodes, 'bidding_zone')
        """
        self.node_model_df = node_model_df
        self.area_column = area_column
        self.geo_location_column = geo_location_column or self._identify_geo_location_column()
        self._validate_inputs()

    def _validate_inputs(self):
        if self.area_column not in self.node_model_df.columns:
            raise ValueError(
                f"Area column '{self.area_column}' not found in node_model_df. "
                f"Available columns: {list(self.node_model_df.columns)}"
            )

    def _identify_geo_location_column(self) -> str | None:
        for c in self.node_model_df.columns:
            if all(isinstance(i, Point) or i is None for i in self.node_model_df[c].values):
                return c
        return None

    def generate_base_area_model_from_area_names_in_node_model_df(self) -> pd.DataFrame:
        """Generate base area model DataFrame from unique area names in node data.

        Creates a minimal area model DataFrame containing only the unique area
        identifiers found in the node model data. This forms the foundation
        for building comprehensive area models.

        Returns:
            pd.DataFrame: Base area model with area identifiers as index.
                Contains no additional columns - serves as starting point
                for enhancement with node counts, geographic data, etc.

        Example:

            >>> generator = AreaModelGenerator(node_data, 'country')
            >>> base_model = generator.generate_base_area_model_from_area_names_in_node_model_df()
            >>> print(base_model)
                Empty DataFrame
                Columns: []
                Index: ['DE', 'FR', 'BE']

        Note:
            Areas with None or NaN values in the area_column are excluded
            from the generated model.
        """
        unique_areas = self.node_model_df[self.area_column].dropna().unique()
        area_model_df = pd.DataFrame(index=unique_areas)
        area_model_df.index.name = self.area_column
        return area_model_df

    def ensure_completeness_of_area_model_df(self, area_model_df: pd.DataFrame) -> pd.DataFrame:
        """Ensure area model contains all areas present in node data.

        Validates and extends an existing area model DataFrame to include
        any areas found in the node data that might be missing from the
        provided area model. This is useful when working with predefined
        area models that may not cover all areas in the dataset.

        Args:
            area_model_df: Existing area model DataFrame to validate and extend.

        Returns:
            pd.DataFrame: Complete area model containing all areas from node data.
                Existing data is preserved, new areas are added with NaN values
                for existing columns.

        Example:

            >>> # Predefined area model missing some areas
            >>> partial_model = pd.DataFrame({
            >>>     'max_price': [5000, 3000]
            >>> }, index=['DE', 'FR'])
            >>> 
            >>> # Ensure completeness (adds 'BE' if present in node data)
            >>> complete_model = generator.ensure_completeness_of_area_model_df(partial_model)
            >>> print(complete_model)
                          max_price
                country
                DE             5000
                FR             3000
                BE              NaN

        Use Case:
            Essential for maintaining data consistency when combining
            predefined area models with dynamic node-based area discovery.
        """
        complete_set = area_model_df.index.to_list()
        for a in self.node_model_df[self.area_column].unique():
            if a is not None and a not in complete_set:
                complete_set.append(a)
        return area_model_df.reindex(complete_set)

    def enhance_area_model_df_by_adding_node_count_per_area(
            self,
            area_model_df: pd.DataFrame,
            node_count_column_name: str = 'node_count'
    ) -> pd.DataFrame:
        """Enhance area model by adding node count statistics per area.

        Aggregates the number of nodes assigned to each area and adds this
        information to the area model DataFrame. Node counts are essential
        for understanding infrastructure density and capacity distribution.

        Args:
            area_model_df: Base area model DataFrame to enhance.
            node_count_column_name: Name for the new node count column.
                Defaults to 'node_count'.

        Returns:
            pd.DataFrame: Enhanced area model with node count column added.
                Existing data is preserved, node counts are added for all areas.
                Areas not present in node data will have NaN node counts.

        Example:

            >>> base_model = generator.generate_base_area_model_from_area_names_in_node_model_df()
            >>> enhanced_model = generator.enhance_area_model_df_by_adding_node_count_per_area(base_model)
            >>> print(enhanced_model)
                          node_count
                country
                DE               2
                FR               2
                BE               1

            >>> # Custom column name
            >>> enhanced_model = generator.enhance_area_model_df_by_adding_node_count_per_area(
            >>>     base_model, 'infrastructure_count'
            >>> )
        """
        enhanced_df = area_model_df.copy()
        node_counts = self.node_model_df[self.area_column].value_counts().to_dict()
        for node, count in node_counts.items():
            if node in enhanced_df.index:
                enhanced_df.loc[node, node_count_column_name] = count
        return enhanced_df

    def enhance_area_model_df_by_adding_representative_geo_point(
            self,
            area_model_df: pd.DataFrame | gpd.GeoDataFrame,
            target_column_name: str = 'projection_point',
            round_point_decimals: int = 4,
    ) -> pd.DataFrame:
        """Enhance area model by adding representative geographic points for
        labeling and KPI printing in map visualizations.

        Calculates representative geographic points for each area based on
        either area geometries (if available) or node locations within each area.
        These points are useful for visualization, labeling, and spatial analysis.

        The method supports two calculation modes:
        1. Geometry-based: Uses area polygon centroids or representative points
        2. Node-based: Calculates centroid from node locations within each area

        Args:
            area_model_df: Area model DataFrame to enhance. Can be regular DataFrame
                or GeoDataFrame with 'geometry' column.
            target_column_name: Name for the new representative point column.
                Defaults to 'projection_point'.
            round_point_decimals: Number of decimal places for coordinate rounding.
                Set to None to disable rounding. Defaults to 4.

        Returns:
            pd.DataFrame: Enhanced area model with representative points added.
                Points are added as Shapely Point objects suitable for mapping
                and spatial analysis.

        Raises:
            TypeError: If geo_location_column contains non-Point objects.

        Example:

            >>> # Node-based representative points
            >>> enhanced_model = generator.enhance_area_model_df_by_adding_representative_geo_point(base_model)
            >>> print(enhanced_model)
                          projection_point
                country
                DE        POINT (10.5 52.5)
                FR        POINT (2.5 48.5)
                BE        POINT (4 50)

            >>> # With custom column name and precision
            >>> enhanced_model = generator.enhance_area_model_df_by_adding_representative_geo_point(
            >>>     base_model, 'center_point', round_point_decimals=2
            >>> )

            >>> # Access coordinates for mapping
            >>> center = enhanced_model.loc['DE', 'projection_point']
            >>> print(f"DE center: {center.x:.2f}, {center.y:.2f}")
        """
        enhanced_df = area_model_df.copy()

        def round_point(point: Point | None) -> Point | None:
            if point is None or round_point_decimals is None:
                return point
            return type(point)(round(point.x, round_point_decimals), round(point.y, round_point_decimals))

        if target_column_name not in enhanced_df:
            enhanced_df[target_column_name] = None

        for area in enhanced_df.index:
            if pd.notna(enhanced_df.loc[area, target_column_name]):
                continue
            if 'geometry' in enhanced_df.columns:
                geo = enhanced_df.loc[area, 'geometry']
                if pd.notna(geo) and isinstance(geo, (Polygon, MultiPolygon)):
                    enhanced_df.loc[area, target_column_name] = round_point(self.get_representative_area_point(geo))
            elif self.geo_location_column:
                nodes = self.node_model_df.loc[self.node_model_df[self.area_column] == area, self.geo_location_column]
                nodes = nodes.dropna()
                if not nodes.empty:
                    locations = [n for n in nodes.values if n is not None]
                    if not all(isinstance(i, Point) for i in locations):
                        raise TypeError(
                            f'Geographic location column "{self.geo_location_column}" must contain only '
                            f'Point objects. Found: {[type(i).__name__ for i in locations if not isinstance(i, Point)]}'
                        )
                    representative_point = self._compute_representative_point_from_cloud_of_2d_points(locations)
                    enhanced_df.loc[area, target_column_name] = round_point(representative_point)
        return enhanced_df

    def generate_area_model(self) -> pd.DataFrame:
        """Generate complete area model with node counts and representative points.

        Creates a comprehensive area model DataFrame by combining base area
        discovery, node count aggregation, and representative geographic point
        calculation. This is the main method for generating complete area models.

        The generated model includes:
            - All unique areas from node data
            - Node count per area for capacity/infrastructure analysis
            - Representative geographic points for visualization

        Returns:
            pd.DataFrame: Complete area model with node counts and geographic data.
                Index contains area identifiers, columns include 'node_count'
                and 'projection_point' (if geographic data available).

        Example:

            >>> generator = AreaModelGenerator(node_data, 'country')
            >>> area_model = generator.generate_area_model()
            >>> print(area_model)
                          node_count projection_point
                country
                DE               2    POINT (10.5 52.5)
                FR               2    POINT (2.5 48.5)
                BE               1    POINT (4 50)
        """
        area_model_df = self.generate_base_area_model_from_area_names_in_node_model_df()
        area_model_df = self.enhance_area_model_df_by_adding_node_count_per_area(area_model_df)
        area_model_df = self.enhance_area_model_df_by_adding_representative_geo_point(area_model_df)
        return area_model_df

    def enhance_with_geometry(
        self,
        area_model_df: pd.DataFrame,
        area_gdf: gpd.GeoDataFrame
    ) -> gpd.GeoDataFrame:
        """Enhance area model with geometric polygon data for spatial analysis.

        Integrates area polygon geometries from a GeoDataFrame into the area model,
        enabling advanced spatial analysis, visualization, and border calculations.
        The method matches areas by index and creates a proper GeoDataFrame output.

        Args:
            area_model_df: Area model DataFrame to enhance with geometry.
            area_gdf: GeoDataFrame containing area polygon geometries.
                Must have 'geometry' column with Polygon or MultiPolygon objects.
                Areas are matched by index values.

        Returns:
            gpd.GeoDataFrame: Enhanced area model as GeoDataFrame with geometry column.
                All original data is preserved, geometry column is added for areas
                that exist in both DataFrames. Missing geometries are set to None.

        Example:

            >>> import geopandas as gpd
            >>> from shapely.geometry import Polygon
            >>> 
            >>> # Create area geometries
            >>> area_polygons = gpd.GeoDataFrame({
            >>>     'area_name': ['Germany', 'France', 'Belgium'],
            >>>     'geometry': [
            >>>         Polygon([(9, 51), (12, 51), (12, 54), (9, 54)]),
            >>>         Polygon([(1, 47), (4, 47), (4, 50), (1, 50)]),
            >>>         Polygon([(3, 49), (5, 49), (5, 51), (3, 51)])
            >>>     ]
            >>> }, index=['DE', 'FR', 'BE'])
            >>> 
            >>> # Enhance area model with geometry
            >>> geo_model = generator.enhance_with_geometry(area_model, area_polygons)
            >>> print(f"Model has geometry: {isinstance(geo_model, gpd.GeoDataFrame)}")
            >>> 
            >>> # Use for spatial operations
            >>> total_area = geo_model['geometry'].area.sum()
            >>> print(f"Total area: {total_area:.0f} square units")
        """
        enhanced_df = area_model_df.copy()
        if 'geometry' not in enhanced_df.columns:
            enhanced_df['geometry'] = None
        for area in area_model_df.index:
            if area in area_gdf.index:
                enhanced_df.loc[area, 'geometry'] = area_gdf.loc[area, 'geometry']
        if not isinstance(enhanced_df, gpd.GeoDataFrame):
            enhanced_df = gpd.GeoDataFrame(enhanced_df, geometry='geometry')
        return enhanced_df

__init__

__init__(node_model_df: DataFrame, area_column: str, geo_location_column: str = None)

Initialize the area model generator.

Parameters:

Name Type Description Default
node_model_df DataFrame

DataFrame containing node-level data with area assignments. Must contain area_column with area identifiers for each node. May contain geographic Point objects for spatial analysis.

required
area_column str

Column name in node_model_df containing area assignments (e.g., 'country', 'bidding_zone', 'market_region', 'control_area').

required
geo_location_column str

Column name containing geographic Point objects for representative point calculation. If None, automatically detects column containing Point geometries.

None

Raises:

Type Description
ValueError

If area_column is not found in node_model_df columns.

Example:

>>> import pandas as pd
>>> from shapely.geometry import Point
>>> 
>>> # Node data with area assignments
>>> nodes = pd.DataFrame({
>>>     'voltage_kv': [380, 220, 380, 150],
>>>     'country': ['DE', 'DE', 'FR', 'FR'], 
>>>     'bidding_zone': ['DE_LU', 'DE_LU', 'FR', 'FR'],
>>>     'coordinates': [Point(10, 52), Point(11, 53), Point(2, 48), Point(3, 49)]
>>> }, index=['DE1', 'DE2', 'FR1', 'FR2'])
>>> 
>>> # Initialize for country-level analysis
>>> generator = AreaModelGenerator(nodes, 'country', 'coordinates')
>>> 
>>> # Or let it auto-detect geographic column
>>> generator = AreaModelGenerator(nodes, 'bidding_zone')
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/area_model_generator.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def __init__(
        self,
        node_model_df: pd.DataFrame,
        area_column: str,
        geo_location_column: str = None,
):
    """Initialize the area model generator.

    Args:
        node_model_df: DataFrame containing node-level data with area assignments.
            Must contain area_column with area identifiers for each node.
            May contain geographic Point objects for spatial analysis.
        area_column: Column name in node_model_df containing area assignments
            (e.g., 'country', 'bidding_zone', 'market_region', 'control_area').
        geo_location_column: Column name containing geographic Point objects
            for representative point calculation. If None, automatically
            detects column containing Point geometries.

    Raises:
        ValueError: If area_column is not found in node_model_df columns.

    Example:

        >>> import pandas as pd
        >>> from shapely.geometry import Point
        >>> 
        >>> # Node data with area assignments
        >>> nodes = pd.DataFrame({
        >>>     'voltage_kv': [380, 220, 380, 150],
        >>>     'country': ['DE', 'DE', 'FR', 'FR'], 
        >>>     'bidding_zone': ['DE_LU', 'DE_LU', 'FR', 'FR'],
        >>>     'coordinates': [Point(10, 52), Point(11, 53), Point(2, 48), Point(3, 49)]
        >>> }, index=['DE1', 'DE2', 'FR1', 'FR2'])
        >>> 
        >>> # Initialize for country-level analysis
        >>> generator = AreaModelGenerator(nodes, 'country', 'coordinates')
        >>> 
        >>> # Or let it auto-detect geographic column
        >>> generator = AreaModelGenerator(nodes, 'bidding_zone')
    """
    self.node_model_df = node_model_df
    self.area_column = area_column
    self.geo_location_column = geo_location_column or self._identify_geo_location_column()
    self._validate_inputs()

generate_base_area_model_from_area_names_in_node_model_df

generate_base_area_model_from_area_names_in_node_model_df() -> DataFrame

Generate base area model DataFrame from unique area names in node data.

Creates a minimal area model DataFrame containing only the unique area identifiers found in the node model data. This forms the foundation for building comprehensive area models.

Returns:

Type Description
DataFrame

pd.DataFrame: Base area model with area identifiers as index. Contains no additional columns - serves as starting point for enhancement with node counts, geographic data, etc.

Example:

>>> generator = AreaModelGenerator(node_data, 'country')
>>> base_model = generator.generate_base_area_model_from_area_names_in_node_model_df()
>>> print(base_model)
    Empty DataFrame
    Columns: []
    Index: ['DE', 'FR', 'BE']
Note

Areas with None or NaN values in the area_column are excluded from the generated model.

Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/area_model_generator.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def generate_base_area_model_from_area_names_in_node_model_df(self) -> pd.DataFrame:
    """Generate base area model DataFrame from unique area names in node data.

    Creates a minimal area model DataFrame containing only the unique area
    identifiers found in the node model data. This forms the foundation
    for building comprehensive area models.

    Returns:
        pd.DataFrame: Base area model with area identifiers as index.
            Contains no additional columns - serves as starting point
            for enhancement with node counts, geographic data, etc.

    Example:

        >>> generator = AreaModelGenerator(node_data, 'country')
        >>> base_model = generator.generate_base_area_model_from_area_names_in_node_model_df()
        >>> print(base_model)
            Empty DataFrame
            Columns: []
            Index: ['DE', 'FR', 'BE']

    Note:
        Areas with None or NaN values in the area_column are excluded
        from the generated model.
    """
    unique_areas = self.node_model_df[self.area_column].dropna().unique()
    area_model_df = pd.DataFrame(index=unique_areas)
    area_model_df.index.name = self.area_column
    return area_model_df

ensure_completeness_of_area_model_df

ensure_completeness_of_area_model_df(area_model_df: DataFrame) -> DataFrame

Ensure area model contains all areas present in node data.

Validates and extends an existing area model DataFrame to include any areas found in the node data that might be missing from the provided area model. This is useful when working with predefined area models that may not cover all areas in the dataset.

Parameters:

Name Type Description Default
area_model_df DataFrame

Existing area model DataFrame to validate and extend.

required

Returns:

Type Description
DataFrame

pd.DataFrame: Complete area model containing all areas from node data. Existing data is preserved, new areas are added with NaN values for existing columns.

Example:

>>> # Predefined area model missing some areas
>>> partial_model = pd.DataFrame({
>>>     'max_price': [5000, 3000]
>>> }, index=['DE', 'FR'])
>>> 
>>> # Ensure completeness (adds 'BE' if present in node data)
>>> complete_model = generator.ensure_completeness_of_area_model_df(partial_model)
>>> print(complete_model)
              max_price
    country
    DE             5000
    FR             3000
    BE              NaN
Use Case

Essential for maintaining data consistency when combining predefined area models with dynamic node-based area discovery.

Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/area_model_generator.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def ensure_completeness_of_area_model_df(self, area_model_df: pd.DataFrame) -> pd.DataFrame:
    """Ensure area model contains all areas present in node data.

    Validates and extends an existing area model DataFrame to include
    any areas found in the node data that might be missing from the
    provided area model. This is useful when working with predefined
    area models that may not cover all areas in the dataset.

    Args:
        area_model_df: Existing area model DataFrame to validate and extend.

    Returns:
        pd.DataFrame: Complete area model containing all areas from node data.
            Existing data is preserved, new areas are added with NaN values
            for existing columns.

    Example:

        >>> # Predefined area model missing some areas
        >>> partial_model = pd.DataFrame({
        >>>     'max_price': [5000, 3000]
        >>> }, index=['DE', 'FR'])
        >>> 
        >>> # Ensure completeness (adds 'BE' if present in node data)
        >>> complete_model = generator.ensure_completeness_of_area_model_df(partial_model)
        >>> print(complete_model)
                      max_price
            country
            DE             5000
            FR             3000
            BE              NaN

    Use Case:
        Essential for maintaining data consistency when combining
        predefined area models with dynamic node-based area discovery.
    """
    complete_set = area_model_df.index.to_list()
    for a in self.node_model_df[self.area_column].unique():
        if a is not None and a not in complete_set:
            complete_set.append(a)
    return area_model_df.reindex(complete_set)

enhance_area_model_df_by_adding_node_count_per_area

enhance_area_model_df_by_adding_node_count_per_area(area_model_df: DataFrame, node_count_column_name: str = 'node_count') -> DataFrame

Enhance area model by adding node count statistics per area.

Aggregates the number of nodes assigned to each area and adds this information to the area model DataFrame. Node counts are essential for understanding infrastructure density and capacity distribution.

Parameters:

Name Type Description Default
area_model_df DataFrame

Base area model DataFrame to enhance.

required
node_count_column_name str

Name for the new node count column. Defaults to 'node_count'.

'node_count'

Returns:

Type Description
DataFrame

pd.DataFrame: Enhanced area model with node count column added. Existing data is preserved, node counts are added for all areas. Areas not present in node data will have NaN node counts.

Example:

>>> base_model = generator.generate_base_area_model_from_area_names_in_node_model_df()
>>> enhanced_model = generator.enhance_area_model_df_by_adding_node_count_per_area(base_model)
>>> print(enhanced_model)
              node_count
    country
    DE               2
    FR               2
    BE               1

>>> # Custom column name
>>> enhanced_model = generator.enhance_area_model_df_by_adding_node_count_per_area(
>>>     base_model, 'infrastructure_count'
>>> )
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/area_model_generator.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
def enhance_area_model_df_by_adding_node_count_per_area(
        self,
        area_model_df: pd.DataFrame,
        node_count_column_name: str = 'node_count'
) -> pd.DataFrame:
    """Enhance area model by adding node count statistics per area.

    Aggregates the number of nodes assigned to each area and adds this
    information to the area model DataFrame. Node counts are essential
    for understanding infrastructure density and capacity distribution.

    Args:
        area_model_df: Base area model DataFrame to enhance.
        node_count_column_name: Name for the new node count column.
            Defaults to 'node_count'.

    Returns:
        pd.DataFrame: Enhanced area model with node count column added.
            Existing data is preserved, node counts are added for all areas.
            Areas not present in node data will have NaN node counts.

    Example:

        >>> base_model = generator.generate_base_area_model_from_area_names_in_node_model_df()
        >>> enhanced_model = generator.enhance_area_model_df_by_adding_node_count_per_area(base_model)
        >>> print(enhanced_model)
                      node_count
            country
            DE               2
            FR               2
            BE               1

        >>> # Custom column name
        >>> enhanced_model = generator.enhance_area_model_df_by_adding_node_count_per_area(
        >>>     base_model, 'infrastructure_count'
        >>> )
    """
    enhanced_df = area_model_df.copy()
    node_counts = self.node_model_df[self.area_column].value_counts().to_dict()
    for node, count in node_counts.items():
        if node in enhanced_df.index:
            enhanced_df.loc[node, node_count_column_name] = count
    return enhanced_df

enhance_area_model_df_by_adding_representative_geo_point

enhance_area_model_df_by_adding_representative_geo_point(area_model_df: DataFrame | GeoDataFrame, target_column_name: str = 'projection_point', round_point_decimals: int = 4) -> DataFrame

Enhance area model by adding representative geographic points for labeling and KPI printing in map visualizations.

Calculates representative geographic points for each area based on either area geometries (if available) or node locations within each area. These points are useful for visualization, labeling, and spatial analysis.

The method supports two calculation modes: 1. Geometry-based: Uses area polygon centroids or representative points 2. Node-based: Calculates centroid from node locations within each area

Parameters:

Name Type Description Default
area_model_df DataFrame | GeoDataFrame

Area model DataFrame to enhance. Can be regular DataFrame or GeoDataFrame with 'geometry' column.

required
target_column_name str

Name for the new representative point column. Defaults to 'projection_point'.

'projection_point'
round_point_decimals int

Number of decimal places for coordinate rounding. Set to None to disable rounding. Defaults to 4.

4

Returns:

Type Description
DataFrame

pd.DataFrame: Enhanced area model with representative points added. Points are added as Shapely Point objects suitable for mapping and spatial analysis.

Raises:

Type Description
TypeError

If geo_location_column contains non-Point objects.

Example:

>>> # Node-based representative points
>>> enhanced_model = generator.enhance_area_model_df_by_adding_representative_geo_point(base_model)
>>> print(enhanced_model)
              projection_point
    country
    DE        POINT (10.5 52.5)
    FR        POINT (2.5 48.5)
    BE        POINT (4 50)

>>> # With custom column name and precision
>>> enhanced_model = generator.enhance_area_model_df_by_adding_representative_geo_point(
>>>     base_model, 'center_point', round_point_decimals=2
>>> )

>>> # Access coordinates for mapping
>>> center = enhanced_model.loc['DE', 'projection_point']
>>> print(f"DE center: {center.x:.2f}, {center.y:.2f}")
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/area_model_generator.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def enhance_area_model_df_by_adding_representative_geo_point(
        self,
        area_model_df: pd.DataFrame | gpd.GeoDataFrame,
        target_column_name: str = 'projection_point',
        round_point_decimals: int = 4,
) -> pd.DataFrame:
    """Enhance area model by adding representative geographic points for
    labeling and KPI printing in map visualizations.

    Calculates representative geographic points for each area based on
    either area geometries (if available) or node locations within each area.
    These points are useful for visualization, labeling, and spatial analysis.

    The method supports two calculation modes:
    1. Geometry-based: Uses area polygon centroids or representative points
    2. Node-based: Calculates centroid from node locations within each area

    Args:
        area_model_df: Area model DataFrame to enhance. Can be regular DataFrame
            or GeoDataFrame with 'geometry' column.
        target_column_name: Name for the new representative point column.
            Defaults to 'projection_point'.
        round_point_decimals: Number of decimal places for coordinate rounding.
            Set to None to disable rounding. Defaults to 4.

    Returns:
        pd.DataFrame: Enhanced area model with representative points added.
            Points are added as Shapely Point objects suitable for mapping
            and spatial analysis.

    Raises:
        TypeError: If geo_location_column contains non-Point objects.

    Example:

        >>> # Node-based representative points
        >>> enhanced_model = generator.enhance_area_model_df_by_adding_representative_geo_point(base_model)
        >>> print(enhanced_model)
                      projection_point
            country
            DE        POINT (10.5 52.5)
            FR        POINT (2.5 48.5)
            BE        POINT (4 50)

        >>> # With custom column name and precision
        >>> enhanced_model = generator.enhance_area_model_df_by_adding_representative_geo_point(
        >>>     base_model, 'center_point', round_point_decimals=2
        >>> )

        >>> # Access coordinates for mapping
        >>> center = enhanced_model.loc['DE', 'projection_point']
        >>> print(f"DE center: {center.x:.2f}, {center.y:.2f}")
    """
    enhanced_df = area_model_df.copy()

    def round_point(point: Point | None) -> Point | None:
        if point is None or round_point_decimals is None:
            return point
        return type(point)(round(point.x, round_point_decimals), round(point.y, round_point_decimals))

    if target_column_name not in enhanced_df:
        enhanced_df[target_column_name] = None

    for area in enhanced_df.index:
        if pd.notna(enhanced_df.loc[area, target_column_name]):
            continue
        if 'geometry' in enhanced_df.columns:
            geo = enhanced_df.loc[area, 'geometry']
            if pd.notna(geo) and isinstance(geo, (Polygon, MultiPolygon)):
                enhanced_df.loc[area, target_column_name] = round_point(self.get_representative_area_point(geo))
        elif self.geo_location_column:
            nodes = self.node_model_df.loc[self.node_model_df[self.area_column] == area, self.geo_location_column]
            nodes = nodes.dropna()
            if not nodes.empty:
                locations = [n for n in nodes.values if n is not None]
                if not all(isinstance(i, Point) for i in locations):
                    raise TypeError(
                        f'Geographic location column "{self.geo_location_column}" must contain only '
                        f'Point objects. Found: {[type(i).__name__ for i in locations if not isinstance(i, Point)]}'
                    )
                representative_point = self._compute_representative_point_from_cloud_of_2d_points(locations)
                enhanced_df.loc[area, target_column_name] = round_point(representative_point)
    return enhanced_df

generate_area_model

generate_area_model() -> DataFrame

Generate complete area model with node counts and representative points.

Creates a comprehensive area model DataFrame by combining base area discovery, node count aggregation, and representative geographic point calculation. This is the main method for generating complete area models.

The generated model includes
  • All unique areas from node data
  • Node count per area for capacity/infrastructure analysis
  • Representative geographic points for visualization

Returns:

Type Description
DataFrame

pd.DataFrame: Complete area model with node counts and geographic data. Index contains area identifiers, columns include 'node_count' and 'projection_point' (if geographic data available).

Example:

>>> generator = AreaModelGenerator(node_data, 'country')
>>> area_model = generator.generate_area_model()
>>> print(area_model)
              node_count projection_point
    country
    DE               2    POINT (10.5 52.5)
    FR               2    POINT (2.5 48.5)
    BE               1    POINT (4 50)
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/area_model_generator.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
def generate_area_model(self) -> pd.DataFrame:
    """Generate complete area model with node counts and representative points.

    Creates a comprehensive area model DataFrame by combining base area
    discovery, node count aggregation, and representative geographic point
    calculation. This is the main method for generating complete area models.

    The generated model includes:
        - All unique areas from node data
        - Node count per area for capacity/infrastructure analysis
        - Representative geographic points for visualization

    Returns:
        pd.DataFrame: Complete area model with node counts and geographic data.
            Index contains area identifiers, columns include 'node_count'
            and 'projection_point' (if geographic data available).

    Example:

        >>> generator = AreaModelGenerator(node_data, 'country')
        >>> area_model = generator.generate_area_model()
        >>> print(area_model)
                      node_count projection_point
            country
            DE               2    POINT (10.5 52.5)
            FR               2    POINT (2.5 48.5)
            BE               1    POINT (4 50)
    """
    area_model_df = self.generate_base_area_model_from_area_names_in_node_model_df()
    area_model_df = self.enhance_area_model_df_by_adding_node_count_per_area(area_model_df)
    area_model_df = self.enhance_area_model_df_by_adding_representative_geo_point(area_model_df)
    return area_model_df

enhance_with_geometry

enhance_with_geometry(area_model_df: DataFrame, area_gdf: GeoDataFrame) -> GeoDataFrame

Enhance area model with geometric polygon data for spatial analysis.

Integrates area polygon geometries from a GeoDataFrame into the area model, enabling advanced spatial analysis, visualization, and border calculations. The method matches areas by index and creates a proper GeoDataFrame output.

Parameters:

Name Type Description Default
area_model_df DataFrame

Area model DataFrame to enhance with geometry.

required
area_gdf GeoDataFrame

GeoDataFrame containing area polygon geometries. Must have 'geometry' column with Polygon or MultiPolygon objects. Areas are matched by index values.

required

Returns:

Type Description
GeoDataFrame

gpd.GeoDataFrame: Enhanced area model as GeoDataFrame with geometry column. All original data is preserved, geometry column is added for areas that exist in both DataFrames. Missing geometries are set to None.

Example:

>>> import geopandas as gpd
>>> from shapely.geometry import Polygon
>>> 
>>> # Create area geometries
>>> area_polygons = gpd.GeoDataFrame({
>>>     'area_name': ['Germany', 'France', 'Belgium'],
>>>     'geometry': [
>>>         Polygon([(9, 51), (12, 51), (12, 54), (9, 54)]),
>>>         Polygon([(1, 47), (4, 47), (4, 50), (1, 50)]),
>>>         Polygon([(3, 49), (5, 49), (5, 51), (3, 51)])
>>>     ]
>>> }, index=['DE', 'FR', 'BE'])
>>> 
>>> # Enhance area model with geometry
>>> geo_model = generator.enhance_with_geometry(area_model, area_polygons)
>>> print(f"Model has geometry: {isinstance(geo_model, gpd.GeoDataFrame)}")
>>> 
>>> # Use for spatial operations
>>> total_area = geo_model['geometry'].area.sum()
>>> print(f"Total area: {total_area:.0f} square units")
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/area_model_generator.py
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
def enhance_with_geometry(
    self,
    area_model_df: pd.DataFrame,
    area_gdf: gpd.GeoDataFrame
) -> gpd.GeoDataFrame:
    """Enhance area model with geometric polygon data for spatial analysis.

    Integrates area polygon geometries from a GeoDataFrame into the area model,
    enabling advanced spatial analysis, visualization, and border calculations.
    The method matches areas by index and creates a proper GeoDataFrame output.

    Args:
        area_model_df: Area model DataFrame to enhance with geometry.
        area_gdf: GeoDataFrame containing area polygon geometries.
            Must have 'geometry' column with Polygon or MultiPolygon objects.
            Areas are matched by index values.

    Returns:
        gpd.GeoDataFrame: Enhanced area model as GeoDataFrame with geometry column.
            All original data is preserved, geometry column is added for areas
            that exist in both DataFrames. Missing geometries are set to None.

    Example:

        >>> import geopandas as gpd
        >>> from shapely.geometry import Polygon
        >>> 
        >>> # Create area geometries
        >>> area_polygons = gpd.GeoDataFrame({
        >>>     'area_name': ['Germany', 'France', 'Belgium'],
        >>>     'geometry': [
        >>>         Polygon([(9, 51), (12, 51), (12, 54), (9, 54)]),
        >>>         Polygon([(1, 47), (4, 47), (4, 50), (1, 50)]),
        >>>         Polygon([(3, 49), (5, 49), (5, 51), (3, 51)])
        >>>     ]
        >>> }, index=['DE', 'FR', 'BE'])
        >>> 
        >>> # Enhance area model with geometry
        >>> geo_model = generator.enhance_with_geometry(area_model, area_polygons)
        >>> print(f"Model has geometry: {isinstance(geo_model, gpd.GeoDataFrame)}")
        >>> 
        >>> # Use for spatial operations
        >>> total_area = geo_model['geometry'].area.sum()
        >>> print(f"Total area: {total_area:.0f} square units")
    """
    enhanced_df = area_model_df.copy()
    if 'geometry' not in enhanced_df.columns:
        enhanced_df['geometry'] = None
    for area in area_model_df.index:
        if area in area_gdf.index:
            enhanced_df.loc[area, 'geometry'] = area_gdf.loc[area, 'geometry']
    if not isinstance(enhanced_df, gpd.GeoDataFrame):
        enhanced_df = gpd.GeoDataFrame(enhanced_df, geometry='geometry')
    return enhanced_df

Border model generation for energy system area connectivity analysis.

This module provides functionality for identifying and modeling borders between energy system areas based on line topologies. It supports the creation of comprehensive border_model_dfs that capture directional relationships, naming conventions, and geometric properties essential for energy systems analysis.

Key Capabilities
  • Automatic border identification from line topology
  • Standardized border naming conventions with directional awareness
  • Integration with geometric border calculators
  • Network graph generation for area connectivity analysis
  • Support for both physical and logical borders (geographically touching borders vs geographically separated borders)
Typical Energy Use Cases
  • Modeling interconnections between countries, control areas, or market zones
  • Cross-border capacity and flow analysis
  • Network visualization and analysis
MESQUAL Integration

This module integrates with MESQUAL's area accounting system to provide border_model_df building capabilities that support spatial energy system analysis and cross-border flow calculations.

AreaBorderNamingConventions

Standardized naming conventions for energy system area borders.

This class provides consistent naming patterns for borders between energy system areas (countries, bidding zones, market regions). It ensures standardized naming across different analysis workflows and supports bidirectional relationship management.

The naming system supports
  • Configurable separators and prefixes/suffixes
  • Bidirectional border identification (A-B and B-A)
  • Alphabetically sorted canonical border names
  • Consistent column naming for source and target areas
Key Features
  • Configurable naming patterns for different use cases
  • Automatic opposite border name generation
  • Alphabetical sorting for canonical border representation
  • Consistent identifier generation for database/DataFrame columns

Attributes:

Name Type Description
JOIN_AREA_NAMES_BY str

Separator for area names in border identifiers

SOURCE_AREA_IDENTIFIER_SUFFIX str

Suffix for source area column names

TARGET_AREA_IDENTIFIER_SUFFIX str

Suffix for target area column names

OPPOSITE_BORDER_IDENTIFIER str

Column name for opposite border references

SORTED_BORDER_IDENTIFIER str

Column name for alphabetically sorted borders

NAME_IS_ALPHABETICALLY_SORTED_IDENTIFIER str

Boolean indicator column

Example:

>>> conventions = AreaBorderNamingConventions('country')
>>> border_name = conventions.get_area_border_name('DE', 'FR')
>>> print(border_name)  # 'DE - FR'
>>> opposite = conventions.get_opposite_area_border_name(border_name)
>>> print(opposite)  # 'FR - DE'
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_generator.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
class AreaBorderNamingConventions:
    """Standardized naming conventions for energy system area borders.

    This class provides consistent naming patterns for borders between energy
    system areas (countries, bidding zones, market regions). It ensures
    standardized naming across different analysis workflows and supports
    bidirectional relationship management.

    The naming system supports:
        - Configurable separators and prefixes/suffixes
        - Bidirectional border identification (A-B and B-A)
        - Alphabetically sorted canonical border names
        - Consistent column naming for source and target areas

    Key Features:
        - Configurable naming patterns for different use cases
        - Automatic opposite border name generation
        - Alphabetical sorting for canonical border representation
        - Consistent identifier generation for database/DataFrame columns

    Attributes:
        JOIN_AREA_NAMES_BY (str): Separator for area names in border identifiers
        SOURCE_AREA_IDENTIFIER_SUFFIX (str): Suffix for source area column names
        TARGET_AREA_IDENTIFIER_SUFFIX (str): Suffix for target area column names
        OPPOSITE_BORDER_IDENTIFIER (str): Column name for opposite border references
        SORTED_BORDER_IDENTIFIER (str): Column name for alphabetically sorted borders
        NAME_IS_ALPHABETICALLY_SORTED_IDENTIFIER (str): Boolean indicator column

    Example:

        >>> conventions = AreaBorderNamingConventions('country')
        >>> border_name = conventions.get_area_border_name('DE', 'FR')
        >>> print(border_name)  # 'DE - FR'
        >>> opposite = conventions.get_opposite_area_border_name(border_name)
        >>> print(opposite)  # 'FR - DE'
    """

    JOIN_AREA_NAMES_BY = ' - '
    SOURCE_AREA_IDENTIFIER_PREFIX = ''
    TARGET_AREA_IDENTIFIER_PREFIX = ''
    SOURCE_AREA_IDENTIFIER_SUFFIX = '_from'
    TARGET_AREA_IDENTIFIER_SUFFIX = '_to'
    OPPOSITE_BORDER_IDENTIFIER = 'opposite_border'
    SORTED_BORDER_IDENTIFIER = 'sorted_border'
    NAME_IS_ALPHABETICALLY_SORTED_IDENTIFIER = 'name_is_alphabetically_sorted'
    PROJECTION_POINT_IDENTIFIER = 'projection_point'
    AZIMUTH_ANGLE_IDENTIFIER = 'azimuth_angle'
    BORDER_IS_PHYSICAL_IDENTIFIER = 'is_physical'
    BORDER_LINE_STRING_IDENTIFIER = 'geo_line_string'

    def __init__(
            self,
            area_column: str,
            border_identifier: str = None,
            source_area_identifier: str = None,
            target_area_identifier: str = None,
    ):
        """Initialize border naming conventions.

        Args:
            area_column: Name of the area column (e.g., 'country', 'bidding_zone')
            border_identifier: Custom name for border identifier column.
                Defaults to '{area_column}_border'
            source_area_identifier: Custom name for source area column.
                Defaults to '{area_column}_from'
            target_area_identifier: Custom name for target area column.
                Defaults to '{area_column}_to'

        Example:

            >>> # Standard naming
            >>> conventions = AreaBorderNamingConventions('country')
            >>> print(conventions.border_identifier)  # 'country_border'
            >>> 
            >>> # Custom naming
            >>> conventions = AreaBorderNamingConventions(
            ...     'bidding_zone',
            ...     border_identifier='interconnection',
            ...     source_area_identifier='origin_zone'
            ... )
        """
        self.area_column = area_column
        self.border_identifier = border_identifier or self._default_border_identifier()
        self.source_area_identifier = source_area_identifier or self._default_source_area_identifier()
        self.target_area_identifier = target_area_identifier or self._default_target_area_identifier()

    def _default_border_identifier(self) -> str:
        return f'{self.area_column}_border'

    def _default_source_area_identifier(self) -> str:
        return f'{self.SOURCE_AREA_IDENTIFIER_PREFIX}{self.area_column}{self.SOURCE_AREA_IDENTIFIER_SUFFIX}'

    def _default_target_area_identifier(self) -> str:
        return f'{self.TARGET_AREA_IDENTIFIER_PREFIX}{self.area_column}{self.TARGET_AREA_IDENTIFIER_SUFFIX}'

    def get_area_border_name(self, area_from: str, area_to: str) -> str:
        """Generate standardized border name from source and target areas.

        Args:
            area_from: Source area identifier (e.g., 'DE', 'FR_North')
            area_to: Target area identifier (e.g., 'FR', 'DE_South')

        Returns:
            str: Formatted border name using the configured separator (e.g. 'DE - FR', 'FR_North - DE_South')

        Example:

            >>> conventions = AreaBorderNamingConventions('country')
            >>> border_name = conventions.get_area_border_name('DE', 'FR')
            >>> print(border_name)  # 'DE - FR'
        """
        return f'{area_from}{self.JOIN_AREA_NAMES_BY}{area_to}'

    def decompose_area_border_name_to_areas(self, border_name: str) -> Tuple[str, str]:
        """Extract source and target area names from border identifier.

        Args:
            border_name: Border name in standard format (e.g., 'DE - FR')

        Returns:
            Tuple[str, str]: Source and target area names

        Raises:
            ValueError: If border_name doesn't contain the expected separator

        Example:

            >>> conventions = AreaBorderNamingConventions('country')
            >>> area_from, area_to = conventions.decompose_area_border_name_to_areas('DE - FR')
            >>> print(f"From: {area_from}, To: {area_to}")  # From: DE, To: FR
        """
        area_from, area_to = border_name.split(self.JOIN_AREA_NAMES_BY)
        return area_from, area_to

    def get_opposite_area_border_name(self, border_name: str) -> str:
        """Generate the opposite direction border name.

        Args:
            border_name: Original border name (e.g., 'DE - FR')

        Returns:
            str: Opposite direction border name (e.g., 'FR - DE')

        Example:

            >>> conventions = AreaBorderNamingConventions('country')
            >>> opposite = conventions.get_opposite_area_border_name('DE - FR')
            >>> print(opposite)  # 'FR - DE'

        Energy Domain Context:
            Energy flows and capacities are often directional, requiring
            tracking of both A→B and B→A relationships for comprehensive
            border analysis.
        """
        area_from, area_to = self.decompose_area_border_name_to_areas(border_name)
        return self.get_area_border_name(area_to, area_from)

    def get_alphabetically_sorted_border(self, border_name: str) -> str:
        """Generate alphabetically sorted canonical border name.

        Creates a canonical representation where area names are sorted
        alphabetically, useful for identifying unique borders regardless
        of direction specification, or for matching borders of opposite direction.

        Args:
            border_name: Border name in any direction (e.g., 'FR - DE' or 'DE - FR')

        Returns:
            str: Alphabetically sorted border name (e.g., 'DE - FR')

        Example:

            >>> conventions = AreaBorderNamingConventions('country')
            >>> sorted_border = conventions.get_alphabetically_sorted_border('FR - DE')
            >>> print(sorted_border)  # 'DE - FR'

        Use Case:
            Canonical naming is essential for border deduplication and
            consistent reference in energy system databases and analysis.
        """
        area_from, area_to = self.decompose_area_border_name_to_areas(border_name)
        return self.get_area_border_name(*list(sorted([area_from, area_to])))

__init__

__init__(area_column: str, border_identifier: str = None, source_area_identifier: str = None, target_area_identifier: str = None)

Initialize border naming conventions.

Parameters:

Name Type Description Default
area_column str

Name of the area column (e.g., 'country', 'bidding_zone')

required
border_identifier str

Custom name for border identifier column. Defaults to '{area_column}_border'

None
source_area_identifier str

Custom name for source area column. Defaults to '{area_column}_from'

None
target_area_identifier str

Custom name for target area column. Defaults to '{area_column}_to'

None

Example:

>>> # Standard naming
>>> conventions = AreaBorderNamingConventions('country')
>>> print(conventions.border_identifier)  # 'country_border'
>>> 
>>> # Custom naming
>>> conventions = AreaBorderNamingConventions(
...     'bidding_zone',
...     border_identifier='interconnection',
...     source_area_identifier='origin_zone'
... )
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_generator.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def __init__(
        self,
        area_column: str,
        border_identifier: str = None,
        source_area_identifier: str = None,
        target_area_identifier: str = None,
):
    """Initialize border naming conventions.

    Args:
        area_column: Name of the area column (e.g., 'country', 'bidding_zone')
        border_identifier: Custom name for border identifier column.
            Defaults to '{area_column}_border'
        source_area_identifier: Custom name for source area column.
            Defaults to '{area_column}_from'
        target_area_identifier: Custom name for target area column.
            Defaults to '{area_column}_to'

    Example:

        >>> # Standard naming
        >>> conventions = AreaBorderNamingConventions('country')
        >>> print(conventions.border_identifier)  # 'country_border'
        >>> 
        >>> # Custom naming
        >>> conventions = AreaBorderNamingConventions(
        ...     'bidding_zone',
        ...     border_identifier='interconnection',
        ...     source_area_identifier='origin_zone'
        ... )
    """
    self.area_column = area_column
    self.border_identifier = border_identifier or self._default_border_identifier()
    self.source_area_identifier = source_area_identifier or self._default_source_area_identifier()
    self.target_area_identifier = target_area_identifier or self._default_target_area_identifier()

get_area_border_name

get_area_border_name(area_from: str, area_to: str) -> str

Generate standardized border name from source and target areas.

Parameters:

Name Type Description Default
area_from str

Source area identifier (e.g., 'DE', 'FR_North')

required
area_to str

Target area identifier (e.g., 'FR', 'DE_South')

required

Returns:

Name Type Description
str str

Formatted border name using the configured separator (e.g. 'DE - FR', 'FR_North - DE_South')

Example:

>>> conventions = AreaBorderNamingConventions('country')
>>> border_name = conventions.get_area_border_name('DE', 'FR')
>>> print(border_name)  # 'DE - FR'
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_generator.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def get_area_border_name(self, area_from: str, area_to: str) -> str:
    """Generate standardized border name from source and target areas.

    Args:
        area_from: Source area identifier (e.g., 'DE', 'FR_North')
        area_to: Target area identifier (e.g., 'FR', 'DE_South')

    Returns:
        str: Formatted border name using the configured separator (e.g. 'DE - FR', 'FR_North - DE_South')

    Example:

        >>> conventions = AreaBorderNamingConventions('country')
        >>> border_name = conventions.get_area_border_name('DE', 'FR')
        >>> print(border_name)  # 'DE - FR'
    """
    return f'{area_from}{self.JOIN_AREA_NAMES_BY}{area_to}'

decompose_area_border_name_to_areas

decompose_area_border_name_to_areas(border_name: str) -> Tuple[str, str]

Extract source and target area names from border identifier.

Parameters:

Name Type Description Default
border_name str

Border name in standard format (e.g., 'DE - FR')

required

Returns:

Type Description
Tuple[str, str]

Tuple[str, str]: Source and target area names

Raises:

Type Description
ValueError

If border_name doesn't contain the expected separator

Example:

>>> conventions = AreaBorderNamingConventions('country')
>>> area_from, area_to = conventions.decompose_area_border_name_to_areas('DE - FR')
>>> print(f"From: {area_from}, To: {area_to}")  # From: DE, To: FR
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_generator.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def decompose_area_border_name_to_areas(self, border_name: str) -> Tuple[str, str]:
    """Extract source and target area names from border identifier.

    Args:
        border_name: Border name in standard format (e.g., 'DE - FR')

    Returns:
        Tuple[str, str]: Source and target area names

    Raises:
        ValueError: If border_name doesn't contain the expected separator

    Example:

        >>> conventions = AreaBorderNamingConventions('country')
        >>> area_from, area_to = conventions.decompose_area_border_name_to_areas('DE - FR')
        >>> print(f"From: {area_from}, To: {area_to}")  # From: DE, To: FR
    """
    area_from, area_to = border_name.split(self.JOIN_AREA_NAMES_BY)
    return area_from, area_to

get_opposite_area_border_name

get_opposite_area_border_name(border_name: str) -> str

Generate the opposite direction border name.

Parameters:

Name Type Description Default
border_name str

Original border name (e.g., 'DE - FR')

required

Returns:

Name Type Description
str str

Opposite direction border name (e.g., 'FR - DE')

Example:

>>> conventions = AreaBorderNamingConventions('country')
>>> opposite = conventions.get_opposite_area_border_name('DE - FR')
>>> print(opposite)  # 'FR - DE'
Energy Domain Context

Energy flows and capacities are often directional, requiring tracking of both A→B and B→A relationships for comprehensive border analysis.

Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_generator.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def get_opposite_area_border_name(self, border_name: str) -> str:
    """Generate the opposite direction border name.

    Args:
        border_name: Original border name (e.g., 'DE - FR')

    Returns:
        str: Opposite direction border name (e.g., 'FR - DE')

    Example:

        >>> conventions = AreaBorderNamingConventions('country')
        >>> opposite = conventions.get_opposite_area_border_name('DE - FR')
        >>> print(opposite)  # 'FR - DE'

    Energy Domain Context:
        Energy flows and capacities are often directional, requiring
        tracking of both A→B and B→A relationships for comprehensive
        border analysis.
    """
    area_from, area_to = self.decompose_area_border_name_to_areas(border_name)
    return self.get_area_border_name(area_to, area_from)

get_alphabetically_sorted_border

get_alphabetically_sorted_border(border_name: str) -> str

Generate alphabetically sorted canonical border name.

Creates a canonical representation where area names are sorted alphabetically, useful for identifying unique borders regardless of direction specification, or for matching borders of opposite direction.

Parameters:

Name Type Description Default
border_name str

Border name in any direction (e.g., 'FR - DE' or 'DE - FR')

required

Returns:

Name Type Description
str str

Alphabetically sorted border name (e.g., 'DE - FR')

Example:

>>> conventions = AreaBorderNamingConventions('country')
>>> sorted_border = conventions.get_alphabetically_sorted_border('FR - DE')
>>> print(sorted_border)  # 'DE - FR'
Use Case

Canonical naming is essential for border deduplication and consistent reference in energy system databases and analysis.

Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_generator.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
def get_alphabetically_sorted_border(self, border_name: str) -> str:
    """Generate alphabetically sorted canonical border name.

    Creates a canonical representation where area names are sorted
    alphabetically, useful for identifying unique borders regardless
    of direction specification, or for matching borders of opposite direction.

    Args:
        border_name: Border name in any direction (e.g., 'FR - DE' or 'DE - FR')

    Returns:
        str: Alphabetically sorted border name (e.g., 'DE - FR')

    Example:

        >>> conventions = AreaBorderNamingConventions('country')
        >>> sorted_border = conventions.get_alphabetically_sorted_border('FR - DE')
        >>> print(sorted_border)  # 'DE - FR'

    Use Case:
        Canonical naming is essential for border deduplication and
        consistent reference in energy system databases and analysis.
    """
    area_from, area_to = self.decompose_area_border_name_to_areas(border_name)
    return self.get_area_border_name(*list(sorted([area_from, area_to])))

AreaBorderModelGenerator

Bases: AreaBorderNamingConventions

Generates comprehensive border models from energy system topology.

This class analyzes line connectivity and node-to-area mappings to automatically identify borders between energy system areas. It creates a comprehensive border_model_df with standardized naming, directional relationships, and integration points for geometric analysis.

The generator processes line topology data to identify cross-area connections. It supports bidirectional relationship tracking and provides network graph representations for connectivity analysis.

Key Features
  • Automatic border discovery from line topology
  • Bidirectional border relationship management
  • Standardized naming conventions with configurable patterns
  • Network graph generation for connectivity analysis
  • Integration with geometric border calculators
  • Support for different area granularities (countries, bidding zones, etc.)
MESQUAL Integration

Designed to work with MESQUAL's area accounting system, providing border modeling capabilities that integrate with flow calculators, capacity analyzers, and visualization tools.

Attributes:

Name Type Description
line_model_df DataFrame

Transmission line data with topology information

node_model_df DataFrame

Node data with area assignments

node_from_col str

Column name for line source nodes

node_to_col str

Column name for line target nodes

node_to_area_map dict

Mapping from nodes to their assigned areas

Example:

>>> # Create border model from transmission data
>>> generator = AreaBorderModelGenerator(
...     node_df, line_df, 'country', 'node_from', 'node_to'
... )
>>> border_model = generator.generate_area_border_model()
>>> print(f"Found {len(border_model)} directional borders")
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_generator.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
class AreaBorderModelGenerator(AreaBorderNamingConventions):
    """Generates comprehensive border models from energy system topology.

    This class analyzes line connectivity and node-to-area mappings
    to automatically identify borders between energy system areas. It creates
    a comprehensive border_model_df with standardized naming, directional relationships,
    and integration points for geometric analysis.

    The generator processes line topology data to identify cross-area connections.
    It supports bidirectional relationship tracking and provides network graph
    representations for connectivity analysis.

    Key Features:
        - Automatic border discovery from line topology
        - Bidirectional border relationship management
        - Standardized naming conventions with configurable patterns
        - Network graph generation for connectivity analysis
        - Integration with geometric border calculators
        - Support for different area granularities (countries, bidding zones, etc.)

    MESQUAL Integration:
        Designed to work with MESQUAL's area accounting system, providing
        border modeling capabilities that integrate with flow calculators,
        capacity analyzers, and visualization tools.

    Attributes:
        line_model_df (pd.DataFrame): Transmission line data with topology information
        node_model_df (pd.DataFrame): Node data with area assignments
        node_from_col (str): Column name for line source nodes
        node_to_col (str): Column name for line target nodes
        node_to_area_map (dict): Mapping from nodes to their assigned areas

    Example:

        >>> # Create border model from transmission data
        >>> generator = AreaBorderModelGenerator(
        ...     node_df, line_df, 'country', 'node_from', 'node_to'
        ... )
        >>> border_model = generator.generate_area_border_model()
        >>> print(f"Found {len(border_model)} directional borders")
    """

    def __init__(
        self, 
        node_model_df: pd.DataFrame,
        line_model_df: pd.DataFrame,
        area_column: str,
        node_from_col: str,
        node_to_col: str,
        border_identifier: str = None,
        source_area_identifier: str = None,
        target_area_identifier: str = None,
    ):
        """Initialize the area border model generator.

        Args:
            node_model_df: DataFrame containing node-level data with area assignments.
                Must contain area_column with area identifiers for each node.
            line_model_df: DataFrame containing transmission line topology data.
                Must contain node_from_col and node_to_col with node identifiers.
            area_column: Column name in node_model_df containing area assignments
                (e.g., 'country', 'bidding_zone', 'market_region')
            node_from_col: Column name in line_model_df for source node identifiers
            node_to_col: Column name in line_model_df for target node identifiers
            border_identifier: Custom border column name (optional)
            source_area_identifier: Custom source area column name (optional)
            target_area_identifier: Custom target area column name (optional)

        Raises:
            ValueError: If required columns are not found in input DataFrames

        Example:

            >>> generator = AreaBorderModelGenerator(
            ...     nodes_df=node_data,
            ...     lines_df=transmission_data,
            ...     area_column='bidding_zone',
            ...     node_from_col='bus_from',
            ...     node_to_col='bus_to'
            ... )
        """
        super().__init__(area_column, border_identifier, source_area_identifier, target_area_identifier)
        self.line_model_df = line_model_df
        self.node_model_df = node_model_df
        self.node_from_col = node_from_col
        self.node_to_col = node_to_col

        self._validate_inputs()
        self.node_to_area_map = self._create_node_to_area_map()

    def _validate_inputs(self):
        if self.area_column not in self.node_model_df.columns:
            raise ValueError(
                f"Area column '{self.area_column}' not found in node_model_df. "
                f"Available columns: {list(self.node_model_df.columns)}"
            )
        if self.node_from_col not in self.line_model_df.columns:
            raise ValueError(
                f"Source node column '{self.node_from_col}' not found in line_model_df. "
                f"Available columns: {list(self.line_model_df.columns)}"
            )
        if self.node_to_col not in self.line_model_df.columns:
            raise ValueError(
                f"Target node column '{self.node_to_col}' not found in line_model_df. "
                f"Available columns: {list(self.line_model_df.columns)}"
            )

    def _create_node_to_area_map(self) -> dict:
        """Create mapping from node identifiers to their assigned areas.

        Returns:
            dict: Mapping from node IDs to area assignments

        Note:
            Nodes with None or NaN area assignments are included in the mapping
            but will be filtered out during border identification.
        """
        return self.node_model_df[self.area_column].to_dict()

    def generate_area_border_model(self) -> pd.DataFrame:
        """Generate comprehensive border model with all relationship data.

        Analyzes transmission line topology to identify borders between areas,
        creating a comprehensive DataFrame with directional relationships,
        naming conventions, and reference data for further analysis.

        The generated model includes:
            - Border identifiers in both directions (A→B and B→A)
            - Source and target area columns
            - Opposite border references for bidirectional analysis
            - Alphabetically sorted canonical border names
            - Boolean indicators for alphabetical sorting

        Returns:
            pd.DataFrame: Comprehensive border model indexed by border identifiers.
                Returns empty DataFrame with proper column structure if no borders found.

        Example:

            >>> border_model = generator.generate_area_border_model()
            >>> print(border_model.columns)
            ['country_from', 'country_to', 'opposite_border', 'sorted_border', 'name_is_alphabetically_sorted']
            >>> 
            >>> # Access border relationships
            >>> for border_id, row in border_model.iterrows():
            ...     print(f"{border_id}: {row['country_from']} → {row['country_to']}")
        """
        borders = self._identify_borders()

        if not borders:
            return pd.DataFrame(
                columns=[
                    self.border_identifier,
                    self.source_area_identifier,
                    self.target_area_identifier,
                    self.OPPOSITE_BORDER_IDENTIFIER,
                    self.SORTED_BORDER_IDENTIFIER,
                    self.NAME_IS_ALPHABETICALLY_SORTED_IDENTIFIER,
                ]
            )

        border_data = []
        for area_from, area_to in borders:
            border_id = self.get_area_border_name(area_from, area_to)
            opposite_id = self.get_opposite_area_border_name(border_id)

            sorted_border = self.get_alphabetically_sorted_border(border_id)

            border_data.append({
                self.border_identifier: border_id,
                self.source_area_identifier: area_from,
                self.target_area_identifier: area_to,
                self.OPPOSITE_BORDER_IDENTIFIER: opposite_id,
                self.SORTED_BORDER_IDENTIFIER: sorted_border,
                self.NAME_IS_ALPHABETICALLY_SORTED_IDENTIFIER: sorted_border == border_id,
            })

        border_model_df = pd.DataFrame(border_data).set_index(self.border_identifier)

        return border_model_df

    def _identify_borders(self) -> set[tuple[str, str]]:
        """Identify borders from line topology.

        Analyzes line connectivity to find areas that are connected by
        lines, creating bidirectional border relationships.

        Returns:
            set: Set of (area_from, area_to) tuples representing directional borders.
                Includes both directions for each physical connection.

        Note:
            - Lines connecting nodes within the same area are ignored
            - Lines with nodes having None/NaN area assignments are ignored
            - Both directions (A→B and B→A) are included for each connection
        """
        borders = set()

        for _, line in self.line_model_df.iterrows():
            node_from = line[self.node_from_col]
            node_to = line[self.node_to_col]

            area_from = self.node_to_area_map.get(node_from)
            area_to = self.node_to_area_map.get(node_to)

            if area_from and area_to and area_from != area_to:
                borders.add((area_from, area_to))
                borders.add((area_to, area_from))

        return borders

    def _get_lines_for_border(self, area_from: str, area_to: str) -> list[str]:
        """Get all lines that cross a specific directional border.

        Args:
            area_from: Source area identifier
            area_to: Target area identifier

        Returns:
            list[str]: List of line identifiers that connect the specified areas
                in the given direction

        Example:

            >>> lines = generator._get_lines_for_border('DE', 'FR')
            >>> print(f"Lines from DE to FR: {lines}")
        """
        lines = []

        for line_id, line in self.line_model_df.iterrows():
            node_from = line[self.node_from_col]
            node_to = line[self.node_to_col]

            node_area_from = self.node_to_area_map.get(node_from)
            node_area_to = self.node_to_area_map.get(node_to)

            if node_area_from == area_from and node_area_to == area_to:
                lines.append(line_id)

        return lines

    def get_area_graph(self) -> nx.Graph:
        """Generate NetworkX graph representation of area connectivity.

        Creates an undirected graph where nodes represent areas and edges
        represent borders. This is useful for network analysis, path finding,
        and connectivity studies in multi-area energy systems.

        Returns:
            nx.Graph: Undirected graph with areas as nodes and borders as edges.
                Graph may contain multiple disconnected components if areas
                are not fully interconnected.

        Example:

            >>> graph = generator.get_area_graph()
            >>> print(f"Areas: {list(graph.nodes())}")
            >>> print(f"Borders: {list(graph.edges())}")
            >>> 
            >>> # Check connectivity
            >>> connected = nx.is_connected(graph)
            >>> print(f"All areas connected: {connected}")
        """
        graph = nx.Graph()
        borders = self._identify_borders()

        for area_from, area_to in borders:
            if not graph.has_edge(area_from, area_to):
                graph.add_edge(area_from, area_to)

        return graph

    def enhance_with_geometry(
        self, 
        border_model_df: pd.DataFrame,
        area_geometry_calculator: AreaBorderGeometryCalculator
    ) -> pd.DataFrame:
        """Enhance border model with geometric properties for visualization.

        Integrates with AreaBorderGeometryCalculator to add geometric information
        to borders, including representative points, directional angles, and
        line geometries. This enables advanced visualization of energy system borders.

        Args:
            border_model_df: Border model DataFrame to enhance
            area_geometry_calculator: Configured geometry calculator with area
                polygon data for geometric computations

        Returns:
            pd.DataFrame: Enhanced border model with additional geometric columns:
                - projection_point: Point for label/arrow placement
                - azimuth_angle: Directional angle in degrees
                - is_physical: Boolean indicating if border is physical (touching areas)
                - geo_line_string: LineString geometry representing the border

        Example:

            >>> # Setup geometry calculator with area polygons
            >>> geo_calc = AreaBorderGeometryCalculator(area_polygons_gdf)
            >>> 
            >>> # Enhance border model
            >>> enhanced_borders = generator.enhance_with_geometry(border_model, geo_calc)
            >>> print(enhanced_borders.columns)  # Includes geometric properties

        Note:
            Geometric enhancement may fail for some borders due to missing
            area geometries or calculation errors. Such failures are logged
            as warnings without stopping the overall process.
        """
        enhanced_df = border_model_df.copy()

        for border_id, border in border_model_df.iterrows():
            area_from = border[self.source_area_identifier]
            area_to = border[self.target_area_identifier]

            try:
                geometry_info = area_geometry_calculator.calculate_border_geometry(
                    area_from, area_to
                )

                enhanced_df.loc[border_id, self.PROJECTION_POINT_IDENTIFIER] = geometry_info[area_geometry_calculator.PROJECTION_POINT_IDENTIFIER]
                enhanced_df.loc[border_id, self.AZIMUTH_ANGLE_IDENTIFIER] = geometry_info[area_geometry_calculator.AZIMUTH_ANGLE_IDENTIFIER]
                enhanced_df.loc[border_id, self.BORDER_IS_PHYSICAL_IDENTIFIER] = geometry_info[area_geometry_calculator.BORDER_IS_PHYSICAL_IDENTIFIER]
                enhanced_df.loc[border_id, self.BORDER_LINE_STRING_IDENTIFIER] = geometry_info[area_geometry_calculator.BORDER_LINE_STRING_IDENTIFIER]

            except Exception as e:
                print(f"Warning: Could not calculate geometry for border {border_id} "
                      f"({area_from}{area_to}): {e}")

        return enhanced_df

__init__

__init__(node_model_df: DataFrame, line_model_df: DataFrame, area_column: str, node_from_col: str, node_to_col: str, border_identifier: str = None, source_area_identifier: str = None, target_area_identifier: str = None)

Initialize the area border model generator.

Parameters:

Name Type Description Default
node_model_df DataFrame

DataFrame containing node-level data with area assignments. Must contain area_column with area identifiers for each node.

required
line_model_df DataFrame

DataFrame containing transmission line topology data. Must contain node_from_col and node_to_col with node identifiers.

required
area_column str

Column name in node_model_df containing area assignments (e.g., 'country', 'bidding_zone', 'market_region')

required
node_from_col str

Column name in line_model_df for source node identifiers

required
node_to_col str

Column name in line_model_df for target node identifiers

required
border_identifier str

Custom border column name (optional)

None
source_area_identifier str

Custom source area column name (optional)

None
target_area_identifier str

Custom target area column name (optional)

None

Raises:

Type Description
ValueError

If required columns are not found in input DataFrames

Example:

>>> generator = AreaBorderModelGenerator(
...     nodes_df=node_data,
...     lines_df=transmission_data,
...     area_column='bidding_zone',
...     node_from_col='bus_from',
...     node_to_col='bus_to'
... )
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_generator.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
def __init__(
    self, 
    node_model_df: pd.DataFrame,
    line_model_df: pd.DataFrame,
    area_column: str,
    node_from_col: str,
    node_to_col: str,
    border_identifier: str = None,
    source_area_identifier: str = None,
    target_area_identifier: str = None,
):
    """Initialize the area border model generator.

    Args:
        node_model_df: DataFrame containing node-level data with area assignments.
            Must contain area_column with area identifiers for each node.
        line_model_df: DataFrame containing transmission line topology data.
            Must contain node_from_col and node_to_col with node identifiers.
        area_column: Column name in node_model_df containing area assignments
            (e.g., 'country', 'bidding_zone', 'market_region')
        node_from_col: Column name in line_model_df for source node identifiers
        node_to_col: Column name in line_model_df for target node identifiers
        border_identifier: Custom border column name (optional)
        source_area_identifier: Custom source area column name (optional)
        target_area_identifier: Custom target area column name (optional)

    Raises:
        ValueError: If required columns are not found in input DataFrames

    Example:

        >>> generator = AreaBorderModelGenerator(
        ...     nodes_df=node_data,
        ...     lines_df=transmission_data,
        ...     area_column='bidding_zone',
        ...     node_from_col='bus_from',
        ...     node_to_col='bus_to'
        ... )
    """
    super().__init__(area_column, border_identifier, source_area_identifier, target_area_identifier)
    self.line_model_df = line_model_df
    self.node_model_df = node_model_df
    self.node_from_col = node_from_col
    self.node_to_col = node_to_col

    self._validate_inputs()
    self.node_to_area_map = self._create_node_to_area_map()

generate_area_border_model

generate_area_border_model() -> DataFrame

Generate comprehensive border model with all relationship data.

Analyzes transmission line topology to identify borders between areas, creating a comprehensive DataFrame with directional relationships, naming conventions, and reference data for further analysis.

The generated model includes
  • Border identifiers in both directions (A→B and B→A)
  • Source and target area columns
  • Opposite border references for bidirectional analysis
  • Alphabetically sorted canonical border names
  • Boolean indicators for alphabetical sorting

Returns:

Type Description
DataFrame

pd.DataFrame: Comprehensive border model indexed by border identifiers. Returns empty DataFrame with proper column structure if no borders found.

Example:

>>> border_model = generator.generate_area_border_model()
>>> print(border_model.columns)
['country_from', 'country_to', 'opposite_border', 'sorted_border', 'name_is_alphabetically_sorted']
>>> 
>>> # Access border relationships
>>> for border_id, row in border_model.iterrows():
...     print(f"{border_id}: {row['country_from']} → {row['country_to']}")
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_generator.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
def generate_area_border_model(self) -> pd.DataFrame:
    """Generate comprehensive border model with all relationship data.

    Analyzes transmission line topology to identify borders between areas,
    creating a comprehensive DataFrame with directional relationships,
    naming conventions, and reference data for further analysis.

    The generated model includes:
        - Border identifiers in both directions (A→B and B→A)
        - Source and target area columns
        - Opposite border references for bidirectional analysis
        - Alphabetically sorted canonical border names
        - Boolean indicators for alphabetical sorting

    Returns:
        pd.DataFrame: Comprehensive border model indexed by border identifiers.
            Returns empty DataFrame with proper column structure if no borders found.

    Example:

        >>> border_model = generator.generate_area_border_model()
        >>> print(border_model.columns)
        ['country_from', 'country_to', 'opposite_border', 'sorted_border', 'name_is_alphabetically_sorted']
        >>> 
        >>> # Access border relationships
        >>> for border_id, row in border_model.iterrows():
        ...     print(f"{border_id}: {row['country_from']} → {row['country_to']}")
    """
    borders = self._identify_borders()

    if not borders:
        return pd.DataFrame(
            columns=[
                self.border_identifier,
                self.source_area_identifier,
                self.target_area_identifier,
                self.OPPOSITE_BORDER_IDENTIFIER,
                self.SORTED_BORDER_IDENTIFIER,
                self.NAME_IS_ALPHABETICALLY_SORTED_IDENTIFIER,
            ]
        )

    border_data = []
    for area_from, area_to in borders:
        border_id = self.get_area_border_name(area_from, area_to)
        opposite_id = self.get_opposite_area_border_name(border_id)

        sorted_border = self.get_alphabetically_sorted_border(border_id)

        border_data.append({
            self.border_identifier: border_id,
            self.source_area_identifier: area_from,
            self.target_area_identifier: area_to,
            self.OPPOSITE_BORDER_IDENTIFIER: opposite_id,
            self.SORTED_BORDER_IDENTIFIER: sorted_border,
            self.NAME_IS_ALPHABETICALLY_SORTED_IDENTIFIER: sorted_border == border_id,
        })

    border_model_df = pd.DataFrame(border_data).set_index(self.border_identifier)

    return border_model_df

get_area_graph

get_area_graph() -> Graph

Generate NetworkX graph representation of area connectivity.

Creates an undirected graph where nodes represent areas and edges represent borders. This is useful for network analysis, path finding, and connectivity studies in multi-area energy systems.

Returns:

Type Description
Graph

nx.Graph: Undirected graph with areas as nodes and borders as edges. Graph may contain multiple disconnected components if areas are not fully interconnected.

Example:

>>> graph = generator.get_area_graph()
>>> print(f"Areas: {list(graph.nodes())}")
>>> print(f"Borders: {list(graph.edges())}")
>>> 
>>> # Check connectivity
>>> connected = nx.is_connected(graph)
>>> print(f"All areas connected: {connected}")
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_generator.py
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
def get_area_graph(self) -> nx.Graph:
    """Generate NetworkX graph representation of area connectivity.

    Creates an undirected graph where nodes represent areas and edges
    represent borders. This is useful for network analysis, path finding,
    and connectivity studies in multi-area energy systems.

    Returns:
        nx.Graph: Undirected graph with areas as nodes and borders as edges.
            Graph may contain multiple disconnected components if areas
            are not fully interconnected.

    Example:

        >>> graph = generator.get_area_graph()
        >>> print(f"Areas: {list(graph.nodes())}")
        >>> print(f"Borders: {list(graph.edges())}")
        >>> 
        >>> # Check connectivity
        >>> connected = nx.is_connected(graph)
        >>> print(f"All areas connected: {connected}")
    """
    graph = nx.Graph()
    borders = self._identify_borders()

    for area_from, area_to in borders:
        if not graph.has_edge(area_from, area_to):
            graph.add_edge(area_from, area_to)

    return graph

enhance_with_geometry

enhance_with_geometry(border_model_df: DataFrame, area_geometry_calculator: AreaBorderGeometryCalculator) -> DataFrame

Enhance border model with geometric properties for visualization.

Integrates with AreaBorderGeometryCalculator to add geometric information to borders, including representative points, directional angles, and line geometries. This enables advanced visualization of energy system borders.

Parameters:

Name Type Description Default
border_model_df DataFrame

Border model DataFrame to enhance

required
area_geometry_calculator AreaBorderGeometryCalculator

Configured geometry calculator with area polygon data for geometric computations

required

Returns:

Type Description
DataFrame

pd.DataFrame: Enhanced border model with additional geometric columns: - projection_point: Point for label/arrow placement - azimuth_angle: Directional angle in degrees - is_physical: Boolean indicating if border is physical (touching areas) - geo_line_string: LineString geometry representing the border

Example:

>>> # Setup geometry calculator with area polygons
>>> geo_calc = AreaBorderGeometryCalculator(area_polygons_gdf)
>>> 
>>> # Enhance border model
>>> enhanced_borders = generator.enhance_with_geometry(border_model, geo_calc)
>>> print(enhanced_borders.columns)  # Includes geometric properties
Note

Geometric enhancement may fail for some borders due to missing area geometries or calculation errors. Such failures are logged as warnings without stopping the overall process.

Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_generator.py
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
def enhance_with_geometry(
    self, 
    border_model_df: pd.DataFrame,
    area_geometry_calculator: AreaBorderGeometryCalculator
) -> pd.DataFrame:
    """Enhance border model with geometric properties for visualization.

    Integrates with AreaBorderGeometryCalculator to add geometric information
    to borders, including representative points, directional angles, and
    line geometries. This enables advanced visualization of energy system borders.

    Args:
        border_model_df: Border model DataFrame to enhance
        area_geometry_calculator: Configured geometry calculator with area
            polygon data for geometric computations

    Returns:
        pd.DataFrame: Enhanced border model with additional geometric columns:
            - projection_point: Point for label/arrow placement
            - azimuth_angle: Directional angle in degrees
            - is_physical: Boolean indicating if border is physical (touching areas)
            - geo_line_string: LineString geometry representing the border

    Example:

        >>> # Setup geometry calculator with area polygons
        >>> geo_calc = AreaBorderGeometryCalculator(area_polygons_gdf)
        >>> 
        >>> # Enhance border model
        >>> enhanced_borders = generator.enhance_with_geometry(border_model, geo_calc)
        >>> print(enhanced_borders.columns)  # Includes geometric properties

    Note:
        Geometric enhancement may fail for some borders due to missing
        area geometries or calculation errors. Such failures are logged
        as warnings without stopping the overall process.
    """
    enhanced_df = border_model_df.copy()

    for border_id, border in border_model_df.iterrows():
        area_from = border[self.source_area_identifier]
        area_to = border[self.target_area_identifier]

        try:
            geometry_info = area_geometry_calculator.calculate_border_geometry(
                area_from, area_to
            )

            enhanced_df.loc[border_id, self.PROJECTION_POINT_IDENTIFIER] = geometry_info[area_geometry_calculator.PROJECTION_POINT_IDENTIFIER]
            enhanced_df.loc[border_id, self.AZIMUTH_ANGLE_IDENTIFIER] = geometry_info[area_geometry_calculator.AZIMUTH_ANGLE_IDENTIFIER]
            enhanced_df.loc[border_id, self.BORDER_IS_PHYSICAL_IDENTIFIER] = geometry_info[area_geometry_calculator.BORDER_IS_PHYSICAL_IDENTIFIER]
            enhanced_df.loc[border_id, self.BORDER_LINE_STRING_IDENTIFIER] = geometry_info[area_geometry_calculator.BORDER_LINE_STRING_IDENTIFIER]

        except Exception as e:
            print(f"Warning: Could not calculate geometry for border {border_id} "
                  f"({area_from}{area_to}): {e}")

    return enhanced_df

AreaBorderGeometryCalculator

Bases: GeoModelGeneratorBase

Advanced geometric calculator for energy system area border analysis.

This class provides sophisticated geometric calculations for borders between energy system areas, handling both physical borders (adjacent areas sharing geographic boundaries) and logical borders (non-adjacent areas requiring connection paths, e.g. through the sea). It's specifically designed to generate properties for energy market cross-border visualizations.

The calculator combines multiple geometric algorithms: - Physical border extraction using geometric intersection - Logical geo-line-border path finding with obstacle avoidance - Representative point computation using pole of inaccessibility for label placements on maps - Azimuth angle calculation for flow icon (arrow) visualization - Geometric validation and optimization

Key Features
  • Automatic detection of physical vs logical borders
  • Optimal path finding for non-crossing connections
  • Representative point calculation for label placement
  • Directional angle computation for arrow orientation
  • Performance optimization with geometric caching
  • Integration with MESQUAL area accounting workflows
Energy Domain Applications
  • Visualization of cross-border (cross-country, cross-biddingzone, cross-macroregion) variables (flows, spreads, capacities, ...)

Attributes:

Name Type Description
area_model_gdf GeoDataFrame

GeoDataFrame with area polygon geometries

non_crossing_path_finder NonCrossingPathFinder

Path optimization engine

_centroid_cache dict

Cached representative points for performance

_line_cache dict

Cached border lines for repeated calculations

Example:

>>> import geopandas as gpd
>>> from shapely.geometry import box
>>> 
>>> # Setup area geometries
>>> areas = gpd.GeoDataFrame({
...     'geometry': [box(0, 0, 1, 1), box(2, 0, 3, 1)]  # Two separate areas
... }, index=['Area_A', 'Area_B'])
>>> 
>>> # Calculate border geometry
>>> calculator = AreaBorderGeometryCalculator(areas)
>>> border_info = calculator.calculate_border_geometry('Area_A', 'Area_B')
>>> print(f"Border type: {'Physical' if border_info['is_physical'] else 'Logical'}")
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_geometry_calculator.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
class AreaBorderGeometryCalculator(GeoModelGeneratorBase):
    """Advanced geometric calculator for energy system area border analysis.

    This class provides sophisticated geometric calculations for borders between
    energy system areas, handling both physical borders (adjacent areas sharing
    geographic boundaries) and logical borders (non-adjacent areas requiring
    connection paths, e.g. through the sea). It's specifically designed to generate
    properties for energy market cross-border visualizations.

    The calculator combines multiple geometric algorithms:
    - Physical border extraction using geometric intersection
    - Logical geo-line-border path finding with obstacle avoidance
    - Representative point computation using pole of inaccessibility for label placements on maps
    - Azimuth angle calculation for flow icon (arrow) visualization
    - Geometric validation and optimization

    Key Features:
        - Automatic detection of physical vs logical borders
        - Optimal path finding for non-crossing connections
        - Representative point calculation for label placement
        - Directional angle computation for arrow orientation
        - Performance optimization with geometric caching
        - Integration with MESQUAL area accounting workflows

    Energy Domain Applications:
        - Visualization of cross-border (cross-country, cross-biddingzone, cross-macroregion) variables (flows, spreads, capacities, ...)

    Attributes:
        area_model_gdf (gpd.GeoDataFrame): GeoDataFrame with area polygon geometries
        non_crossing_path_finder (NonCrossingPathFinder): Path optimization engine
        _centroid_cache (dict): Cached representative points for performance
        _line_cache (dict): Cached border lines for repeated calculations

    Example:

        >>> import geopandas as gpd
        >>> from shapely.geometry import box
        >>> 
        >>> # Setup area geometries
        >>> areas = gpd.GeoDataFrame({
        ...     'geometry': [box(0, 0, 1, 1), box(2, 0, 3, 1)]  # Two separate areas
        ... }, index=['Area_A', 'Area_B'])
        >>> 
        >>> # Calculate border geometry
        >>> calculator = AreaBorderGeometryCalculator(areas)
        >>> border_info = calculator.calculate_border_geometry('Area_A', 'Area_B')
        >>> print(f"Border type: {'Physical' if border_info['is_physical'] else 'Logical'}")
    """

    PROJECTION_POINT_IDENTIFIER = 'projection_point'
    AZIMUTH_ANGLE_IDENTIFIER = 'azimuth_angle'
    BORDER_IS_PHYSICAL_IDENTIFIER = 'is_physical'
    BORDER_LINE_STRING_IDENTIFIER = 'geo_line_string'

    def __init__(self, area_model_gdf: gpd.GeoDataFrame, non_crossing_path_finder: 'NonCrossingPathFinder' = None):
        """Initialize the border geometry calculator.

        Args:
            area_model_gdf: GeoDataFrame containing area geometries with polygon
                boundaries. Index should contain area identifiers (e.g., country codes,
                bidding zone names). Must contain valid polygon geometries in 'geometry' column.
            non_crossing_path_finder: Optional custom path finder for logical borders.
                If None, creates default NonCrossingPathFinder with standard parameters.

        Raises:
            ValueError: If geometries are invalid or area_model_gdf lacks required structure

        Example:

            >>> areas_gdf = gpd.read_file('countries.geojson').set_index('ISO_A2')
            >>> calculator = AreaBorderGeometryCalculator(areas_gdf)
            >>> 
            >>> # Custom path finder for specific requirements
            >>> custom_finder = NonCrossingPathFinder(num_points=200, min_clearance=10000)
            >>> calculator = AreaBorderGeometryCalculator(areas_gdf, custom_finder)

        Note:
            Invalid geometries are automatically cleaned using buffer(0) operation.
            Large area datasets benefit from using projected coordinate systems
            for accurate geometric calculations.
        """
        self.area_model_gdf = area_model_gdf
        self.non_crossing_path_finder = non_crossing_path_finder or NonCrossingPathFinder()
        self._validate_geometries()

        self._centroid_cache: dict[str, Point] = {}
        self._line_cache: dict[Tuple[str, str], LineString] = {}

    def _validate_geometries(self):
        """Validate and clean area geometries for reliable calculations.

        Applies buffer(0) operation to fix invalid geometries (self-intersections,
        unclosed rings, etc.) that could cause calculation failures. This is
        particularly important for real-world geographic data that may have
        topology issues.

        Note:
            The buffer(0) operation is a common technique for fixing invalid
            polygon geometries without changing their fundamental shape.
        """
        self.area_model_gdf['geometry'] = self.area_model_gdf['geometry'].apply(
            lambda geom: geom if geom.is_valid else geom.buffer(0)
        )

    def calculate_border_geometry(
        self, 
        area_from: str, 
        area_to: str
    ) -> dict[str, Union[Point, float, LineString, bool]]:
        """Calculate comprehensive geometric properties for an area border.

        This is the main interface method that computes all geometric properties
        needed for border visualization and analysis. It automatically detects
        whether areas are physically adjacent or logically connected and applies
        appropriate geometric algorithms.

        Processing Logic:
            1. Detect if areas share physical boundary (touching/intersecting)
            2. For physical borders: extract shared boundary line
            3. For logical borders: compute optimal connection path
            4. Calculate representative point for label/arrow placement
            5. Compute azimuth angle for arrow icon visualization

        Args:
            area_from: Source area identifier (must exist in area_model_gdf index)
            area_to: Target area identifier (must exist in area_model_gdf index)

        Returns:
            dict: Comprehensive border geometry information containing:
                - 'projection_point' (Point): Optimal point for label/arrow placement
                - 'azimuth_angle' (float): Directional angle in degrees (0-360)
                - 'geo_line_string' (LineString): Border line geometry
                - 'is_physical' (bool): True for touching areas, False for logical borders

        Raises:
            KeyError: If area_from or area_to not found in area_model_gdf
            ValueError: If geometric calculations fail

        Example:

            >>> border_info = calculator.calculate_border_geometry('DE', 'FR')
            >>> 
            >>> # Use for visualization
            >>> point = border_info['projection_point']
            >>> angle = border_info['azimuth_angle']
            >>> is_physical = border_info['is_physical']
            >>> 
            >>> print(f"Border DE→FR: {point} at {angle}° ({'physical' if is_physical else 'logical'})")
        """
        midpoint, angle = self.get_area_border_midpoint_and_angle(area_from, area_to)

        if self.areas_touch(area_from, area_to) or self.areas_intersect(area_from, area_to):
            geom_from = self.get_area_geometry(area_from)
            geom_to = self.get_area_geometry(area_to)
            border_line = self._get_continuous_border_line(geom_from, geom_to)
            is_physical = True
        else:
            border_line = self.get_straight_line_between_areas(area_from, area_to)
            is_physical = False

        return {
            self.PROJECTION_POINT_IDENTIFIER: midpoint,
            self.AZIMUTH_ANGLE_IDENTIFIER: angle,
            self.BORDER_LINE_STRING_IDENTIFIER: border_line,
            self.BORDER_IS_PHYSICAL_IDENTIFIER: is_physical
        }

    def areas_touch(self, area_from: str, area_to: str) -> bool:
        """Check if two areas share a common physical (geographic) border.

        Uses Shapely's touches() method to determine if area boundaries
        intersect without overlapping. This is the standard definition
        of physical adjacency for energy market regions.

        Args:
            area_from: Source area identifier
            area_to: Target area identifier

        Returns:
            bool: True if areas share a common boundary, False otherwise

        Example:

            >>> touching = calculator.areas_touch('DE', 'FR')  # True for neighboring countries
            >>> separated = calculator.areas_touch('DE', 'GB')  # False for non-adjacent countries
        """
        geom_from = self.get_area_geometry(area_from)
        geom_to = self.get_area_geometry(area_to)
        return geom_from.touches(geom_to)

    def areas_intersect(self, area_from: str, area_to: str) -> bool:
        """Check if two areas have any geometric intersection.

        Uses Shapely's intersects() method to check for any form of geometric
        intersection, including touching, overlapping, or containment. This is
        broader than the touches() check and handles edge cases in geographic data.

        Args:
            area_from: Source area identifier
            area_to: Target area identifier

        Returns:
            bool: True if areas have any geometric intersection, False otherwise

        Note:
            This method is used as a fallback for areas_touch() to handle
            geographic data with small overlaps or slight topology inconsistencies
            that are common in real-world boundary datasets.
        """
        geom_from = self.get_area_geometry(area_from)
        geom_to = self.get_area_geometry(area_to)
        return geom_from.intersects(geom_to)

    def get_area_border_midpoint_and_angle(
        self, 
        area_from: str, 
        area_to: str
    ) -> tuple[Point, float]:
        """Calculate representative point and directional angle for border.

        Computes the optimal point for placing directional indicators (arrows,
        labels) and the corresponding angle for proper orientation. The algorithm
        adapts to both physical and logical borders to ensure optimal placement.

        For Physical Borders:
            - Uses midpoint of shared boundary line
            - Angle is perpendicular to boundary, pointing toward target area

        For Logical Borders:
            - Uses midpoint of optimal connection line
            - Angle follows connection direction from source to target

        Args:
            area_from: Source area identifier
            area_to: Target area identifier

        Returns:
            tuple[Point, float]: Representative point and directional angle in degrees.
                Angle range: 0-360 degrees, where 0° is North, 90° is East.

        Example:

            >>> point, angle = calculator.get_area_border_midpoint_and_angle('DE', 'FR')
            >>> print(f"Place arrow at {point} oriented at {angle}° for DE→FR flow")
        """
        geom_from = self.get_area_geometry(area_from)
        geom_to = self.get_area_geometry(area_to)

        if self.areas_touch(area_from, area_to) or self.areas_intersect(area_from, area_to):
            midpoint, angle = self._get_midpoint_and_angle_for_touching_areas(geom_from, geom_to)
        else:
            straight_line = self.get_straight_line_between_areas(area_from, area_to)
            midpoint, angle = self._get_midpoint_and_angle_from_line(straight_line)

        # Ensure angle points from area_from to area_to
        if not self._angle_points_to_target(geom_from, geom_to, midpoint, angle):
            angle = (angle + 180) % 360

        return midpoint, angle

    def get_area_geometry(self, area: str) -> Union[Polygon, MultiPolygon]:
        """Retrieve and validate geometry for a specified area.

        Args:
            area: Area identifier that must exist in area_model_gdf index

        Returns:
            Union[Polygon, MultiPolygon]: Cleaned geometry with buffer(0) applied
                to ensure validity for geometric operations

        Raises:
            KeyError: If area is not found in area_model_gdf

        Note:
            The buffer(0) operation ensures geometric validity for complex
            calculations, which is essential for reliable border analysis.
        """
        return self.area_model_gdf.loc[area].geometry.buffer(0)

    def get_straight_line_between_areas(self, area_from: str, area_to: str) -> LineString:
        """Compute optimal straight-line connection between non-adjacent areas.

        Creates a direct line connection between area boundaries, with intelligent
        path optimization to avoid crossing other areas when possible. This is
        particularly important for non-physical borders.

        Algorithm:
            1. Find representative points for both areas
            2. Create line connecting area centroids
            3. Calculate intersection points with area boundaries  
            4. Check for conflicts with other areas
            5. Apply non-crossing path optimization if needed

        Args:
            area_from: Source area identifier
            area_to: Target area identifier

        Returns:
            LineString: Optimized connection line between area boundaries.
                Line endpoints touch the area boundaries, not the centroids.

        Raises:
            ValueError: If areas are touching (should use physical border instead)

        Example:

            >>> # Connect non-adjacent areas (e.g., Germany to UK)
            >>> line = calculator.get_straight_line_between_areas('DE', 'GB')
            >>> print(f"Connection length: {line.length:.0f} km")

        Performance Note:
            Results are cached to improve performance for repeated calculations.
            Path optimization can be computationally intensive for complex geometries.
        """
        key = tuple(sorted((area_from, area_to)))
        if key in self._line_cache:
            return self._line_cache[key]

        if self.areas_touch(area_from, area_to):
            raise ValueError(f"Areas {area_from} and {area_to} touch - use border line instead")

        geom_from = self._get_largest_polygon(self.get_area_geometry(area_from))
        geom_to = self._get_largest_polygon(self.get_area_geometry(area_to))

        centroid_from = self.get_representative_area_point(geom_from)
        centroid_to = self.get_representative_area_point(geom_to)

        line_full = LineString([centroid_from, centroid_to])

        # Find intersection points with area boundaries
        intersection_from = self._get_boundary_intersection(geom_from, line_full, centroid_to)
        intersection_to = self._get_boundary_intersection(geom_to, line_full, centroid_from)

        straight_line = LineString([intersection_from, intersection_to])

        # Check if line crosses other areas
        if self._line_crosses_other_areas(straight_line, area_from, area_to):
            # Try to find alternative path
            better_line = self._find_non_crossing_line(area_from, area_to)
            if better_line is not None:
                straight_line = better_line

        self._line_cache[key] = straight_line
        return straight_line

    def _get_midpoint_and_angle_for_touching_areas(
        self,
        geom_from: Union[Polygon, MultiPolygon],
        geom_to: Union[Polygon, MultiPolygon]
    ) -> tuple[Point, float]:
        """Calculate midpoint and angle for physically adjacent areas.

        For areas that share a physical boundary, this method extracts the
        shared border line and computes the optimal point and angle for
        directional indicators.

        Args:
            geom_from: Source area geometry
            geom_to: Target area geometry

        Returns:
            tuple[Point, float]: Midpoint of shared border and perpendicular angle
                pointing from source toward target area

        Algorithm:
            1. Extract continuous border line from geometric intersection
            2. Find midpoint along border line (50% interpolation)
            3. Calculate border bearing and perpendicular angle
            4. Ensure angle points from source to target area
        """
        border_line = self._get_continuous_border_line(geom_from, geom_to)
        midpoint = border_line.interpolate(0.5, normalized=True)

        # Get angle perpendicular to border
        start_to_end = self._get_straight_line_from_endpoints(border_line)
        border_bearing = self._calculate_bearing(start_to_end)
        perpendicular_angle = (border_bearing + 90) % 360

        return midpoint, perpendicular_angle

    def _get_midpoint_and_angle_from_line(self, line: LineString) -> tuple[Point, float]:
        """Extract midpoint and directional angle from a LineString.

        Args:
            line: Input LineString geometry

        Returns:
            tuple[Point, float]: Midpoint and bearing angle in degrees

        Note:
            Uses 50% interpolation to find the midpoint, ensuring consistent
            positioning regardless of coordinate density along the line.
        """
        midpoint = line.interpolate(0.5, normalized=True)
        angle = self._calculate_bearing(line)
        return midpoint, angle

    def _angle_points_to_target(
        self,
        geom_from: Union[Polygon, MultiPolygon],
        geom_to: Union[Polygon, MultiPolygon],
        midpoint: Point,
        angle: float
    ) -> bool:
        """Validate that computed angle points from source toward target area.

        Ensures directional consistency by checking if the angle points closer
        to the target area than to the source area. This is essential for
        correct arrow orientation in energy flow visualization.

        Args:
            geom_from: Source area geometry
            geom_to: Target area geometry  
            midpoint: Reference point for angle measurement
            angle: Angle to validate (in degrees)

        Returns:
            bool: True if angle points toward target, False if it points toward source

        Algorithm:
            1. Calculate bearings from midpoint to both area centroids
            2. Compute angular differences between proposed angle and both bearings
            3. Return True if angle is closer to target bearing than source bearing
        """
        centroid_from = self.get_representative_area_point(geom_from)
        centroid_to = self.get_representative_area_point(geom_to)

        bearing_to_from = self._calculate_bearing(LineString([midpoint, centroid_from]))
        bearing_to_to = self._calculate_bearing(LineString([midpoint, centroid_to]))

        angle_diff_from = self._angular_difference(bearing_to_from, angle)
        angle_diff_to = self._angular_difference(bearing_to_to, angle)

        return angle_diff_to < angle_diff_from

    def _get_continuous_border_line(
        self,
        geom_a: Union[Polygon, MultiPolygon],
        geom_b: Union[Polygon, MultiPolygon]
    ) -> LineString:
        """Extract shared boundary line between touching geometries.

        Computes the geometric intersection between two touching areas and
        converts the result into a continuous LineString representing the
        shared border. Handles complex intersection geometries including
        multiple segments and mixed geometry types.

        Args:
            geom_a: First area geometry
            geom_b: Second area geometry

        Returns:
            LineString: Continuous line representing the shared boundary

        Raises:
            ValueError: If geometries don't touch or intersect
            TypeError: If intersection cannot be converted to LineString

        Algorithm:
            1. Compute geometric intersection of the two areas
            2. Handle GeometryCollection by extracting line components
            3. Convert Polygon boundaries to LineString if needed
            4. Merge multiple LineStrings into continuous representation
            5. Handle MultiLineString by connecting segments optimally

        Note:
            This method handles the complexity of real-world geographic boundaries
            which may result in complex intersection geometries.
        """
        """Get the shared border between two touching geometries."""
        if not (geom_a.touches(geom_b) or geom_a.intersects(geom_b)):
            raise ValueError("Geometries do not touch or intersect")

        border = geom_a.intersection(geom_b)

        if isinstance(border, GeometryCollection):
            extracted_lines = []

            for g in border.geoms:
                if isinstance(g, LineString):
                    extracted_lines.append(g)
                elif isinstance(g, Polygon):
                    extracted_lines.append(g.boundary)
                elif isinstance(g, MultiLineString):
                    extracted_lines.extend(g.geoms)
                elif isinstance(g, MultiPolygon):
                    extracted_lines.extend([p.boundary for p in g.geoms])

            if not extracted_lines:
                raise TypeError(f"GeometryCollection could not be converted into line: {type(border)}")

            border = linemerge(extracted_lines)

        if isinstance(border, MultiPolygon):
            border = linemerge([p.boundary for p in border.geoms])

        if isinstance(border, Polygon):
            border = border.boundary

        if isinstance(border, MultiLineString):
            border = self._merge_multilinestring(border)

        if isinstance(border, LineString):
            return border

        raise TypeError(f"Unexpected border type: {type(border)}")

    def _get_boundary_intersection(
        self,
        geom: Polygon,
        line: LineString,
        target_point: Point
    ) -> Point:
        """Find optimal intersection point between line and polygon boundary.

        When a line intersects a polygon boundary at multiple points, this method
        selects the point that is closest to a specified target point. This is
        essential for creating clean border connections.

        Args:
            geom: Polygon whose boundary to intersect with
            line: LineString to intersect with polygon boundary
            target_point: Reference point for choosing among multiple intersections

        Returns:
            Point: Intersection point closest to target_point

        Raises:
            TypeError: If intersection geometry type is unexpected

        Algorithm:
            1. Compute intersection between line and polygon boundary
            2. Handle different intersection geometry types (Point, MultiPoint, LineString)
            3. For multiple options, select point closest to target
            4. Extract coordinates and create Point geometry
        """
        """Find where a line intersects a polygon boundary, choosing the point closest to target."""
        intersection = geom.boundary.intersection(line)

        if isinstance(intersection, Point):
            return intersection
        elif isinstance(intersection, MultiPoint):
            return min(intersection.geoms, key=lambda p: p.distance(target_point))
        elif isinstance(intersection, (LineString, MultiLineString)):
            # Get all coordinates and find closest
            coords = []
            if isinstance(intersection, LineString):
                coords = list(intersection.coords)
            else:
                for line in intersection.geoms:
                    coords.extend(list(line.coords))
            return Point(min(coords, key=lambda c: Point(c).distance(target_point)))
        else:
            raise TypeError(f"Unexpected intersection type: {type(intersection)}")

    def _line_crosses_other_areas(
        self,
        line: LineString,
        *exclude_areas: str
    ) -> bool:
        """Check if a line crosses through any areas except specified exclusions.

        This method is crucial for validating logical border connections to ensure
        they don't inappropriately cross through other energy market areas, which
        would be misleading in visualization.

        Args:
            line: LineString to test for crossings
            *exclude_areas: Area identifiers to exclude from crossing check
                (typically the source and target areas of the line)

        Returns:
            bool: True if line crosses any non-excluded areas, False otherwise
        """
        other_areas = self.area_model_gdf.drop(list(exclude_areas))
        return other_areas.geometry.crosses(line).any()

    def _find_non_crossing_line(
        self,
        area_from: str,
        area_to: str
    ) -> Union[LineString, None]:
        """Find optimal connection path that avoids crossing other areas.

        Uses the NonCrossingPathFinder to compute the shortest connection between
        two areas that maintains minimum clearance from other areas. This creates
        clean visualization paths for logical borders.

        Args:
            area_from: Source area identifier
            area_to: Target area identifier

        Returns:
            LineString or None: Optimal non-crossing path, or None if no suitable
                path found within the configured constraints

        Algorithm:
            1. Extract largest polygons from MultiPolygon geometries
            2. Create exclusion set of all other areas
            3. Apply NonCrossingPathFinder algorithm
            4. Return shortest valid path or None if impossible

        Performance Note:
            This operation can be computationally intensive for complex geometries
            and large numbers of areas. Results are cached for efficiency.
        """
        poly_from = self._get_largest_polygon(self.get_area_geometry(area_from))
        poly_to = self._get_largest_polygon(self.get_area_geometry(area_to))

        return self.non_crossing_path_finder.find_shortest_path(
            poly_from,
            poly_to,
            self.area_model_gdf.drop([area_from, area_to]),
            f"{area_from} to {area_to}"
        )

    def _get_largest_polygon(self, geom: Union[Polygon, MultiPolygon]) -> Polygon:
        """Extract largest polygon component from MultiPolygon geometry.

        For MultiPolygon geometries (e.g., countries with islands), this method
        returns the largest polygon by area, which is typically the main landmass.
        This simplifies calculations while focusing on the most significant
        geographic component.

        Args:
            geom: Input geometry (Polygon returned as-is, MultiPolygon simplified)

        Returns:
            Polygon: Largest polygon component by area
        """
        if isinstance(geom, Polygon):
            return geom
        return max(geom.geoms, key=lambda p: p.area)

    def _merge_multilinestring(self, mls: MultiLineString) -> LineString:
        """Merge disconnected LineString segments into continuous line.

        Converts a MultiLineString into a single continuous LineString by
        intelligently connecting segments to minimize total length while
        preserving the overall geometric relationship.

        Args:
            mls: MultiLineString with potentially disconnected segments

        Returns:
            LineString: Continuous line connecting all segments optimally

        Algorithm:
            1. Attempt automatic merge using Shapely's linemerge
            2. If segments remain disconnected, apply iterative connection:
               - Find closest pair of segment endpoints
               - Connect segments with optimal orientation
               - Repeat until single continuous line achieved

        Note:
            This method is particularly important for complex international
            borders that may be represented as multiple disconnected segments
            in geographic datasets.
        """
        merged = linemerge(list(mls.geoms))

        if isinstance(merged, LineString):
            return merged

        # Connect disconnected segments
        lines = list(merged.geoms)

        while len(lines) > 1:
            # Find closest pair
            min_dist = float('inf')
            closest_pair = None

            for i, line1 in enumerate(lines):
                for j, line2 in enumerate(lines[i+1:], i+1):
                    p1, p2 = nearest_points(line1, line2)
                    dist = p1.distance(p2)
                    if dist < min_dist:
                        min_dist = dist
                        closest_pair = (i, j)

            # Connect closest pair
            i, j = closest_pair
            line1, line2 = lines[i], lines[j]

            # Create connected line
            coords1 = list(line1.coords)
            coords2 = list(line2.coords)

            # Find best connection direction
            connections = [
                (coords1 + coords2, LineString(coords1 + coords2).length),
                (coords1 + coords2[::-1], LineString(coords1 + coords2[::-1]).length),
                (coords1[::-1] + coords2, LineString(coords1[::-1] + coords2).length),
                (coords1[::-1] + coords2[::-1], LineString(coords1[::-1] + coords2[::-1]).length)
            ]

            best_coords = min(connections, key=lambda x: x[1])[0]
            new_line = LineString(best_coords)

            # Update lines list
            lines = [l for k, l in enumerate(lines) if k not in (i, j)]
            lines.append(new_line)

        return lines[0]

    def _get_straight_line_from_endpoints(self, line: LineString) -> LineString:
        """Create straight line connecting first and last coordinates.

        Args:
            line: Input LineString with potentially complex path

        Returns:
            LineString: Simplified straight line from start to end

        Use Case:
            Useful for computing bearing angles from complex border geometries
            by simplifying to the fundamental start-to-end direction.
        """
        return LineString([line.coords[0], line.coords[-1]])

    def _calculate_bearing(self, line: LineString) -> float:
        """Calculate compass bearing (Azimuth angle) for a LineString.

        Args:
            line: LineString from which to calculate bearing

        Returns:
            float: Compass bearing (Azimuth angle) in degrees (0-360)
                - 0° = North
                - 90° = East  
                - 180° = South
                - 270° = West

        Algorithm:
            Uses the forward azimuth formula from geodetic calculations:
            1. Convert coordinates to radians
            2. Apply spherical trigonometry formulas
            3. Convert result to compass bearing (0-360°)

        Note:
            This implementation assumes coordinates are in geographic (lat/lon)
            format. For projected coordinates, results approximate true bearings
            within reasonable accuracy for visualization purposes.
        """
        start = line.coords[0]
        end = line.coords[-1]

        lat1 = math.radians(start[1])
        lat2 = math.radians(end[1])

        diff_lon = math.radians(end[0] - start[0])

        x = math.sin(diff_lon) * math.cos(lat2)
        y = (math.cos(lat1) * math.sin(lat2) - 
             math.sin(lat1) * math.cos(lat2) * math.cos(diff_lon))

        bearing = math.atan2(x, y)
        bearing = math.degrees(bearing)
        compass_bearing = (bearing + 360) % 360

        return compass_bearing

    def _angular_difference(self, angle1: float, angle2: float) -> float:
        """Calculate minimum angular difference between two compass bearings.

        Handles the circular nature of compass bearings to find the shortest
        angular distance between two directions, accounting for the 0°/360° wraparound.

        Args:
            angle1: First angle in degrees (0-360)
            angle2: Second angle in degrees (0-360)

        Returns:
            float: Minimum angular difference in degrees (0-180)

        Example:

            >>> diff = calculator._angular_difference(10, 350)  # Returns 20, not 340
            >>> diff = calculator._angular_difference(90, 270)  # Returns 180
        """
        diff = abs(angle1 - angle2) % 360
        return min(diff, 360 - diff)

__init__

__init__(area_model_gdf: GeoDataFrame, non_crossing_path_finder: NonCrossingPathFinder = None)

Initialize the border geometry calculator.

Parameters:

Name Type Description Default
area_model_gdf GeoDataFrame

GeoDataFrame containing area geometries with polygon boundaries. Index should contain area identifiers (e.g., country codes, bidding zone names). Must contain valid polygon geometries in 'geometry' column.

required
non_crossing_path_finder NonCrossingPathFinder

Optional custom path finder for logical borders. If None, creates default NonCrossingPathFinder with standard parameters.

None

Raises:

Type Description
ValueError

If geometries are invalid or area_model_gdf lacks required structure

Example:

>>> areas_gdf = gpd.read_file('countries.geojson').set_index('ISO_A2')
>>> calculator = AreaBorderGeometryCalculator(areas_gdf)
>>> 
>>> # Custom path finder for specific requirements
>>> custom_finder = NonCrossingPathFinder(num_points=200, min_clearance=10000)
>>> calculator = AreaBorderGeometryCalculator(areas_gdf, custom_finder)
Note

Invalid geometries are automatically cleaned using buffer(0) operation. Large area datasets benefit from using projected coordinate systems for accurate geometric calculations.

Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_geometry_calculator.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def __init__(self, area_model_gdf: gpd.GeoDataFrame, non_crossing_path_finder: 'NonCrossingPathFinder' = None):
    """Initialize the border geometry calculator.

    Args:
        area_model_gdf: GeoDataFrame containing area geometries with polygon
            boundaries. Index should contain area identifiers (e.g., country codes,
            bidding zone names). Must contain valid polygon geometries in 'geometry' column.
        non_crossing_path_finder: Optional custom path finder for logical borders.
            If None, creates default NonCrossingPathFinder with standard parameters.

    Raises:
        ValueError: If geometries are invalid or area_model_gdf lacks required structure

    Example:

        >>> areas_gdf = gpd.read_file('countries.geojson').set_index('ISO_A2')
        >>> calculator = AreaBorderGeometryCalculator(areas_gdf)
        >>> 
        >>> # Custom path finder for specific requirements
        >>> custom_finder = NonCrossingPathFinder(num_points=200, min_clearance=10000)
        >>> calculator = AreaBorderGeometryCalculator(areas_gdf, custom_finder)

    Note:
        Invalid geometries are automatically cleaned using buffer(0) operation.
        Large area datasets benefit from using projected coordinate systems
        for accurate geometric calculations.
    """
    self.area_model_gdf = area_model_gdf
    self.non_crossing_path_finder = non_crossing_path_finder or NonCrossingPathFinder()
    self._validate_geometries()

    self._centroid_cache: dict[str, Point] = {}
    self._line_cache: dict[Tuple[str, str], LineString] = {}

calculate_border_geometry

calculate_border_geometry(area_from: str, area_to: str) -> dict[str, Union[Point, float, LineString, bool]]

Calculate comprehensive geometric properties for an area border.

This is the main interface method that computes all geometric properties needed for border visualization and analysis. It automatically detects whether areas are physically adjacent or logically connected and applies appropriate geometric algorithms.

Processing Logic
  1. Detect if areas share physical boundary (touching/intersecting)
  2. For physical borders: extract shared boundary line
  3. For logical borders: compute optimal connection path
  4. Calculate representative point for label/arrow placement
  5. Compute azimuth angle for arrow icon visualization

Parameters:

Name Type Description Default
area_from str

Source area identifier (must exist in area_model_gdf index)

required
area_to str

Target area identifier (must exist in area_model_gdf index)

required

Returns:

Name Type Description
dict dict[str, Union[Point, float, LineString, bool]]

Comprehensive border geometry information containing: - 'projection_point' (Point): Optimal point for label/arrow placement - 'azimuth_angle' (float): Directional angle in degrees (0-360) - 'geo_line_string' (LineString): Border line geometry - 'is_physical' (bool): True for touching areas, False for logical borders

Raises:

Type Description
KeyError

If area_from or area_to not found in area_model_gdf

ValueError

If geometric calculations fail

Example:

>>> border_info = calculator.calculate_border_geometry('DE', 'FR')
>>> 
>>> # Use for visualization
>>> point = border_info['projection_point']
>>> angle = border_info['azimuth_angle']
>>> is_physical = border_info['is_physical']
>>> 
>>> print(f"Border DE→FR: {point} at {angle}° ({'physical' if is_physical else 'logical'})")
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_geometry_calculator.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def calculate_border_geometry(
    self, 
    area_from: str, 
    area_to: str
) -> dict[str, Union[Point, float, LineString, bool]]:
    """Calculate comprehensive geometric properties for an area border.

    This is the main interface method that computes all geometric properties
    needed for border visualization and analysis. It automatically detects
    whether areas are physically adjacent or logically connected and applies
    appropriate geometric algorithms.

    Processing Logic:
        1. Detect if areas share physical boundary (touching/intersecting)
        2. For physical borders: extract shared boundary line
        3. For logical borders: compute optimal connection path
        4. Calculate representative point for label/arrow placement
        5. Compute azimuth angle for arrow icon visualization

    Args:
        area_from: Source area identifier (must exist in area_model_gdf index)
        area_to: Target area identifier (must exist in area_model_gdf index)

    Returns:
        dict: Comprehensive border geometry information containing:
            - 'projection_point' (Point): Optimal point for label/arrow placement
            - 'azimuth_angle' (float): Directional angle in degrees (0-360)
            - 'geo_line_string' (LineString): Border line geometry
            - 'is_physical' (bool): True for touching areas, False for logical borders

    Raises:
        KeyError: If area_from or area_to not found in area_model_gdf
        ValueError: If geometric calculations fail

    Example:

        >>> border_info = calculator.calculate_border_geometry('DE', 'FR')
        >>> 
        >>> # Use for visualization
        >>> point = border_info['projection_point']
        >>> angle = border_info['azimuth_angle']
        >>> is_physical = border_info['is_physical']
        >>> 
        >>> print(f"Border DE→FR: {point} at {angle}° ({'physical' if is_physical else 'logical'})")
    """
    midpoint, angle = self.get_area_border_midpoint_and_angle(area_from, area_to)

    if self.areas_touch(area_from, area_to) or self.areas_intersect(area_from, area_to):
        geom_from = self.get_area_geometry(area_from)
        geom_to = self.get_area_geometry(area_to)
        border_line = self._get_continuous_border_line(geom_from, geom_to)
        is_physical = True
    else:
        border_line = self.get_straight_line_between_areas(area_from, area_to)
        is_physical = False

    return {
        self.PROJECTION_POINT_IDENTIFIER: midpoint,
        self.AZIMUTH_ANGLE_IDENTIFIER: angle,
        self.BORDER_LINE_STRING_IDENTIFIER: border_line,
        self.BORDER_IS_PHYSICAL_IDENTIFIER: is_physical
    }

areas_touch

areas_touch(area_from: str, area_to: str) -> bool

Check if two areas share a common physical (geographic) border.

Uses Shapely's touches() method to determine if area boundaries intersect without overlapping. This is the standard definition of physical adjacency for energy market regions.

Parameters:

Name Type Description Default
area_from str

Source area identifier

required
area_to str

Target area identifier

required

Returns:

Name Type Description
bool bool

True if areas share a common boundary, False otherwise

Example:

>>> touching = calculator.areas_touch('DE', 'FR')  # True for neighboring countries
>>> separated = calculator.areas_touch('DE', 'GB')  # False for non-adjacent countries
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_geometry_calculator.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def areas_touch(self, area_from: str, area_to: str) -> bool:
    """Check if two areas share a common physical (geographic) border.

    Uses Shapely's touches() method to determine if area boundaries
    intersect without overlapping. This is the standard definition
    of physical adjacency for energy market regions.

    Args:
        area_from: Source area identifier
        area_to: Target area identifier

    Returns:
        bool: True if areas share a common boundary, False otherwise

    Example:

        >>> touching = calculator.areas_touch('DE', 'FR')  # True for neighboring countries
        >>> separated = calculator.areas_touch('DE', 'GB')  # False for non-adjacent countries
    """
    geom_from = self.get_area_geometry(area_from)
    geom_to = self.get_area_geometry(area_to)
    return geom_from.touches(geom_to)

areas_intersect

areas_intersect(area_from: str, area_to: str) -> bool

Check if two areas have any geometric intersection.

Uses Shapely's intersects() method to check for any form of geometric intersection, including touching, overlapping, or containment. This is broader than the touches() check and handles edge cases in geographic data.

Parameters:

Name Type Description Default
area_from str

Source area identifier

required
area_to str

Target area identifier

required

Returns:

Name Type Description
bool bool

True if areas have any geometric intersection, False otherwise

Note

This method is used as a fallback for areas_touch() to handle geographic data with small overlaps or slight topology inconsistencies that are common in real-world boundary datasets.

Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_geometry_calculator.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def areas_intersect(self, area_from: str, area_to: str) -> bool:
    """Check if two areas have any geometric intersection.

    Uses Shapely's intersects() method to check for any form of geometric
    intersection, including touching, overlapping, or containment. This is
    broader than the touches() check and handles edge cases in geographic data.

    Args:
        area_from: Source area identifier
        area_to: Target area identifier

    Returns:
        bool: True if areas have any geometric intersection, False otherwise

    Note:
        This method is used as a fallback for areas_touch() to handle
        geographic data with small overlaps or slight topology inconsistencies
        that are common in real-world boundary datasets.
    """
    geom_from = self.get_area_geometry(area_from)
    geom_to = self.get_area_geometry(area_to)
    return geom_from.intersects(geom_to)

get_area_border_midpoint_and_angle

get_area_border_midpoint_and_angle(area_from: str, area_to: str) -> tuple[Point, float]

Calculate representative point and directional angle for border.

Computes the optimal point for placing directional indicators (arrows, labels) and the corresponding angle for proper orientation. The algorithm adapts to both physical and logical borders to ensure optimal placement.

For Physical Borders
  • Uses midpoint of shared boundary line
  • Angle is perpendicular to boundary, pointing toward target area
For Logical Borders
  • Uses midpoint of optimal connection line
  • Angle follows connection direction from source to target

Parameters:

Name Type Description Default
area_from str

Source area identifier

required
area_to str

Target area identifier

required

Returns:

Type Description
tuple[Point, float]

tuple[Point, float]: Representative point and directional angle in degrees. Angle range: 0-360 degrees, where 0° is North, 90° is East.

Example:

>>> point, angle = calculator.get_area_border_midpoint_and_angle('DE', 'FR')
>>> print(f"Place arrow at {point} oriented at {angle}° for DE→FR flow")
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_geometry_calculator.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def get_area_border_midpoint_and_angle(
    self, 
    area_from: str, 
    area_to: str
) -> tuple[Point, float]:
    """Calculate representative point and directional angle for border.

    Computes the optimal point for placing directional indicators (arrows,
    labels) and the corresponding angle for proper orientation. The algorithm
    adapts to both physical and logical borders to ensure optimal placement.

    For Physical Borders:
        - Uses midpoint of shared boundary line
        - Angle is perpendicular to boundary, pointing toward target area

    For Logical Borders:
        - Uses midpoint of optimal connection line
        - Angle follows connection direction from source to target

    Args:
        area_from: Source area identifier
        area_to: Target area identifier

    Returns:
        tuple[Point, float]: Representative point and directional angle in degrees.
            Angle range: 0-360 degrees, where 0° is North, 90° is East.

    Example:

        >>> point, angle = calculator.get_area_border_midpoint_and_angle('DE', 'FR')
        >>> print(f"Place arrow at {point} oriented at {angle}° for DE→FR flow")
    """
    geom_from = self.get_area_geometry(area_from)
    geom_to = self.get_area_geometry(area_to)

    if self.areas_touch(area_from, area_to) or self.areas_intersect(area_from, area_to):
        midpoint, angle = self._get_midpoint_and_angle_for_touching_areas(geom_from, geom_to)
    else:
        straight_line = self.get_straight_line_between_areas(area_from, area_to)
        midpoint, angle = self._get_midpoint_and_angle_from_line(straight_line)

    # Ensure angle points from area_from to area_to
    if not self._angle_points_to_target(geom_from, geom_to, midpoint, angle):
        angle = (angle + 180) % 360

    return midpoint, angle

get_area_geometry

get_area_geometry(area: str) -> Union[Polygon, MultiPolygon]

Retrieve and validate geometry for a specified area.

Parameters:

Name Type Description Default
area str

Area identifier that must exist in area_model_gdf index

required

Returns:

Type Description
Union[Polygon, MultiPolygon]

Union[Polygon, MultiPolygon]: Cleaned geometry with buffer(0) applied to ensure validity for geometric operations

Raises:

Type Description
KeyError

If area is not found in area_model_gdf

Note

The buffer(0) operation ensures geometric validity for complex calculations, which is essential for reliable border analysis.

Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_geometry_calculator.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
def get_area_geometry(self, area: str) -> Union[Polygon, MultiPolygon]:
    """Retrieve and validate geometry for a specified area.

    Args:
        area: Area identifier that must exist in area_model_gdf index

    Returns:
        Union[Polygon, MultiPolygon]: Cleaned geometry with buffer(0) applied
            to ensure validity for geometric operations

    Raises:
        KeyError: If area is not found in area_model_gdf

    Note:
        The buffer(0) operation ensures geometric validity for complex
        calculations, which is essential for reliable border analysis.
    """
    return self.area_model_gdf.loc[area].geometry.buffer(0)

get_straight_line_between_areas

get_straight_line_between_areas(area_from: str, area_to: str) -> LineString

Compute optimal straight-line connection between non-adjacent areas.

Creates a direct line connection between area boundaries, with intelligent path optimization to avoid crossing other areas when possible. This is particularly important for non-physical borders.

Algorithm
  1. Find representative points for both areas
  2. Create line connecting area centroids
  3. Calculate intersection points with area boundaries
  4. Check for conflicts with other areas
  5. Apply non-crossing path optimization if needed

Parameters:

Name Type Description Default
area_from str

Source area identifier

required
area_to str

Target area identifier

required

Returns:

Name Type Description
LineString LineString

Optimized connection line between area boundaries. Line endpoints touch the area boundaries, not the centroids.

Raises:

Type Description
ValueError

If areas are touching (should use physical border instead)

Example:

>>> # Connect non-adjacent areas (e.g., Germany to UK)
>>> line = calculator.get_straight_line_between_areas('DE', 'GB')
>>> print(f"Connection length: {line.length:.0f} km")
Performance Note

Results are cached to improve performance for repeated calculations. Path optimization can be computationally intensive for complex geometries.

Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_geometry_calculator.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
def get_straight_line_between_areas(self, area_from: str, area_to: str) -> LineString:
    """Compute optimal straight-line connection between non-adjacent areas.

    Creates a direct line connection between area boundaries, with intelligent
    path optimization to avoid crossing other areas when possible. This is
    particularly important for non-physical borders.

    Algorithm:
        1. Find representative points for both areas
        2. Create line connecting area centroids
        3. Calculate intersection points with area boundaries  
        4. Check for conflicts with other areas
        5. Apply non-crossing path optimization if needed

    Args:
        area_from: Source area identifier
        area_to: Target area identifier

    Returns:
        LineString: Optimized connection line between area boundaries.
            Line endpoints touch the area boundaries, not the centroids.

    Raises:
        ValueError: If areas are touching (should use physical border instead)

    Example:

        >>> # Connect non-adjacent areas (e.g., Germany to UK)
        >>> line = calculator.get_straight_line_between_areas('DE', 'GB')
        >>> print(f"Connection length: {line.length:.0f} km")

    Performance Note:
        Results are cached to improve performance for repeated calculations.
        Path optimization can be computationally intensive for complex geometries.
    """
    key = tuple(sorted((area_from, area_to)))
    if key in self._line_cache:
        return self._line_cache[key]

    if self.areas_touch(area_from, area_to):
        raise ValueError(f"Areas {area_from} and {area_to} touch - use border line instead")

    geom_from = self._get_largest_polygon(self.get_area_geometry(area_from))
    geom_to = self._get_largest_polygon(self.get_area_geometry(area_to))

    centroid_from = self.get_representative_area_point(geom_from)
    centroid_to = self.get_representative_area_point(geom_to)

    line_full = LineString([centroid_from, centroid_to])

    # Find intersection points with area boundaries
    intersection_from = self._get_boundary_intersection(geom_from, line_full, centroid_to)
    intersection_to = self._get_boundary_intersection(geom_to, line_full, centroid_from)

    straight_line = LineString([intersection_from, intersection_to])

    # Check if line crosses other areas
    if self._line_crosses_other_areas(straight_line, area_from, area_to):
        # Try to find alternative path
        better_line = self._find_non_crossing_line(area_from, area_to)
        if better_line is not None:
            straight_line = better_line

    self._line_cache[key] = straight_line
    return straight_line

NonCrossingPathFinder

Optimized path finder for non-crossing connections between areas.

This class implements an algorithm for finding the shortest path between two polygon areas while maintaining specified clearance from other areas. It's specifically designed for energy system visualization where geographic border line representations should not misleadingly cross through other market areas.

The algorithm uses a brute-force approach to test multiple potential paths and select the optimal solution.

Key Features
  • Configurable boundary point sampling density
  • Adjustable minimum clearance distances
  • Progress tracking for long-running operations
  • Optimization for common geometric scenarios
Performance Characteristics
  • Time complexity: O(n² × m) where n=num_points, m=number of other areas
  • Memory usage scales with point sampling density
  • Results improve with higher point sampling but at computational cost

Attributes:

Name Type Description
num_points int

Number of boundary points to sample per area

min_clearance float

Minimum distance from other areas (in CRS units)

show_progress bool

Whether to display progress bars for long operations

Example:

>>> # High-precision path finding
>>> finder = NonCrossingPathFinder(num_points=500, min_clearance=50000)
>>> path = finder.find_shortest_path(area1_poly, area2_poly, other_areas_gdf)
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_geometry_calculator.py
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
class NonCrossingPathFinder:
    """Optimized path finder for non-crossing connections between areas.

    This class implements an algorithm for finding the shortest
    path between two polygon areas while maintaining specified clearance from
    other areas. It's specifically designed for energy system visualization
    where geographic border line representations should not misleadingly
    cross through other market areas.

    The algorithm uses a brute-force approach to test multiple potential paths
    and select the optimal solution.

    Key Features:
        - Configurable boundary point sampling density
        - Adjustable minimum clearance distances
        - Progress tracking for long-running operations
        - Optimization for common geometric scenarios

    Performance Characteristics:
        - Time complexity: O(n² × m) where n=num_points, m=number of other areas
        - Memory usage scales with point sampling density
        - Results improve with higher point sampling but at computational cost

    Attributes:
        num_points (int): Number of boundary points to sample per area
        min_clearance (float): Minimum distance from other areas (in CRS units)
        show_progress (bool): Whether to display progress bars for long operations

    Example:

        >>> # High-precision path finding
        >>> finder = NonCrossingPathFinder(num_points=500, min_clearance=50000)
        >>> path = finder.find_shortest_path(area1_poly, area2_poly, other_areas_gdf)
    """

    def __init__(
        self,
        num_points: int = 100,
        min_clearance: float = 50000,
        show_progress: bool = True
    ):
        """Initialize the non-crossing path finder.

        Args:
            num_points: Number of boundary points to sample per polygon.
                Higher values improve path quality but increase computation time.
                Typical range: 50-500 depending on precision requirements.
            min_clearance: Minimum clearance distance from other areas in
                coordinate reference system units. For geographic coordinates,
                this is typically in meters when using projected CRS.
            show_progress: Whether to display progress bars during computation.
                Useful for long-running operations with high num_points values.

        Example:

            >>> # High-precision finder for detailed analysis
            >>> finder = NonCrossingPathFinder(
            ...     num_points=300,      # High sampling density
            ...     min_clearance=25000, # 25km minimum clearance
            ...     show_progress=True   # Show progress for long operations
            ... )
        """
        self.num_points = num_points
        self.min_clearance = min_clearance
        self.show_progress = show_progress

    def find_shortest_path(
        self,
        polygon1: Polygon,
        polygon2: Polygon,
        other_areas: gpd.GeoDataFrame,
        name: str = None
    ) -> Union[LineString, None]:
        """Find shortest non-crossing path between two polygons.

        Tests all combinations of boundary points between two polygons to find
        the shortest connection that maintains minimum clearance from other areas.
        If the algorithm succeedes and finds a non-crossing LineString, it ensures
        clean visualization paths for energy market border analysis.

        Args:
            polygon1: Source polygon geometry
            polygon2: Target polygon geometry
            other_areas: GeoDataFrame of areas to avoid crossing through
            name: Optional name for progress tracking display

        Returns:
            LineString or None: Shortest valid path between polygons, or None
                if no path meets clearance requirements

        Algorithm:
            1. Sample boundary points for both polygons
            2. Buffer other areas by minimum clearance distance
            3. Test all point-to-point connections
            4. Filter out paths that cross buffered areas
            5. Return shortest valid path

        Performance Scaling:
            - Total paths tested: num_points² 
            - With default num_points=100: tests 10,000 potential paths
            - Computation time scales roughly O(n² × m) where m = number of other areas

        Example:

            >>> path = finder.find_shortest_path(
            ...     source_area, target_area, obstacles_gdf, "Germany to UK"
            ... )
            >>> if path:
            ...     print(f"Found path with length: {path.length:.0f} meters")
            ... else:
            ...     print("No valid path found with current clearance settings")
        """
        buffered_areas = self._buffer_areas(other_areas, self.min_clearance)
        points1 = self._get_boundary_points(polygon1, self.num_points)
        points2 = self._get_boundary_points(polygon2, self.num_points)

        lines = [LineString([p1, p2]) for p1, p2 in itertools.product(points1, points2)]
        shortest_line = None
        min_length = float('inf')

        iterator = tqdm(lines, desc=f"Finding path for {name or 'path'}") if self.show_progress else lines

        for line in iterator:
            if not buffered_areas.geometry.crosses(line).any():
                if line.length < min_length:
                    shortest_line = line
                    min_length = line.length

        return shortest_line

    def _buffer_areas(self, areas: gpd.GeoDataFrame, buffer_distance: float) -> gpd.GeoDataFrame:
        """Apply buffer operation to create clearance zones around areas.

        Creates expanded geometries around areas to enforce minimum clearance
        distances. Handles coordinate system transformations to ensure accurate
        distance-based buffering regardless of input CRS.

        Args:
            areas: GeoDataFrame containing areas to buffer
            buffer_distance: Buffer distance in meters

        Returns:
            gpd.GeoDataFrame: Areas with buffered geometries in original CRS

        Raises:
            ValueError: If GeoDataFrame lacks valid CRS definition

        Algorithm:
            1. Check if CRS is geographic (lat/lon)
            2. If geographic, temporarily project to Web Mercator (EPSG:3857)
            3. Apply buffer operation in projected coordinates
            4. Transform back to original CRS
            5. If already projected, buffer directly
        """
        areas_copy = areas.copy()
        original_crs = areas_copy.crs

        if original_crs is None:
            raise ValueError("GeoDataFrame must have a valid CRS defined.")

        if original_crs.is_geographic:
            # Use Web Mercator for accurate distance-based buffering
            projected_crs = "EPSG:3857"
            areas_copy = areas_copy.to_crs(projected_crs)
            areas_copy['geometry'] = areas_copy.buffer(buffer_distance)
            areas_copy = areas_copy.to_crs(original_crs)
        else:
            # Already in projected coordinates
            areas_copy['geometry'] = areas_copy.buffer(buffer_distance)

        return areas_copy

    def _get_boundary_points(self, polygon: Polygon, num_points: int) -> list[Point]:
        """Sample evenly distributed points along polygon boundary.

        Creates a uniform sampling of points along the polygon perimeter using
        interpolation. This provides comprehensive coverage for path-finding
        while maintaining computational efficiency.

        Args:
            polygon: Input polygon to sample
            num_points: Number of points to sample along boundary

        Returns:
            list[Point]: List of evenly spaced boundary points

        Algorithm:
            1. Calculate total boundary length
            2. Divide into equal segments
            3. Interpolate points at regular intervals
            4. Return as list of Point geometries

        Note:
            Points are distributed proportionally to boundary length,
            ensuring uniform density regardless of polygon complexity.
        """
        boundary = polygon.boundary
        total_length = boundary.length
        # Generate evenly spaced points along boundary
        return [boundary.interpolate((i / num_points) * total_length) for i in range(num_points)]

__init__

__init__(num_points: int = 100, min_clearance: float = 50000, show_progress: bool = True)

Initialize the non-crossing path finder.

Parameters:

Name Type Description Default
num_points int

Number of boundary points to sample per polygon. Higher values improve path quality but increase computation time. Typical range: 50-500 depending on precision requirements.

100
min_clearance float

Minimum clearance distance from other areas in coordinate reference system units. For geographic coordinates, this is typically in meters when using projected CRS.

50000
show_progress bool

Whether to display progress bars during computation. Useful for long-running operations with high num_points values.

True

Example:

>>> # High-precision finder for detailed analysis
>>> finder = NonCrossingPathFinder(
...     num_points=300,      # High sampling density
...     min_clearance=25000, # 25km minimum clearance
...     show_progress=True   # Show progress for long operations
... )
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_geometry_calculator.py
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
def __init__(
    self,
    num_points: int = 100,
    min_clearance: float = 50000,
    show_progress: bool = True
):
    """Initialize the non-crossing path finder.

    Args:
        num_points: Number of boundary points to sample per polygon.
            Higher values improve path quality but increase computation time.
            Typical range: 50-500 depending on precision requirements.
        min_clearance: Minimum clearance distance from other areas in
            coordinate reference system units. For geographic coordinates,
            this is typically in meters when using projected CRS.
        show_progress: Whether to display progress bars during computation.
            Useful for long-running operations with high num_points values.

    Example:

        >>> # High-precision finder for detailed analysis
        >>> finder = NonCrossingPathFinder(
        ...     num_points=300,      # High sampling density
        ...     min_clearance=25000, # 25km minimum clearance
        ...     show_progress=True   # Show progress for long operations
        ... )
    """
    self.num_points = num_points
    self.min_clearance = min_clearance
    self.show_progress = show_progress

find_shortest_path

find_shortest_path(polygon1: Polygon, polygon2: Polygon, other_areas: GeoDataFrame, name: str = None) -> Union[LineString, None]

Find shortest non-crossing path between two polygons.

Tests all combinations of boundary points between two polygons to find the shortest connection that maintains minimum clearance from other areas. If the algorithm succeedes and finds a non-crossing LineString, it ensures clean visualization paths for energy market border analysis.

Parameters:

Name Type Description Default
polygon1 Polygon

Source polygon geometry

required
polygon2 Polygon

Target polygon geometry

required
other_areas GeoDataFrame

GeoDataFrame of areas to avoid crossing through

required
name str

Optional name for progress tracking display

None

Returns:

Type Description
Union[LineString, None]

LineString or None: Shortest valid path between polygons, or None if no path meets clearance requirements

Algorithm
  1. Sample boundary points for both polygons
  2. Buffer other areas by minimum clearance distance
  3. Test all point-to-point connections
  4. Filter out paths that cross buffered areas
  5. Return shortest valid path
Performance Scaling
  • Total paths tested: num_points²
  • With default num_points=100: tests 10,000 potential paths
  • Computation time scales roughly O(n² × m) where m = number of other areas

Example:

>>> path = finder.find_shortest_path(
...     source_area, target_area, obstacles_gdf, "Germany to UK"
... )
>>> if path:
...     print(f"Found path with length: {path.length:.0f} meters")
... else:
...     print("No valid path found with current clearance settings")
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/border_model_geometry_calculator.py
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
def find_shortest_path(
    self,
    polygon1: Polygon,
    polygon2: Polygon,
    other_areas: gpd.GeoDataFrame,
    name: str = None
) -> Union[LineString, None]:
    """Find shortest non-crossing path between two polygons.

    Tests all combinations of boundary points between two polygons to find
    the shortest connection that maintains minimum clearance from other areas.
    If the algorithm succeedes and finds a non-crossing LineString, it ensures
    clean visualization paths for energy market border analysis.

    Args:
        polygon1: Source polygon geometry
        polygon2: Target polygon geometry
        other_areas: GeoDataFrame of areas to avoid crossing through
        name: Optional name for progress tracking display

    Returns:
        LineString or None: Shortest valid path between polygons, or None
            if no path meets clearance requirements

    Algorithm:
        1. Sample boundary points for both polygons
        2. Buffer other areas by minimum clearance distance
        3. Test all point-to-point connections
        4. Filter out paths that cross buffered areas
        5. Return shortest valid path

    Performance Scaling:
        - Total paths tested: num_points² 
        - With default num_points=100: tests 10,000 potential paths
        - Computation time scales roughly O(n² × m) where m = number of other areas

    Example:

        >>> path = finder.find_shortest_path(
        ...     source_area, target_area, obstacles_gdf, "Germany to UK"
        ... )
        >>> if path:
        ...     print(f"Found path with length: {path.length:.0f} meters")
        ... else:
        ...     print("No valid path found with current clearance settings")
    """
    buffered_areas = self._buffer_areas(other_areas, self.min_clearance)
    points1 = self._get_boundary_points(polygon1, self.num_points)
    points2 = self._get_boundary_points(polygon2, self.num_points)

    lines = [LineString([p1, p2]) for p1, p2 in itertools.product(points1, points2)]
    shortest_line = None
    min_length = float('inf')

    iterator = tqdm(lines, desc=f"Finding path for {name or 'path'}") if self.show_progress else lines

    for line in iterator:
        if not buffered_areas.geometry.crosses(line).any():
            if line.length < min_length:
                shortest_line = line
                min_length = line.length

    return shortest_line

GeoModelGeneratorBase

Base class for generating geometric models with representative points.

This class provides common functionality for working with geometric representations of energy system areas, including methods for computing representative points within polygons and multipolygons. It's designed to support energy market analysis where spatial aggregation of nodes into areas is required.

The class supports two methods for computing representative points: - 'polylabel': Uses pole of inaccessibility algorithm for optimal label placement - 'representative_point': Uses Shapely's built-in representative point method

Attributes:

Name Type Description
REPRESENTATIVE_POINT_METHOD str

Method used for computing representative points ('polylabel' or 'representative_point')

_polylabel_cache dict

Cache for expensive polylabel calculations

Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/model_generator_base.py
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
class GeoModelGeneratorBase:
    """Base class for generating geometric models with representative points.

    This class provides common functionality for working with geometric representations
    of energy system areas, including methods for computing representative points
    within polygons and multipolygons. It's designed to support energy market
    analysis where spatial aggregation of nodes into areas is required.

    The class supports two methods for computing representative points:
    - 'polylabel': Uses pole of inaccessibility algorithm for optimal label placement
    - 'representative_point': Uses Shapely's built-in representative point method

    Attributes:
        REPRESENTATIVE_POINT_METHOD (str): Method used for computing representative
            points ('polylabel' or 'representative_point')
        _polylabel_cache (dict): Cache for expensive polylabel calculations
    """
    REPRESENTATIVE_POINT_METHOD: Literal['polylabel', 'representative_point'] = 'polylabel'
    _polylabel_cache = {}

    def get_representative_area_point(self, geom: Union[Polygon, MultiPolygon]) -> Point:
        """Get a representative point for a polygon or multipolygon geometry.

        This method computes a point that is guaranteed to be inside the geometry
        and is suitable for label placement or other visualization purposes in
        energy system maps. For MultiPolygons, it operates on the largest constituent.

        Args:
            geom: A Shapely Polygon or MultiPolygon geometry representing an
                energy system area (e.g., bidding zone, market region)

        Returns:
            Point: A Shapely Point guaranteed to be inside the input geometry,
                suitable for map labels or representative location analysis

        Raises:
            ValueError: If REPRESENTATIVE_POINT_METHOD is not supported

        Example:

            >>> from shapely.geometry import Polygon, Point
            >>> generator = GeoModelGeneratorBase()
            >>> area_polygon = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
            >>> rep_point = generator.get_representative_area_point(area_polygon)
            >>> print(f"Representative point: {rep_point.x:.2f}, {rep_point.y:.2f}")
        """
        if self.REPRESENTATIVE_POINT_METHOD == 'polylabel':
            return self._get_polylabel_point(geom)
        elif self.REPRESENTATIVE_POINT_METHOD == 'representative_point':
            return geom.representative_point()
        else:
            raise ValueError(f'REPRESENTATIVE_POINT_METHOD {self.REPRESENTATIVE_POINT_METHOD} not supported')

    def _get_polylabel_point(self, geom: Union[Polygon, MultiPolygon]) -> Point:
        """Compute representative point using the polylabel algorithm.

        The polylabel algorithm finds the pole of inaccessibility - the most distant
        internal point from the polygon outline. This is particularly useful for
        placing labels on complex energy system area geometries.

        For MultiPolygons, operates on the largest polygon by area, which is
        typically the main landmass for country/region representations.

        Args:
            geom: A Shapely Polygon or MultiPolygon geometry

        Returns:
            Point: The pole of inaccessibility point, cached for performance

        Note:
            Results are cached using the geometry's WKT representation as key.
            The precision parameter (1.0) provides good balance between accuracy
            and performance for typical energy system area sizes.
        """
        key = geom.wkt
        if key in self._polylabel_cache:
            return self._polylabel_cache[key]

        if isinstance(geom, MultiPolygon):
            geom = max(geom.geoms, key=lambda g: g.area)

        exterior = list(geom.exterior.coords)
        holes = [list(ring.coords) for ring in geom.interiors]
        rings = [exterior] + holes

        point = Point(polylabel(rings, 1.0))
        self._polylabel_cache[key] = point
        return point

    @staticmethod
    def _compute_representative_point_from_cloud_of_2d_points(points: List[Point]) -> Point:
        """Compute geometric centroid from a collection of 2D points.

        This method is particularly useful in energy systems analysis for computing
        representative locations of energy assets (e.g., power plants, substations)
        that belong to the same area or region.

        The algorithm adapts based on the number of input points:
        - 1 point: Returns the point itself
        - 2 points: Returns the midpoint
        - ≥3 points: Computes convex hull and returns polygon centroid

        Args:
            points: List of Shapely Point objects representing energy asset
                locations or other spatial features within an area

        Returns:
            Point: Representative point for the collection of input points

        Raises:
            ValueError: If the input list is empty

        Example:

            >>> from shapely.geometry import Point
            >>> power_plants = [Point(1, 1), Point(3, 2), Point(2, 4)]
            >>> centroid = GeoModelGeneratorBase._compute_representative_point_from_cloud_of_2d_points(power_plants)
            >>> print(f"Regional centroid: {centroid.x:.2f}, {centroid.y:.2f}")
        """
        from scipy.spatial import ConvexHull

        n = len(points)
        if n == 0:
            raise ValueError("Empty point list provided - cannot compute representative point")
        if n == 1:
            return points[0]
        if n == 2:
            return Point((points[0].x + points[1].x) / 2, (points[0].y + points[1].y) / 2)

        # For 3+ points, compute convex hull and return centroid
        coords = [(p.x, p.y) for p in points]
        hull = ConvexHull(coords)
        hull_coords = [coords[i] for i in hull.vertices]
        polygon = Polygon(hull_coords)
        return polygon.representative_point()

get_representative_area_point

get_representative_area_point(geom: Union[Polygon, MultiPolygon]) -> Point

Get a representative point for a polygon or multipolygon geometry.

This method computes a point that is guaranteed to be inside the geometry and is suitable for label placement or other visualization purposes in energy system maps. For MultiPolygons, it operates on the largest constituent.

Parameters:

Name Type Description Default
geom Union[Polygon, MultiPolygon]

A Shapely Polygon or MultiPolygon geometry representing an energy system area (e.g., bidding zone, market region)

required

Returns:

Name Type Description
Point Point

A Shapely Point guaranteed to be inside the input geometry, suitable for map labels or representative location analysis

Raises:

Type Description
ValueError

If REPRESENTATIVE_POINT_METHOD is not supported

Example:

>>> from shapely.geometry import Polygon, Point
>>> generator = GeoModelGeneratorBase()
>>> area_polygon = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
>>> rep_point = generator.get_representative_area_point(area_polygon)
>>> print(f"Representative point: {rep_point.x:.2f}, {rep_point.y:.2f}")
Source code in submodules/mesqual/mesqual/energy_data_handling/area_accounting/model_generator_base.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def get_representative_area_point(self, geom: Union[Polygon, MultiPolygon]) -> Point:
    """Get a representative point for a polygon or multipolygon geometry.

    This method computes a point that is guaranteed to be inside the geometry
    and is suitable for label placement or other visualization purposes in
    energy system maps. For MultiPolygons, it operates on the largest constituent.

    Args:
        geom: A Shapely Polygon or MultiPolygon geometry representing an
            energy system area (e.g., bidding zone, market region)

    Returns:
        Point: A Shapely Point guaranteed to be inside the input geometry,
            suitable for map labels or representative location analysis

    Raises:
        ValueError: If REPRESENTATIVE_POINT_METHOD is not supported

    Example:

        >>> from shapely.geometry import Polygon, Point
        >>> generator = GeoModelGeneratorBase()
        >>> area_polygon = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
        >>> rep_point = generator.get_representative_area_point(area_polygon)
        >>> print(f"Representative point: {rep_point.x:.2f}, {rep_point.y:.2f}")
    """
    if self.REPRESENTATIVE_POINT_METHOD == 'polylabel':
        return self._get_polylabel_point(geom)
    elif self.REPRESENTATIVE_POINT_METHOD == 'representative_point':
        return geom.representative_point()
    else:
        raise ValueError(f'REPRESENTATIVE_POINT_METHOD {self.REPRESENTATIVE_POINT_METHOD} not supported')