aligner – BRDR - Aligning borders

# Aligner

    aligner.Aligner(processor=None, crs=DEFAULT_CRS, config=None, feedback=None)

Compares and aligns thematic geospatial data against a set of reference data.

The Aligner manages the loading of both thematic and reference data, configures the geometric processing rules, and executes the alignment, prediction, and evaluation logic across a series of relevant distances.

## Attributes

Name | Type | Description
--- | --- | ---
logger | Logger | Instance for logging feedback and information.
log_metadata | bool | If True, metadata about the actuation is logged in the results.
add_observations | bool | If True, process result observations will be computed by default.
processor | BaseProcessor or AlignerGeometryProcessor | The geometric processor used for alignment calculations.
correction_distance | float | Distance used in buffer operations to remove slivers (technical correction).
mitre_limit | int | Parameter for the buffer operation to control the maximum length of join corners.
max_workers | (int, optional) | The maximum number of workers for parallel execution (ThreadPoolExecutor).
crs | str | The Coordinate Reference System (CRS) being used (bv. ‘EPSG:31370’).
name_thematic_id | str | Name of the identifier field for thematic data.
diff_metric | DiffMetric | The metric used to measure differences between geometries (bv. area change).
reference_data | (AlignerFeatureCollection, optional) | Loaded collection of reference features.
thematic_data | (AlignerFeatureCollection, optional) | Loaded collection of thematic features.

## Notes

The Aligner acts as the central orchestrator of the `brdr` package. It connects data loaders with geometric processors.

    graph LR
        T[Thematic Data] --> Aligner
        R[Reference Data] --> Aligner
        Aligner --> P{Processor}
        P --> Res[AlignerResult]

## Examples

    >>> from brdr.aligner import Aligner
    >>> aligner = Aligner(crs="EPSG:31370")
    >>> aligner.load_thematic_data(my_features)
    >>> results = aligner.align(relevant_distances=[0.5, 1.0])

## Methods

Name | Description
--- | ---
compare_to_reference | Calculates observation-related information based on the input geometry.
evaluate | Evaluates input geometries against predictions using observation matching
get_difference_metrics_for_thematic_data | Calculates difference metrics for thematic elements across a series of distances.
load_reference_data | Loads the reference features into the aligner and prepares them for processing.
load_thematic_data | Loads the thematic features into the aligner using a specific loader.
predict | Predicts the ‘most interesting’ relevant distances for changes in thematic
process | Executes the alignment process across multiple relevant distances.

### compare_to_reference

    aligner.Aligner.compare_to_reference(geometry, with_geom=False)

Calculates observation-related information based on the input geometry.

This method performs a spatial analysis to determine how much of the input geometry is covered by reference features and identifies which specific reference elements are involved. It also detects “OD” (Onbekend Terrein/Open Space) areas that are not covered by any reference data.

#### Parameters

Name | Type | Description | Default
--- | --- | --- | ---
geometry | BaseGeometry | The input geometry to be analyzed against the reference dataset. | required
with_geom | bool | If True, includes the GeoJSON representation of each intersection and the OD-area in the output. Defaults to False. | `False`

#### Returns

Name | Type | Description
--- | --- | ---
| Dict[str, Any] | A dictionary containing the alignment analysis: - alignment_date (str): Timestamp of the calculation. - brdr_version (str): Version of the package used. - full (bool): True if the geometry is entirely composed of complete reference features. - area (float): Total area of the input geometry. - last_version_date (str, optional): The most recent version date found among the intersected reference features. - reference_features (dict): A mapping of reference IDs to: - full (bool): True if the reference feature is fully contained. - area (float): Area of the intersection. - percentage (float): Coverage percentage relative to the reference. - geometry (str, optional): GeoJSON string (if with_geom is True). - reference_od (dict, optional): Description of areas not covered by reference features, containing ‘area’ and optionally ‘geometry’.

#### Notes

The method uses a spatial index (R-tree) to efficiently find intersecting reference features. A correction distance is applied to the “OD” calculation to filter out insignificant slivers.

    graph TD
        In[Input Geometry] --> Query[Spatial Index Query]
        Query --> Intersect[Calculate Intersections]
        Intersect --> Stats[Compute Area & %]
        In --> Diff[Difference with Union of Refs]
        Diff --> OD[Identify Unknown Area / OD]
        Stats --> Dict[Final observation Dictionary]
        OD --> Dict

#### Examples

    >>> info = aligner.compare_to_reference(my_geom, with_geom=True)
    >>> print(info["full"])
    >>> print(info["reference_features"].keys())  # IDs of intersected refs

### evaluate

    aligner.Aligner.evaluate(
        relevant_distances=None,
        *,
        thematic_ids=None,
        metadata_field=METADATA_FIELD_NAME,
        full_reference_strategy=FullReferenceStrategy.NO_FULL_REFERENCE,
        max_predictions=-1,
        multi_to_best_prediction=True,
    )

Evaluates input geometries against predictions using observation matching and selection strategies.

This method identifies the best candidate geometries for alignment by comparing predicted geometries against the original attributes and reference data. It tags results with evaluation labels (e.g., PREDICTION_UNIQUE) to facilitate decision-making.

#### Parameters

Name | Type | Description | Default
--- | --- | --- | ---
relevant_distances | Iterable[float] | Distances to evaluate. Defaults to 0.0m to 3.0m (step 0.1m). | `None`
thematic_ids | List[ThematicId] | List of IDs to evaluate. If None, all loaded thematic features are processed. Features not in this list are marked as NOT_EVALUATED. | `None`
metadata_field | str | The field name containing the metadata of the input geometry. Defaults to METADATA_FIELD_NAME. | `METADATA_FIELD_NAME`
full_reference_strategy | FullReferenceStrategy | Strategy to prioritize predictions based on their ‘fullness’ relative to reference data. Defaults to NO_FULL_REFERENCE. | `FullReferenceStrategy.NO_FULL_REFERENCE`
max_predictions | int | Maximum number of predictions to return per feature. -1 returns all candidates. Defaults to -1. | `-1`
multi_to_best_prediction | bool | If True and `max_predictions=1`, returns the candidate with the highest score. If False, returns the original geometry when multiple candidates exist. Defaults to True. | `True`

#### Returns

Name | Type | Description
--- | --- | ---
| AlignerResult | An object containing evaluated results with detailed metadata and evaluation status tags.

#### Raises

Name | Type | Description
--- | --- | ---
| ValueError | If provided `thematic_ids` are not present in the thematic data.

#### Notes

The evaluation logic follows a specific priority tree: 1. observation Match: If a prediction matches the base observation exactly, it is prioritized (score 100). 2. Fullness: If a strategy is set, ‘full’ predictions get a score boost. 3. Scoring: Candidates are ranked by their prediction score. 4. Selection: Based on `max_predictions`, the best candidates are selected.

    graph TD
        Start[Start Evaluation] --> Pred[Generate Predictions]
        Pred --> Match{observation Match?}
        Match -- Yes --> HighScore[Prioritize & Score 100]
        Match -- No --> Full{Full Reference?}
        Full -- Yes --> Strategy[Apply Fullness Strategy]
        Full -- No --> Rank[Rank by Prediction Score]
        Strategy --> Rank
        HighScore --> Rank
        Rank --> Select{Max Predictions?}
        Select --> Final[Return AlignerResult]

#### Examples

    >>> aligner = Aligner()
    >>> # Evaluate with a limit of 1 best prediction per feature
    >>> results = aligner.evaluate(thematic_ids=["id_01"],full_reference_strategy=FullReferenceStrategy.ONLY_FULL_REFERENCE,max_predictions=1)

### get_difference_metrics_for_thematic_data

    aligner.Aligner.get_difference_metrics_for_thematic_data(
        dict_processresults=None,
        thematic_data=None,
        diff_metric=DiffMetric.SYMMETRICAL_AREA_CHANGE,
    )

Calculates difference metrics for thematic elements across a series of distances.

This method iterates through the processed results and compares the resulting geometries against their original thematic counterparts. It uses the spatial context of the reference data to calculate metrics like area change or symmetrical difference.

#### Parameters

Name | Type | Description | Default
--- | --- | --- | ---
dict_processresults | Dict[ThematicId, Dict[float, Optional[ProcessResult]]] | A nested dictionary where keys are thematic IDs and values are dictionaries mapping relevant distances to `ProcessResult`[] objects. Required if not provided via internal state. | `None`
thematic_data | AlignerFeatureCollection | The collection containing the original thematic geometries. Defaults to `self.thematic_data`. | `None`
diff_metric | DiffMetric | The metric used to quantify the geometric change. Defaults to `DiffMetric.SYMMETRICAL_AREA_CHANGE`. | `DiffMetric.SYMMETRICAL_AREA_CHANGE`

#### Returns

Name | Type | Description
--- | --- | ---
| Dict[ThematicId, Dict[float, float]] | A nested dictionary where each thematic ID maps to a dictionary of distances and their corresponding calculated metric values.

#### Raises

Name | Type | Description
--- | --- | ---
| ValueError | If `dict_processresults` is not provided and cannot be resolved.

#### Notes

The calculation involves three primary geometric components: 1. Original Geometry: The thematic feature before alignment. 2. Result Geometry: The feature after alignment at a specific distance. 3. Reference Union: The combined geometry of all reference data, used to contextualize the change.

    graph LR
        PR[Process Results] --> Calc[Metric Calculator]
        Orig[Original Geometries] --> Calc
        Ref[Reference Union] --> Calc
        Calc --> Output[Distance-Metric Map]

#### Examples

    >>> metrics = aligner.get_difference_metrics_for_thematic_data(
    ...     dict_processresults=my_results,
    ...     diff_metric=DiffMetric.AREA_CHANGE
    ... )
    >>> # Get area change for feature 'A' at distance 0.5
    >>> print(metrics['A'][0.5])

    ### load_reference_data { #brdr.aligner.Aligner.load_reference_data }

    ```python
    aligner.Aligner.load_reference_data(loader)

Loads the reference features into the aligner and prepares them for processing.

This method retrieves data via the provided loader, synchronizes the CRS, and marks the feature collection as a reference dataset to enable spatial indexing and comparison logic.

#### Parameters

Name | Type | Description | Default
--- | --- | --- | ---
loader | Loader | An instance of a Loader class (e.g., WFSReferenceLoader) that implements the `load_data` interface. | required

#### Notes

Setting `is_reference = True` on the resulting dataset is essential for the internal alignment logic, as it distinguishes the ‘anchor’ geometries from the thematic geometries that need to be shifted.

#### Examples

    >>> from brdr.loader import WFSReferenceLoader
    >>> ref_loader = WFSReferenceLoader(url="https://geoserver.com/wfs")
    >>> aligner.load_reference_data(ref_loader)
    >>> print(aligner.reference_data.is_reference)
    True

### load_thematic_data

    aligner.Aligner.load_thematic_data(loader)

Loads the thematic features into the aligner using a specific loader.

This method executes the loader’s data retrieval logic and ensures that the resulting feature collection is tagged with the Aligner’s CRS.

#### Parameters

Name | Type | Description | Default
--- | --- | --- | ---
loader | Loader | An instance of a Loader class (e.g., GeoJsonLoader or WFSReferenceLoader) that implements the `load_data` interface. | required

#### Notes

The method automatically synchronizes the CRS of the loaded data with the `self.crs` attribute of the Aligner instance.

#### Examples

    >>> from brdr.loader import GeoJsonLoader
    >>> loader = GeoJsonLoader(path="data/themes.json")
    >>> aligner.load_thematic_data(loader)

### predict

    aligner.Aligner.predict(
        relevant_distances=None,
        *,
        thematic_ids=None,
        diff_metric=None,
    )

Predicts the ‘most interesting’ relevant distances for changes in thematic elements.

This method analyzes the stability of geometric differences across a range of distances. It identifies “breakpoints” where changes occur and “zero-streaks” where the geometry remains stable. Based on this, a prediction score is assigned to specific results.

#### Parameters

Name | Type | Description | Default
--- | --- | --- | ---
relevant_distances | List[float] or np.ndarray | A series of distances (in meters) to be analyzed. Defaults to a range from 0.0 to 3.0 meters with steps of 0.1m. | `None`
thematic_ids | List[ThematicId] | Specific thematic IDs to process. If None, all loaded thematic geometries are used. | `None`
diff_metric | DiffMetric | The metric used to determine differences (e.g., area change). If None, the Aligner’s default `diff_metric` is used. | `None`

#### Returns

Name | Type | Description
--- | --- | ---
| AlignerResult | A result object containing the process results enriched with stability properties and prediction scores.

#### Raises

Name | Type | Description
--- | --- | ---
| ValueError | If provided `thematic_ids` are not found in the loaded data.

#### Notes

The prediction logic follows a four-step process: 1. Series Processing: Calculates results for all specified distances. 2. Metric Calculation: Computes the difference between original and result for each step. 3. Stability Analysis: Identifies streaks where the change is minimal (zero-streaks) or where the metric flips sign (breakpoints). 4. Scoring: Assigns a `PREDICTION_SCORE` to results that fall within a significant stability window.

    graph TD
        A[Input Distances] --> B[Process Series]
        B --> C[Calculate Diffs]
        C --> D[Stability Analysis]
        D --> E{Stable?}
        E -- Yes --> F[Assign Prediction Score]
        E -- No --> G[Mark unstable]
        F --> H[AlignerResult]
        G --> H

#### Examples

    >>> aligner = Aligner(crs="EPSG:31370")
    >>> # Predict using default distance range
    >>> prediction_results = aligner.predict(thematic_ids=["id_1", "id_2"])

### process

    aligner.Aligner.process(
        relevant_distances=None,
        *,
        thematic_ids=None,
        max_workers=None,
    )

Executes the alignment process across multiple relevant distances.

This method iterates through the specified thematic features and calculates the alignment for each provided ‘relevant distance’. It can utilize parallel processing via a ThreadPoolExecutor to speed up calculations.

#### Parameters

Name | Type | Description | Default
--- | --- | --- | ---
relevant_distances | Iterable[float] | A series of distances (in meters) to use for the alignment logic. This parameter is mandatory. | `None`
thematic_ids | List[ThematicId] | A specific list of IDs to process. If None, all thematic features currently loaded in the aligner will be processed. | `None`
max_workers | int | The number of threads for parallel execution. If -1, execution is serial. If None, the Aligner’s default `max_workers` is used. | `None`

#### Returns

Name | Type | Description
--- | --- | ---
| [AlignerResult][] | An object containing the structured results, accessible by thematic ID and relevant distance.

#### Raises

Name | Type | Description
--- | --- | ---
| ValueError | If `relevant_distances` is None or empty. If any provided `thematic_ids` are not found in the loaded thematic data.

#### Notes

The processing flow involves distributing geometry-distance pairs across available worker threads:

    graph TD
        Start[Process Call] --> Check{Valid IDs?}
        Check -- Yes --> Parallel{max_workers > 0?}
        Parallel -- Yes --> Workers[ThreadPoolExecutor]
        Parallel -- No --> Serial[Single Thread]
        Workers --> Proc[Geometry Processor]
        Serial --> Proc
        Proc --> Result[AlignerResult]

#### Examples

    >>> aligner.process(relevant_distances=[0, 0.5, 1.0], max_workers=4)
    <brdr.aligner.AlignerResult object at ...>