From 7e2bc8f57c1b31f4f7002c9795ff7919f8637e52 Mon Sep 17 00:00:00 2001 From: aprabou Date: Sun, 19 Jul 2026 23:48:10 -0700 Subject: [PATCH 1/3] Add left-hand traffic demo maps and validation tests. Provide small OpenDRIVE LHT fixtures and tests for centerline flow, faster/slower lane adjacency, geometry integrity, and driving placement. --- assets/maps/demo/lht/01_two_lane_oneway.xodr | 33 +++ .../maps/demo/lht/02_three_lane_speeds.xodr | 38 +++ assets/maps/demo/lht/03_two_way.xodr | 41 +++ tests/domains/driving/test_lht_driving.py | 98 +++++++ tests/formats/opendrive/test_lht.py | 252 ++++++++++++++++++ 5 files changed, 462 insertions(+) create mode 100644 assets/maps/demo/lht/01_two_lane_oneway.xodr create mode 100644 assets/maps/demo/lht/02_three_lane_speeds.xodr create mode 100644 assets/maps/demo/lht/03_two_way.xodr create mode 100644 tests/domains/driving/test_lht_driving.py create mode 100644 tests/formats/opendrive/test_lht.py diff --git a/assets/maps/demo/lht/01_two_lane_oneway.xodr b/assets/maps/demo/lht/01_two_lane_oneway.xodr new file mode 100644 index 000000000..ebd47a6e8 --- /dev/null +++ b/assets/maps/demo/lht/01_two_lane_oneway.xodr @@ -0,0 +1,33 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ diff --git a/assets/maps/demo/lht/02_three_lane_speeds.xodr b/assets/maps/demo/lht/02_three_lane_speeds.xodr new file mode 100644 index 000000000..4a0a765f5 --- /dev/null +++ b/assets/maps/demo/lht/02_three_lane_speeds.xodr @@ -0,0 +1,38 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+ diff --git a/assets/maps/demo/lht/03_two_way.xodr b/assets/maps/demo/lht/03_two_way.xodr new file mode 100644 index 000000000..2c3c2535a --- /dev/null +++ b/assets/maps/demo/lht/03_two_way.xodr @@ -0,0 +1,41 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+
+ diff --git a/tests/domains/driving/test_lht_driving.py b/tests/domains/driving/test_lht_driving.py new file mode 100644 index 000000000..8804b550b --- /dev/null +++ b/tests/domains/driving/test_lht_driving.py @@ -0,0 +1,98 @@ +"""Driving-domain smoke tests on left-hand traffic demo maps.""" + +import inspect +from pathlib import Path + +import pytest + +from tests.utils import compileScenic, sampleEgo, sampleScene + +pytestmark = pytest.mark.filterwarnings( + "ignore::scenic.formats.opendrive.OpenDriveWarning" +) + +LHT = Path(__file__).resolve().parents[3] / "assets" / "maps" / "demo" / "lht" + +template = inspect.cleandoc( + """ + param map = r'{map}' + param map_options = dict(useCache=False) + model scenic.domains.driving.model + """ +) + +basicScenario = inspect.cleandoc( + """ + lane = Uniform(*network.lanes) + ego = new Car in lane + new Car on visible lane.centerline + """ +) + +fasterLaneScenario = inspect.cleandoc( + """ + candidates = [lane for lane in network.lanes if lane.sections[0]._fasterLane] + require len(candidates) > 0 + lane = Uniform(*candidates) + ego = new Car in lane + other = new Car in lane.sections[0].fasterLane.lane + """ +) + +slowerLaneScenario = inspect.cleandoc( + """ + candidates = [lane for lane in network.lanes if lane.sections[0]._slowerLane] + require len(candidates) > 0 + lane = Uniform(*candidates) + ego = new Car in lane + other = new Car in lane.sections[0].slowerLane.lane + """ +) + + +def _compile(path, code): + preamble = template.format(map=path) + return compileScenic(preamble + "\n" + code, mode2D=True) + + +@pytest.mark.parametrize( + "name", + [ + "01_two_lane_oneway.xodr", + "02_three_lane_speeds.xodr", + "03_two_way.xodr", + ], +) +def test_lht_driving_scenario_compiles(name): + scenario = _compile(LHT / name, basicScenario) + sampleScene(scenario, maxIterations=1000) + + +@pytest.mark.parametrize( + "name", + [ + "01_two_lane_oneway.xodr", + "02_three_lane_speeds.xodr", + "03_two_way.xodr", + ], +) +def test_lht_faster_lane_placement(name): + """Cars can be placed on a lane and on that lane's fasterLane.""" + scenario = _compile(LHT / name, fasterLaneScenario) + ego = sampleEgo(scenario, maxIterations=1000) + assert ego.lane is not None + + +@pytest.mark.parametrize( + "name", + [ + "01_two_lane_oneway.xodr", + "02_three_lane_speeds.xodr", + "03_two_way.xodr", + ], +) +def test_lht_slower_lane_placement(name): + """Cars can be placed on a lane and on that lane's slowerLane.""" + scenario = _compile(LHT / name, slowerLaneScenario) + ego = sampleEgo(scenario, maxIterations=1000) + assert ego.lane is not None diff --git a/tests/formats/opendrive/test_lht.py b/tests/formats/opendrive/test_lht.py new file mode 100644 index 000000000..5af442d1e --- /dev/null +++ b/tests/formats/opendrive/test_lht.py @@ -0,0 +1,252 @@ +"""Left-hand traffic OpenDRIVE maps — centerlines and faster/slower lanes. + +These tests exercise ``assets/maps/demo/lht`` only. They do not change the +OpenDRIVE parser: with the current Scenic loader, one-way LHT maps that place +driving lanes on positive OpenDRIVE IDs still yield a consistent traffic flow +and median/curb faster-slower relationships after centerlines are oriented for +driving. + +Absolute OpenDRIVE LHT conventions (positive IDs travel +s, oncoming to the +driver's right) are intentionally not asserted here. +""" + +from pathlib import Path + +import pytest + +from scenic.domains.driving.roads import Network + +pytestmark = pytest.mark.filterwarnings( + "ignore::scenic.formats.opendrive.OpenDriveWarning" +) + +DEMO = Path(__file__).resolve().parents[3] / "assets" / "maps" / "demo" / "lht" + + +def load(name): + return Network.fromFile(DEMO / name, useCache=False) + + +def section_by_id(network, od_id): + for road in network.roads: + for sec in road.sections: + if od_id in sec.lanesByOpenDriveID: + return sec.lanesByOpenDriveID[od_id] + raise AssertionError(f"no lane section with OpenDRIVE id {od_id}") + + +def centerline_delta(lane): + pts = lane.centerline.points + return pts[-1][0] - pts[0][0], pts[-1][1] - pts[0][1] + + +def carriageway_lanes(road): + """Yield each same-direction lane group on the road.""" + if road.forwardLanes: + yield road.forwardLanes + if road.backwardLanes: + yield road.backwardLanes + + +# --------------------------------------------------------------------------- +# Maps load +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "name, n_lanes, oneway", + [ + ("01_two_lane_oneway.xodr", 2, True), + ("02_three_lane_speeds.xodr", 3, True), + ("03_two_way.xodr", 4, False), + ], +) +def test_lht_maps_load(name, n_lanes, oneway): + network = load(name) + assert len(network.roads) == 1 + road = network.roads[0] + assert len(network.lanes) == n_lanes + assert road.is1Way is oneway + + +# --------------------------------------------------------------------------- +# Centerlines follow the flow of traffic +# --------------------------------------------------------------------------- + + +def test_centerlines_agree_within_each_carriageway(): + """Every lane in a carriageway points the same way (traffic flow).""" + for name in ( + "01_two_lane_oneway.xodr", + "02_three_lane_speeds.xodr", + "03_two_way.xodr", + ): + road = load(name).roads[0] + for group in carriageway_lanes(road): + deltas = [centerline_delta(lane) for lane in group.lanes] + assert deltas, name + # Same sign in the dominant axis (these maps are axis-aligned on x). + signs = [1 if dx > 0 else -1 if dx < 0 else 0 for dx, _ in deltas] + assert 0 not in signs, f"{name}: zero-length centerline" + assert len(set(signs)) == 1, f"{name}: mixed centerline directions {deltas}" + + +def test_centerline_matches_lane_orientation(): + """Lane orientation at a centerline point matches the network flow direction.""" + network = load("02_three_lane_speeds.xodr") + for lane in network.lanes: + pt = lane.centerline.pointAlongBy(0.5, normalized=True) + dirs = network.nominalDirectionsAt(pt) + assert pytest.approx(lane.orientation[pt]) in dirs + assert lane.containsPoint(pt) + + +def test_two_way_carriageways_oppose_each_other(): + """Opposite carriageways on a two-way road travel opposite directions.""" + road = load("03_two_way.xodr").roads[0] + assert road.forwardLanes and road.backwardLanes + fwd = centerline_delta(road.forwardLanes.lanes[0])[0] + bwd = centerline_delta(road.backwardLanes.lanes[0])[0] + assert fwd * bwd < 0 + + +# --------------------------------------------------------------------------- +# fasterLane / slowerLane point toward median / curb +# --------------------------------------------------------------------------- + + +def test_two_lane_faster_slower_toward_median(): + """Curb lane's faster neighbor is the median lane; median's slower is curb.""" + network = load("01_two_lane_oneway.xodr") + median = section_by_id(network, 1) + curb = section_by_id(network, 2) + + assert curb._fasterLane is median + assert median._slowerLane is curb + assert curb._slowerLane is None + assert median._fasterLane is None + + assert curb.fasterLane is median + assert median.slowerLane is curb + + +def test_three_lane_faster_slower_chain(): + """Curb → middle → median is the faster direction; reverse is slower.""" + network = load("02_three_lane_speeds.xodr") + median = section_by_id(network, 1) + middle = section_by_id(network, 2) + curb = section_by_id(network, 3) + + assert curb._fasterLane is middle + assert middle._slowerLane is curb + assert middle._fasterLane is median + assert median._slowerLane is middle + assert curb._slowerLane is None + assert median._fasterLane is None + + +def test_two_way_faster_slower_each_carriageway(): + """Each side of a two-way LHT road: outer/curb is slower, inner/median faster.""" + network = load("03_two_way.xodr") + + # Positive (left) carriageway: +2 curb, +1 median + pos_curb = section_by_id(network, 2) + pos_med = section_by_id(network, 1) + assert pos_curb._fasterLane is pos_med + assert pos_med._slowerLane is pos_curb + assert pos_med._fasterLane is None + + # Negative (right) carriageway: -2 curb, -1 median + neg_curb = section_by_id(network, -2) + neg_med = section_by_id(network, -1) + assert neg_curb._fasterLane is neg_med + assert neg_med._slowerLane is neg_curb + assert neg_med._fasterLane is None + + +def test_faster_slower_are_same_direction_neighbors(): + """faster/slower never point across the median to oncoming traffic.""" + for name in ( + "01_two_lane_oneway.xodr", + "02_three_lane_speeds.xodr", + "03_two_way.xodr", + ): + network = load(name) + for road in network.roads: + for sec in road.sections: + for lane in sec.lanes: + if lane._fasterLane: + assert lane._fasterLane.isForward == lane.isForward + if lane._slowerLane: + assert lane._slowerLane.isForward == lane.isForward + + +# --------------------------------------------------------------------------- +# Geometry integrity +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "name", + [ + "01_two_lane_oneway.xodr", + "02_three_lane_speeds.xodr", + "03_two_way.xodr", + ], +) +def test_lane_polygons_valid_and_contain_centerline(name): + network = load(name) + for lane in network.lanes: + assert not lane.polygon.is_empty + assert lane.polygon.is_valid + for sec in lane.sections: + assert sec.containsRegion(sec.centerline, tolerance=0.5) + assert sec.containsRegion(sec.leftEdge, tolerance=0.5) + assert sec.containsRegion(sec.rightEdge, tolerance=0.5) + + +# --------------------------------------------------------------------------- +# Adjacency helpers (shiftedBy, opposite group, edge rejection) +# --------------------------------------------------------------------------- + + +def test_shifted_by_matches_lane_to_left_right(): + network = load("02_three_lane_speeds.xodr") + for sec in network.roads[0].sections[0].lanes: + if sec._laneToLeft: + assert sec.shiftedBy(1) is sec._laneToLeft + assert sec._laneToLeft.shiftedBy(-1) is sec + if sec._laneToRight: + assert sec.shiftedBy(-1) is sec._laneToRight + assert sec._laneToRight.shiftedBy(1) is sec + + +def test_faster_slower_are_left_or_right_neighbors(): + network = load("02_three_lane_speeds.xodr") + for sec in network.roads[0].sections[0].lanes: + neighbors = {sec._laneToLeft, sec._laneToRight} - {None} + if sec._fasterLane: + assert sec._fasterLane in neighbors + if sec._slowerLane: + assert sec._slowerLane in neighbors + + +def test_two_way_opposite_lane_groups(): + road = load("03_two_way.xodr").roads[0] + assert road.forwardLanes._opposite is road.backwardLanes + assert road.backwardLanes._opposite is road.forwardLanes + assert road.forwardLanes.opposite is road.backwardLanes + assert road.backwardLanes.opposite is road.forwardLanes + + +def test_edge_lanes_reject_missing_faster_or_slower(): + from scenic.core.distributions import RejectionException + + network = load("01_two_lane_oneway.xodr") + median = section_by_id(network, 1) + curb = section_by_id(network, 2) + + with pytest.raises(RejectionException): + _ = median.fasterLane + with pytest.raises(RejectionException): + _ = curb.slowerLane From 5f5fe9553f86a17a7bc487d7bf2c33fa526623ac Mon Sep 17 00:00:00 2001 From: aprabou Date: Mon, 20 Jul 2026 00:01:09 -0700 Subject: [PATCH 2/3] stronger coverage with tests- dropped weaker ones --- tests/domains/driving/test_lht_driving.py | 90 ++++---- tests/formats/opendrive/test_lht.py | 267 +++++++--------------- 2 files changed, 135 insertions(+), 222 deletions(-) diff --git a/tests/domains/driving/test_lht_driving.py b/tests/domains/driving/test_lht_driving.py index 8804b550b..9dcf97cba 100644 --- a/tests/domains/driving/test_lht_driving.py +++ b/tests/domains/driving/test_lht_driving.py @@ -1,11 +1,11 @@ -"""Driving-domain smoke tests on left-hand traffic demo maps.""" +"""Driving-domain tests on left-hand traffic demo maps.""" import inspect from pathlib import Path import pytest -from tests.utils import compileScenic, sampleEgo, sampleScene +from tests.utils import compileScenic, sampleScene pytestmark = pytest.mark.filterwarnings( "ignore::scenic.formats.opendrive.OpenDriveWarning" @@ -13,6 +13,12 @@ LHT = Path(__file__).resolve().parents[3] / "assets" / "maps" / "demo" / "lht" +ALL_MAPS = ( + "01_two_lane_oneway.xodr", + "02_three_lane_speeds.xodr", + "03_two_way.xodr", +) + template = inspect.cleandoc( """ param map = r'{map}' @@ -25,7 +31,7 @@ """ lane = Uniform(*network.lanes) ego = new Car in lane - new Car on visible lane.centerline + follower = new Car on visible lane.centerline """ ) @@ -55,44 +61,44 @@ def _compile(path, code): return compileScenic(preamble + "\n" + code, mode2D=True) -@pytest.mark.parametrize( - "name", - [ - "01_two_lane_oneway.xodr", - "02_three_lane_speeds.xodr", - "03_two_way.xodr", - ], -) -def test_lht_driving_scenario_compiles(name): - scenario = _compile(LHT / name, basicScenario) - sampleScene(scenario, maxIterations=1000) - - -@pytest.mark.parametrize( - "name", - [ - "01_two_lane_oneway.xodr", - "02_three_lane_speeds.xodr", - "03_two_way.xodr", - ], -) +def _sample_two_car_scene(name, scenario_code): + scenario = _compile(LHT / name, scenario_code) + scene = sampleScene(scenario, maxIterations=1000) + assert len(scene.objects) == 2 + ego, other = scene.objects + assert ego is scene.egoObject + return ego, other + + +@pytest.mark.parametrize("name", ALL_MAPS) +def test_lht_basic_car_placement(name): + """Ego and a second car can be placed on the same sampled lane.""" + ego, follower = _sample_two_car_scene(name, basicScenario) + + assert ego.lane is follower.lane + assert ego.laneSection.lane is follower.laneSection.lane + assert follower.lane.containsPoint(follower.position) + + +@pytest.mark.parametrize("name", ALL_MAPS) def test_lht_faster_lane_placement(name): - """Cars can be placed on a lane and on that lane's fasterLane.""" - scenario = _compile(LHT / name, fasterLaneScenario) - ego = sampleEgo(scenario, maxIterations=1000) - assert ego.lane is not None - - -@pytest.mark.parametrize( - "name", - [ - "01_two_lane_oneway.xodr", - "02_three_lane_speeds.xodr", - "03_two_way.xodr", - ], -) + """Ego sits on a slower lane; other is placed on its fasterLane neighbor.""" + ego, other = _sample_two_car_scene(name, fasterLaneScenario) + + ego_sec = ego.laneSection + assert ego_sec._fasterLane is not None + assert other.lane is ego_sec.fasterLane.lane + assert other.lane is not ego.lane + assert other.laneSection.lane is ego_sec.fasterLane.lane + + +@pytest.mark.parametrize("name", ALL_MAPS) def test_lht_slower_lane_placement(name): - """Cars can be placed on a lane and on that lane's slowerLane.""" - scenario = _compile(LHT / name, slowerLaneScenario) - ego = sampleEgo(scenario, maxIterations=1000) - assert ego.lane is not None + """Ego sits on a faster lane; other is placed on its slowerLane neighbor.""" + ego, other = _sample_two_car_scene(name, slowerLaneScenario) + + ego_sec = ego.laneSection + assert ego_sec._slowerLane is not None + assert other.lane is ego_sec.slowerLane.lane + assert other.lane is not ego.lane + assert other.laneSection.lane is ego_sec.slowerLane.lane diff --git a/tests/formats/opendrive/test_lht.py b/tests/formats/opendrive/test_lht.py index 5af442d1e..ecadaceb3 100644 --- a/tests/formats/opendrive/test_lht.py +++ b/tests/formats/opendrive/test_lht.py @@ -1,19 +1,10 @@ -"""Left-hand traffic OpenDRIVE maps — centerlines and faster/slower lanes. - -These tests exercise ``assets/maps/demo/lht`` only. They do not change the -OpenDRIVE parser: with the current Scenic loader, one-way LHT maps that place -driving lanes on positive OpenDRIVE IDs still yield a consistent traffic flow -and median/curb faster-slower relationships after centerlines are oriented for -driving. - -Absolute OpenDRIVE LHT conventions (positive IDs travel +s, oncoming to the -driver's right) are intentionally not asserted here. -""" +"""Left-hand traffic OpenDRIVE maps — centerlines and faster/slower lanes.""" from pathlib import Path import pytest +from scenic.core.distributions import RejectionException from scenic.domains.driving.roads import Network pytestmark = pytest.mark.filterwarnings( @@ -22,6 +13,12 @@ DEMO = Path(__file__).resolve().parents[3] / "assets" / "maps" / "demo" / "lht" +ALL_MAPS = ( + "01_two_lane_oneway.xodr", + "02_three_lane_speeds.xodr", + "03_two_way.xodr", +) + def load(name): return Network.fromFile(DEMO / name, useCache=False) @@ -40,17 +37,28 @@ def centerline_delta(lane): return pts[-1][0] - pts[0][0], pts[-1][1] - pts[0][1] -def carriageway_lanes(road): - """Yield each same-direction lane group on the road.""" +def carriageway_groups(road): if road.forwardLanes: yield road.forwardLanes if road.backwardLanes: yield road.backwardLanes -# --------------------------------------------------------------------------- -# Maps load -# --------------------------------------------------------------------------- +def assert_faster_slower(network, sec, *, faster_id, slower_id): + faster = section_by_id(network, faster_id) if faster_id else None + slower = section_by_id(network, slower_id) if slower_id else None + assert sec._fasterLane is faster + assert sec._slowerLane is slower + if faster_id is None: + with pytest.raises(RejectionException): + _ = sec.fasterLane + else: + assert sec.fasterLane is faster + if slower_id is None: + with pytest.raises(RejectionException): + _ = sec.slowerLane + else: + assert sec.slowerLane is slower @pytest.mark.parametrize( @@ -61,192 +69,91 @@ def carriageway_lanes(road): ("03_two_way.xodr", 4, False), ], ) -def test_lht_maps_load(name, n_lanes, oneway): +def test_lht_network_structure(name, n_lanes, oneway): network = load(name) - assert len(network.roads) == 1 road = network.roads[0] + + assert len(network.roads) == 1 assert len(network.lanes) == n_lanes assert road.is1Way is oneway - -# --------------------------------------------------------------------------- -# Centerlines follow the flow of traffic -# --------------------------------------------------------------------------- - - -def test_centerlines_agree_within_each_carriageway(): - """Every lane in a carriageway points the same way (traffic flow).""" - for name in ( - "01_two_lane_oneway.xodr", - "02_three_lane_speeds.xodr", - "03_two_way.xodr", - ): - road = load(name).roads[0] - for group in carriageway_lanes(road): - deltas = [centerline_delta(lane) for lane in group.lanes] - assert deltas, name - # Same sign in the dominant axis (these maps are axis-aligned on x). - signs = [1 if dx > 0 else -1 if dx < 0 else 0 for dx, _ in deltas] - assert 0 not in signs, f"{name}: zero-length centerline" - assert len(set(signs)) == 1, f"{name}: mixed centerline directions {deltas}" - - -def test_centerline_matches_lane_orientation(): - """Lane orientation at a centerline point matches the network flow direction.""" - network = load("02_three_lane_speeds.xodr") for lane in network.lanes: - pt = lane.centerline.pointAlongBy(0.5, normalized=True) - dirs = network.nominalDirectionsAt(pt) - assert pytest.approx(lane.orientation[pt]) in dirs - assert lane.containsPoint(pt) + assert not lane.polygon.is_empty + assert lane.polygon.is_valid + for sec in lane.sections: + assert sec.containsRegion(sec.centerline, tolerance=0.5) + assert sec.containsRegion(sec.leftEdge, tolerance=0.5) + assert sec.containsRegion(sec.rightEdge, tolerance=0.5) + +@pytest.mark.parametrize("name", ALL_MAPS) +def test_centerlines_follow_traffic_flow(name): + """Centerlines point with traffic; orientation matches flow at lane midpoints.""" + road = load(name).roads[0] -def test_two_way_carriageways_oppose_each_other(): - """Opposite carriageways on a two-way road travel opposite directions.""" - road = load("03_two_way.xodr").roads[0] - assert road.forwardLanes and road.backwardLanes - fwd = centerline_delta(road.forwardLanes.lanes[0])[0] - bwd = centerline_delta(road.backwardLanes.lanes[0])[0] - assert fwd * bwd < 0 + for group in carriageway_groups(road): + deltas = [centerline_delta(lane) for lane in group.lanes] + signs = [1 if dx > 0 else -1 if dx < 0 else 0 for dx, _ in deltas] + assert 0 not in signs, f"{name}: zero-length centerline" + assert len(set(signs)) == 1, f"{name}: mixed centerline directions {deltas}" + for lane in group.lanes: + pt = lane.centerline.pointAlongBy(0.5, normalized=True) + dirs = road.network.nominalDirectionsAt(pt) + assert pytest.approx(lane.orientation[pt]) in dirs + assert lane.containsPoint(pt) -# --------------------------------------------------------------------------- -# fasterLane / slowerLane point toward median / curb -# --------------------------------------------------------------------------- + if not road.is1Way: + fwd = centerline_delta(road.forwardLanes.lanes[0])[0] + bwd = centerline_delta(road.backwardLanes.lanes[0])[0] + assert fwd * bwd < 0 def test_two_lane_faster_slower_toward_median(): - """Curb lane's faster neighbor is the median lane; median's slower is curb.""" network = load("01_two_lane_oneway.xodr") - median = section_by_id(network, 1) - curb = section_by_id(network, 2) - - assert curb._fasterLane is median - assert median._slowerLane is curb - assert curb._slowerLane is None - assert median._fasterLane is None - - assert curb.fasterLane is median - assert median.slowerLane is curb + assert_faster_slower(network, section_by_id(network, 2), faster_id=1, slower_id=None) + assert_faster_slower(network, section_by_id(network, 1), faster_id=None, slower_id=2) def test_three_lane_faster_slower_chain(): - """Curb → middle → median is the faster direction; reverse is slower.""" network = load("02_three_lane_speeds.xodr") - median = section_by_id(network, 1) - middle = section_by_id(network, 2) - curb = section_by_id(network, 3) - - assert curb._fasterLane is middle - assert middle._slowerLane is curb - assert middle._fasterLane is median - assert median._slowerLane is middle - assert curb._slowerLane is None - assert median._fasterLane is None + assert_faster_slower(network, section_by_id(network, 3), faster_id=2, slower_id=None) + assert_faster_slower(network, section_by_id(network, 2), faster_id=1, slower_id=3) + assert_faster_slower(network, section_by_id(network, 1), faster_id=None, slower_id=2) def test_two_way_faster_slower_each_carriageway(): - """Each side of a two-way LHT road: outer/curb is slower, inner/median faster.""" network = load("03_two_way.xodr") + assert_faster_slower(network, section_by_id(network, 2), faster_id=1, slower_id=None) + assert_faster_slower(network, section_by_id(network, 1), faster_id=None, slower_id=2) + assert_faster_slower(network, section_by_id(network, -2), faster_id=-1, slower_id=None) + assert_faster_slower(network, section_by_id(network, -1), faster_id=None, slower_id=-2) - # Positive (left) carriageway: +2 curb, +1 median - pos_curb = section_by_id(network, 2) - pos_med = section_by_id(network, 1) - assert pos_curb._fasterLane is pos_med - assert pos_med._slowerLane is pos_curb - assert pos_med._fasterLane is None - - # Negative (right) carriageway: -2 curb, -1 median - neg_curb = section_by_id(network, -2) - neg_med = section_by_id(network, -1) - assert neg_curb._fasterLane is neg_med - assert neg_med._slowerLane is neg_curb - assert neg_med._fasterLane is None - - -def test_faster_slower_are_same_direction_neighbors(): - """faster/slower never point across the median to oncoming traffic.""" - for name in ( - "01_two_lane_oneway.xodr", - "02_three_lane_speeds.xodr", - "03_two_way.xodr", - ): - network = load(name) - for road in network.roads: - for sec in road.sections: - for lane in sec.lanes: - if lane._fasterLane: - assert lane._fasterLane.isForward == lane.isForward - if lane._slowerLane: - assert lane._slowerLane.isForward == lane.isForward - - -# --------------------------------------------------------------------------- -# Geometry integrity -# --------------------------------------------------------------------------- - -@pytest.mark.parametrize( - "name", - [ - "01_two_lane_oneway.xodr", - "02_three_lane_speeds.xodr", - "03_two_way.xodr", - ], -) -def test_lane_polygons_valid_and_contain_centerline(name): +@pytest.mark.parametrize("name", ALL_MAPS) +def test_lane_adjacency_helpers(name): + """shiftedBy, faster/slower neighbors, and same-direction constraints.""" network = load(name) - for lane in network.lanes: - assert not lane.polygon.is_empty - assert lane.polygon.is_valid - for sec in lane.sections: - assert sec.containsRegion(sec.centerline, tolerance=0.5) - assert sec.containsRegion(sec.leftEdge, tolerance=0.5) - assert sec.containsRegion(sec.rightEdge, tolerance=0.5) - - -# --------------------------------------------------------------------------- -# Adjacency helpers (shiftedBy, opposite group, edge rejection) -# --------------------------------------------------------------------------- - - -def test_shifted_by_matches_lane_to_left_right(): - network = load("02_three_lane_speeds.xodr") - for sec in network.roads[0].sections[0].lanes: - if sec._laneToLeft: - assert sec.shiftedBy(1) is sec._laneToLeft - assert sec._laneToLeft.shiftedBy(-1) is sec - if sec._laneToRight: - assert sec.shiftedBy(-1) is sec._laneToRight - assert sec._laneToRight.shiftedBy(1) is sec - - -def test_faster_slower_are_left_or_right_neighbors(): - network = load("02_three_lane_speeds.xodr") - for sec in network.roads[0].sections[0].lanes: - neighbors = {sec._laneToLeft, sec._laneToRight} - {None} - if sec._fasterLane: - assert sec._fasterLane in neighbors - if sec._slowerLane: - assert sec._slowerLane in neighbors - - -def test_two_way_opposite_lane_groups(): - road = load("03_two_way.xodr").roads[0] - assert road.forwardLanes._opposite is road.backwardLanes - assert road.backwardLanes._opposite is road.forwardLanes - assert road.forwardLanes.opposite is road.backwardLanes - assert road.backwardLanes.opposite is road.forwardLanes - - -def test_edge_lanes_reject_missing_faster_or_slower(): - from scenic.core.distributions import RejectionException - - network = load("01_two_lane_oneway.xodr") - median = section_by_id(network, 1) - curb = section_by_id(network, 2) - - with pytest.raises(RejectionException): - _ = median.fasterLane - with pytest.raises(RejectionException): - _ = curb.slowerLane + for road in network.roads: + for sec in road.sections: + for lane_sec in sec.lanes: + neighbors = {lane_sec._laneToLeft, lane_sec._laneToRight} - {None} + if lane_sec._laneToLeft: + assert lane_sec.shiftedBy(1) is lane_sec._laneToLeft + if lane_sec._laneToLeft.isForward == lane_sec.isForward: + assert lane_sec._laneToLeft.shiftedBy(-1) is lane_sec + if lane_sec._laneToRight: + assert lane_sec.shiftedBy(-1) is lane_sec._laneToRight + if lane_sec._laneToRight.isForward == lane_sec.isForward: + assert lane_sec._laneToRight.shiftedBy(1) is lane_sec + if lane_sec._fasterLane: + assert lane_sec._fasterLane in neighbors + assert lane_sec._fasterLane.isForward == lane_sec.isForward + if lane_sec._slowerLane: + assert lane_sec._slowerLane in neighbors + assert lane_sec._slowerLane.isForward == lane_sec.isForward + + if name == "03_two_way.xodr": + road = network.roads[0] + assert road.forwardLanes._opposite is road.backwardLanes + assert road.backwardLanes.opposite is road.forwardLanes From 1a7900f98820325880e1f2aa3f937d0194611a39 Mon Sep 17 00:00:00 2001 From: aprabou Date: Mon, 20 Jul 2026 00:04:37 -0700 Subject: [PATCH 3/3] quick reformat --- tests/formats/opendrive/test_lht.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/formats/opendrive/test_lht.py b/tests/formats/opendrive/test_lht.py index ecadaceb3..38c686b85 100644 --- a/tests/formats/opendrive/test_lht.py +++ b/tests/formats/opendrive/test_lht.py @@ -126,8 +126,12 @@ def test_two_way_faster_slower_each_carriageway(): network = load("03_two_way.xodr") assert_faster_slower(network, section_by_id(network, 2), faster_id=1, slower_id=None) assert_faster_slower(network, section_by_id(network, 1), faster_id=None, slower_id=2) - assert_faster_slower(network, section_by_id(network, -2), faster_id=-1, slower_id=None) - assert_faster_slower(network, section_by_id(network, -1), faster_id=None, slower_id=-2) + assert_faster_slower( + network, section_by_id(network, -2), faster_id=-1, slower_id=None + ) + assert_faster_slower( + network, section_by_id(network, -1), faster_id=None, slower_id=-2 + ) @pytest.mark.parametrize("name", ALL_MAPS)