1414import geopandas as gpd
1515import xarray as xr
1616
17- from revrt .models .routing import validate_routing_options
17+ from revrt .models .routing import (
18+ validate_driver_configs ,
19+ validate_routing_options ,
20+ validate_transition_cost_input ,
21+ )
1822from revrt .routing .cli .utilities import routing_layer_mover
1923from revrt .routing .base import RoutingScenario
2024from revrt .routing .processing import BatchRouteProcessor
@@ -64,11 +68,12 @@ def __init__(
6468 route_points : pandas.DataFrame
6569 DataFrame defining the points to be routed. This DataFrame
6670 should contain route definitions to be transformed and
67- passed down to the Rust routing algorithm. Route values for
68- polarity and voltage must be provided either as one shared
69- `polarity` / `voltage` pair that applies to every routing
70- option, or as a full per-option set of
71- `polarity_<option>` / `voltage_<option>` columns.
71+ passed down to the Rust routing algorithm. Provide route
72+ values for polarity and voltage when cost multipliers or
73+ transition costs depend on them. Values may be supplied as
74+ one shared ``polarity`` / ``voltage`` pair or as a full
75+ per-option set of ``polarity_<option>`` /
76+ ``voltage_<option>`` columns.
7277 out_fp : path-like
7378 Path to output file where computed routes will be saved.
7479 This file will be checked for existing routes to avoid
@@ -82,15 +87,27 @@ def __init__(
8287 path to JSON/JSON5 file containing this dictionary. See the
8388 description of
8489 :func:`revrt.routing.cli.point_to_point.compute_lcp_routes`
85- for more details.
90+ for more details. By default, ``None``.
91+ drivers : dict, optional
92+ Optional driver-rule configuration keyed by routing option.
93+ By default, ``None``.
94+ transition_costs : dict, optional
95+ Optional transition-cost configuration between routing
96+ options. A voltage/polarity-dependent cost requires matching
97+ values for every affected routing option.
98+ By default, ``None``.
8699 """
87100 self .cost_fpath = cost_fpath
88101 self .out_fp = Path (out_fp )
89102 self .transmission_config = transmission_config
90- self .drivers = drivers
91- self .transition_costs = transition_costs
92103 self ._input_route_points = route_points
93104 self ._routing_options = RoutingOptions (routing_options )
105+ self .drivers = validate_driver_configs (
106+ drivers , self ._routing_options .routing_options
107+ )
108+ self .transition_costs = validate_transition_cost_input (
109+ transition_costs , self ._routing_options .routing_options
110+ )
94111
95112 @cached_property
96113 def route_points (self ):
@@ -169,10 +186,18 @@ def __iter__(self):
169186 pv_by_option = pv_by_option ,
170187 transmission_config = self .transmission_config ,
171188 )
189+ transition_costs = _resolve_transition_costs (
190+ self .transition_costs , pv_by_option
191+ )
172192 route_definitions , route_attrs = (
173193 self ._convert_to_route_definitions (routes )
174194 )
175- yield route_options , route_definitions , route_attrs
195+ yield (
196+ route_options ,
197+ transition_costs ,
198+ route_definitions ,
199+ route_attrs ,
200+ )
176201
177202 @property
178203 def _paths_to_compute (self ):
@@ -330,12 +355,17 @@ def _run_all_lcp_batches(
330355 """Run LCP routing for all batches of routes and save results"""
331356 out_fp = Path (out_fp )
332357 save_paths = out_fp .suffix .lower () == ".gpkg"
333- for route_options , route_definitions , route_attrs in routes_to_compute :
358+ for (
359+ route_options ,
360+ transition_costs ,
361+ route_definitions ,
362+ route_attrs ,
363+ ) in routes_to_compute :
334364 scenario = RoutingScenario (
335365 cost_fpath = cost_fpath ,
336366 routing_options = route_options ,
337367 drivers = routes_to_compute .drivers ,
338- transition_costs = routes_to_compute . transition_costs ,
368+ transition_costs = transition_costs ,
339369 tracked_layers = tracked_layers ,
340370 invalid_costs_block_routing = invalid_costs_block_routing ,
341371 algorithm = algorithm ,
@@ -427,6 +457,110 @@ def _handle_maybe_missing_column(points, option, base_col):
427457 return points
428458
429459
460+ def _resolve_transition_costs (transition_costs , pv_by_option ):
461+ """Resolve transition-cost mappings from route values"""
462+ if transition_costs is None :
463+ return None
464+
465+ resolved = {}
466+ if "default" in transition_costs :
467+ resolved ["default" ] = _resolve_transition_cost_value (
468+ transition_costs ["default" ],
469+ pv_by_option ,
470+ list (pv_by_option ),
471+ context = "transition_costs.default" ,
472+ )
473+
474+ if "pairwise" in transition_costs :
475+ resolved ["pairwise" ] = []
476+ for rule in transition_costs ["pairwise" ]:
477+ options = rule ["between" ]
478+ resolved ["pairwise" ].append (
479+ {
480+ "between" : options ,
481+ "cost" : _resolve_transition_cost_value (
482+ rule ["cost" ],
483+ pv_by_option ,
484+ options ,
485+ context = (
486+ "transition_costs.pairwise cost between "
487+ f"{ options [0 ]!r} and { options [1 ]!r} "
488+ ),
489+ ),
490+ }
491+ )
492+
493+ return resolved
494+
495+
496+ def _resolve_transition_cost_value (value , pv_by_option , options , context ):
497+ """Resolve one scalar or voltage/polarity transition-cost value"""
498+ if not isinstance (value , dict ):
499+ return value
500+
501+ option_values = {
502+ option : _normalized_transition_values (pv_by_option [option ], option )
503+ for option in options
504+ }
505+ unique_values = set (option_values .values ())
506+ if len (unique_values ) != 1 :
507+ received = ", " .join (
508+ f"{ option } (polarity={ polarity !r} , voltage={ voltage !r} )"
509+ for option , (voltage , polarity ) in option_values .items ()
510+ )
511+ msg = (
512+ f"{ context } requires matching voltage and polarity across "
513+ f"routing options. Received: { received } "
514+ )
515+ raise revrtConfigurationError (msg )
516+
517+ voltage , polarity = next (iter (unique_values ))
518+ try :
519+ resolved = value [voltage ]
520+ except KeyError as error :
521+ msg = (
522+ f"{ context } has no cost for voltage { voltage !r} . Available "
523+ f"voltages: { list (value )} "
524+ )
525+ raise revrtKeyError (msg ) from error
526+
527+ if not isinstance (resolved , dict ):
528+ return resolved
529+
530+ try :
531+ return resolved [polarity ]
532+ except KeyError as error :
533+ msg = (
534+ f"{ context } has no cost for voltage { voltage !r} and polarity "
535+ f"{ polarity !r} . Available polarities: { list (resolved )} "
536+ )
537+ raise revrtKeyError (msg ) from error
538+
539+
540+ def _normalized_transition_values (values , option ):
541+ """tuple[str, str]: Normalized values for one routing option"""
542+ polarity = values .get (_POLARITY )
543+ voltage = values .get (_VOLTAGE )
544+ unknowns = {None , "None" , "unknown" }
545+ if polarity in unknowns or voltage in unknowns :
546+ msg = (
547+ "voltage/polarity-dependent transition costs require known "
548+ f"values for routing option { option !r} "
549+ )
550+ raise revrtConfigurationError (msg )
551+
552+ try :
553+ voltage = str (int (voltage ))
554+ except (TypeError , ValueError , OverflowError ) as error :
555+ msg = (
556+ "voltage/polarity-dependent transition costs require an integer "
557+ f"voltage for routing option { option !r} , received { voltage !r} "
558+ )
559+ raise revrtConfigurationError (msg ) from error
560+
561+ return voltage , str (polarity )
562+
563+
430564def update_multipliers (
431565 layers , polarity , voltage , routing_option , transmission_config
432566):
0 commit comments