GenPlanner

GenPlanner is the main orchestration class of the library. It prepares input territory geometry, applies preprocessing (roads, exclusions, existing zones), and executes the zoning pipeline.

The planner operates in a projected local CRS (UTM), but all outputs are returned in the original CRS of the input territory.

See also:


Conceptual pipeline

High-level workflow:

  1. Prepare territory geometry.

  2. Apply exclusions (optional).

  3. Split by roads (optional).

  4. Integrate existing territorial zones (optional).

  5. Balance target ratios.

  6. Build relation matrix.

  7. Run territorial optimization.

  8. (Optional) Split zones into blocks.

The heavy geometric split is performed by the internal split_polygon engine.


Minimal example

import geopandas as gpd
from genplanner import GenPlanner
from genplanner.zones import basic_func_zone

territory = gpd.read_file("territory.geojson")

gp = GenPlanner(features_gdf=territory)

zones_gdf, roads_gdf = gp.features2terr_zones(
    funczone=basic_func_zone
)

Generate blocks instead of zone-level polygons:

blocks_gdf, roads_gdf = gp.features2terr_zones2blocks(
    funczone=basic_func_zone
)

Relation matrix

The relation_matrix argument controls adjacency rules between zones. See Zone Relations for details.

Supported values:

  • "default" — built from predefined forbidden neighborhood rules

  • "empty" — no adjacency constraints

  • ZoneRelationMatrix instance

  • None — treated as "default"

Example:

from genplanner import ZoneRelationMatrix
from genplanner.zones import default_terr_zones

res = default_terr_zones.residential_terr
ind = default_terr_zones.industrial_terr

mat = ZoneRelationMatrix.from_pairs(
    zones=[res, ind],
    forbidden=[(res, ind)],
)

zones, roads = gp.features2terr_zones(
    relation_matrix=mat
)

Fixed zone anchor points

Both zoning methods accept an optional argument:

terr_zones_fix_points: GeoDataFrame

It allows you to constrain spatial allocation by specifying anchor points for particular territorial zones.

Each row represents a fixed spatial hint for a zone.

Requirements:

  • Geometry must be Point

  • Column fixed_zone must be present

  • fixed_zone values must be TerritoryZone objects present in funczone.zones_ratio

All fixed points must lie inside the working territory.

Example:

import geopandas as gpd
from shapely.geometry import Point
from genplanner.zones import default_terr_zones

fix_points = gpd.GeoDataFrame(
    {
        "fixed_zone": [
            default_terr_zones.residential_terr,
            default_terr_zones.business_terr,
        ],
        "geometry": [
            Point(30.1, 59.9),
            Point(30.2, 59.91),
        ],
    },
    crs="EPSG:4326",
)

zones, roads = gp.features2terr_zones(
    terr_zones_fix_points=fix_points
)

Existing zones integration

If existing_terr_zones is provided:

  • Territory fragments sufficiently covered by existing zones are merged.

  • Static fix points are derived automatically.

  • Target ratios are rebalanced.

  • Final output always includes existing zones.

See internal validation logic for details.


Full example with roads, exclusions, existing zones and fixed anchors

import geopandas as gpd
from shapely.geometry import Point
from genplanner import GenPlanner, default_terr_zones

roads = gpd.read_file("roads.geojson")
territory = gpd.read_file("territory.geojson")
exclude = gpd.read_file("exclude_features.geojson")
existing_zones = gpd.read_file("existing_zones.geojson")

terr_zone_map = {
    "industrial": default_terr_zones.industrial_terr,
    "special": default_terr_zones.special_terr,
}

existing_zones["territory_zone"] = (
    existing_zones["territory_zone"]
    .map(terr_zone_map)
)

# Optional: add anchor points for certain zones
fix_points = gpd.GeoDataFrame(
    {
        "fixed_zone": [
            default_terr_zones.industrial_terr,
        ],
        "geometry": [
            Point(30.15, 59.92),
        ],
    },
    crs=territory.crs,
)

gp = GenPlanner(
    features_gdf=territory,
    roads_gdf=roads,
    exclude_gdf=exclude,
    existing_terr_zones=existing_zones,
    parallel=True,
)

new_zones, new_roads = gp.features2terr_zones2blocks(
    terr_zones_fix_points=fix_points
)

Visualize:

m = new_zones.explore(column="territory_zone", tiles="cartodb positron")
new_roads.explore(m=m, column="road_lvl")

Parallel execution

If parallel=True (default), the planner uses ProcessPoolExecutor for downstream tasks.

  • Worker count defaults to cpu_count - 1.

  • Falls back to single-thread mode if only one CPU is available.


Logging

If rust_write_logs=True:

  • A directory named after run_name is created.

  • Optimizer logs are written per split attempt.

Useful for debugging zoning instability.


Returned objects

Both zoning methods return:

(zones_or_blocks_gdf, roads_gdf)

  • All geometries are returned in the original input CRS.

  • roads_width column is always present.

  • road_lvl column describes road type.


API reference