tkadkghdlf commited on
Commit
4e5857a
·
verified ·
1 Parent(s): 3daf3a8

Sync DemoService 607dcedd38c0c5513b842dc6f9d1ccec13360b91

Browse files
api/ai-service/app/api/v1/endpoints/route_generation.py CHANGED
@@ -434,12 +434,21 @@ def _run_fast_road_pipeline(
434
  # Explore multiple physical sizes on the same road graph. The UI did
435
  # not request a distance; these are search scales, not length targets.
436
  candidates = []
 
437
  for search_distance_km in (6.0, 11.0, 18.0):
 
 
 
 
 
 
 
438
  candidates.extend(build_candidates(
439
  graph=graph,
440
  route_count=min(6, candidate_pool),
441
  target_distance_m=search_distance_km * 1000.0,
442
  selected_label=selected_label,
 
443
  ))
444
  else:
445
  candidates = build_candidates(
 
434
  # Explore multiple physical sizes on the same road graph. The UI did
435
  # not request a distance; these are search scales, not length targets.
436
  candidates = []
437
+ rank_projections = getattr(module, "rank_road_template_projections", None)
438
  for search_distance_km in (6.0, 11.0, 18.0):
439
+ template_kwargs = {}
440
+ if selected_label == "symbol_star" and callable(rank_projections):
441
+ # Screen every supplied star outline against this road graph;
442
+ # only the best fits enter the expensive shortest-path stage.
443
+ template_kwargs["template_examples"] = rank_projections(
444
+ graph, examples, search_distance_km * 1000.0,
445
+ )
446
  candidates.extend(build_candidates(
447
  graph=graph,
448
  route_count=min(6, candidate_pool),
449
  target_distance_m=search_distance_km * 1000.0,
450
  selected_label=selected_label,
451
+ **template_kwargs,
452
  ))
453
  else:
454
  candidates = build_candidates(
api/ai-service/tests/test_gpx_shape_templates.py CHANGED
@@ -10,6 +10,7 @@ TEMPLATES = ROOT / "router" / "ai" / "data" / "gpx_shape_templates.json"
10
  APPROVED_TEMPLATES = ROOT / "router" / "ai" / "data" / "approved_road_shape_templates.json"
11
  TSHIRT_TEMPLATES = ROOT / "router" / "ai" / "data" / "tshirt_shape_templates.json"
12
  STAR_TEMPLATES = ROOT / "router" / "ai" / "data" / "star_shape_templates.json"
 
13
 
14
 
15
  def test_dog_and_star_gpx_examples_are_normalized_and_closed():
@@ -63,6 +64,31 @@ def test_new_star_gpx_examples_are_available_for_road_generation():
63
  exec(compile(ast.Module(body=[loader], type_ignores=[]), str(script), "exec"), scope)
64
  loaded = scope["_load_gpx_shape_templates"]()
65
  assert all([tuple(point) for point in outline] in loaded["symbol_star"] for outline in outlines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
 
68
  def test_road_checked_examples_are_packaged_for_generation():
 
10
  APPROVED_TEMPLATES = ROOT / "router" / "ai" / "data" / "approved_road_shape_templates.json"
11
  TSHIRT_TEMPLATES = ROOT / "router" / "ai" / "data" / "tshirt_shape_templates.json"
12
  STAR_TEMPLATES = ROOT / "router" / "ai" / "data" / "star_shape_templates.json"
13
+ ALL_STAR_TEMPLATES = ROOT / "router" / "ai" / "data" / "star_shape_templates_all.json"
14
 
15
 
16
  def test_dog_and_star_gpx_examples_are_normalized_and_closed():
 
64
  exec(compile(ast.Module(body=[loader], type_ignores=[]), str(script), "exec"), scope)
65
  loaded = scope["_load_gpx_shape_templates"]()
66
  assert all([tuple(point) for point in outline] in loaded["symbol_star"] for outline in outlines)
67
+ all_outlines = json.loads(ALL_STAR_TEMPLATES.read_text(encoding="utf-8"))["symbol_star"]
68
+ assert len(all_outlines) == 992
69
+ assert all([tuple(point) for point in outline] in loaded["symbol_star"] for outline in all_outlines)
70
+
71
+
72
+ def test_every_star_outline_is_screened_before_road_pathfinding():
73
+ import math
74
+ import networkx as nx
75
+ import numpy as np
76
+ from scipy.spatial import cKDTree
77
+
78
+ script = ROOT / "router" / "ai" / "scripts" / "clip_osm_region_test.py"
79
+ tree = ast.parse(script.read_text(encoding="utf-8-sig"))
80
+ names = {"guided_template_perimeter", "rank_road_template_projections"}
81
+ functions = ast.Module(body=[node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name in names], type_ignores=[])
82
+ scope = {"math": math, "np": np, "cKDTree": cKDTree, "nx": nx, "list": list}
83
+ exec(compile(functions, str(script), "exec"), scope)
84
+ graph = nx.MultiDiGraph()
85
+ for index in range(11):
86
+ for other in range(11):
87
+ graph.add_node(index * 11 + other, x=127 + index * 0.001, y=37.5 + other * 0.001)
88
+ nearby = [(0, 0), (0.02, 0), (0.02, 0.02), (0, 0.02), (0, 0)]
89
+ distant = [(0.4, 0.4), (0.48, 0.4), (0.48, 0.48), (0.4, 0.48), (0.4, 0.4)]
90
+ ranked = scope["rank_road_template_projections"](graph, [distant, nearby], 1000, limit=1)
91
+ assert ranked == [nearby]
92
 
93
 
94
  def test_road_checked_examples_are_packaged_for_generation():
api/ai-service/tests/test_route_generation_recovery.py CHANGED
@@ -157,6 +157,36 @@ def test_shape_priority_searches_multiple_lengths_and_ranks_by_shape(monkeypatch
157
  assert result["target_distance_km"] == 0
158
 
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  def test_shape_priority_uses_guided_outline_for_labels_without_gpx(monkeypatch):
161
  module = SimpleNamespace(
162
  GPX_SHAPE_TEMPLATES={},
 
157
  assert result["target_distance_km"] == 0
158
 
159
 
160
+ def test_star_search_screens_all_outlines_before_road_routing(monkeypatch):
161
+ examples = [[(index, 0), (index, 1), (index + 1, 1), (index, 0)] for index in range(1000)]
162
+ screened = []
163
+ routed = []
164
+
165
+ def screen(_graph, templates, distance):
166
+ screened.append((len(templates), distance))
167
+ return templates[-36:]
168
+
169
+ def candidates(**kwargs):
170
+ routed.append(kwargs["template_examples"])
171
+ return [{"nodes": [1, 2], "length_m": kwargs["target_distance_m"],
172
+ "shape_quality": 0.6, "is_loop": True}]
173
+
174
+ module = SimpleNamespace(
175
+ GPX_SHAPE_TEMPLATES={"symbol_star": examples},
176
+ _build_or_get_walk_graph=lambda *_: (object(), "test"),
177
+ rank_road_template_projections=screen,
178
+ generate_candidate_routes=candidates,
179
+ route_points=lambda *_: [], simplify_coords=lambda points: points,
180
+ )
181
+ monkeypatch.setattr(routes, "_route_nodes_to_coords", lambda _graph, nodes: [[nodes[0], 0], [nodes[1], 1]])
182
+ monkeypatch.setattr(routes, "best_gpx_shape_match", lambda _coords, templates: (0.9, len(templates) - 1))
183
+ routes._run_fast_road_pipeline(module, place=None, center_lat=37, center_lon=127,
184
+ radius_km=5, route_count=2, target_distance_km=6, selected_label="symbol_star", shape_priority=True)
185
+ assert screened == [(1000, 6000), (1000, 11000), (1000, 18000)]
186
+ assert len(routed) == 3
187
+ assert all(len(templates) == 36 and templates[-1] == examples[-1] for templates in routed)
188
+
189
+
190
  def test_shape_priority_uses_guided_outline_for_labels_without_gpx(monkeypatch):
191
  module = SimpleNamespace(
192
  GPX_SHAPE_TEMPLATES={},
router/ai/data/star_shape_templates_all.json ADDED
The diff for this file is too large to render. See raw diff
 
router/ai/scripts/build_star_shape_templates.py CHANGED
@@ -1,6 +1,6 @@
1
  """Package a bounded set of location-free outlines from a multi-track star GPX.
2
 
3
- Usage: python build_star_shape_templates.py STAR_ALL.gpx OUTPUT_JSON
4
  """
5
 
6
  from __future__ import annotations
@@ -17,7 +17,7 @@ from build_gpx_shape_templates import normalized_outline_from_track
17
  MAX_TEMPLATES = 64
18
 
19
 
20
- def build(source: Path) -> dict[str, list[list[list[float]]]]:
21
  outlines = []
22
  seen = set()
23
  root = ET.parse(source).getroot()
@@ -34,14 +34,16 @@ def build(source: Path) -> dict[str, list[list[list[float]]]]:
34
  outlines.append(outline)
35
  if not outlines:
36
  raise ValueError("No star outlines found")
37
- return {"symbol_star": random.Random(42).sample(outlines, min(MAX_TEMPLATES, len(outlines)))}
 
 
38
 
39
 
40
  if __name__ == "__main__":
41
- if len(sys.argv) != 3:
42
- raise SystemExit("Usage: build_star_shape_templates.py STAR_ALL.gpx OUTPUT_JSON")
43
  output = Path(sys.argv[2])
44
- templates = build(Path(sys.argv[1]))
45
  output.parent.mkdir(parents=True, exist_ok=True)
46
  output.write_text(json.dumps(templates, separators=(",", ":")) + "\n", encoding="utf-8")
47
  print({label: len(items) for label, items in templates.items()})
 
1
  """Package a bounded set of location-free outlines from a multi-track star GPX.
2
 
3
+ Usage: python build_star_shape_templates.py STAR_ALL.gpx OUTPUT_JSON [--all]
4
  """
5
 
6
  from __future__ import annotations
 
17
  MAX_TEMPLATES = 64
18
 
19
 
20
+ def build(source: Path, max_templates: int | None = MAX_TEMPLATES) -> dict[str, list[list[list[float]]]]:
21
  outlines = []
22
  seen = set()
23
  root = ET.parse(source).getroot()
 
34
  outlines.append(outline)
35
  if not outlines:
36
  raise ValueError("No star outlines found")
37
+ if max_templates is None:
38
+ return {"symbol_star": outlines}
39
+ return {"symbol_star": random.Random(42).sample(outlines, min(max_templates, len(outlines)))}
40
 
41
 
42
  if __name__ == "__main__":
43
+ if len(sys.argv) not in (3, 4) or (len(sys.argv) == 4 and sys.argv[3] != "--all"):
44
+ raise SystemExit("Usage: build_star_shape_templates.py STAR_ALL.gpx OUTPUT_JSON [--all]")
45
  output = Path(sys.argv[2])
46
+ templates = build(Path(sys.argv[1]), max_templates=None if len(sys.argv) == 4 else MAX_TEMPLATES)
47
  output.parent.mkdir(parents=True, exist_ok=True)
48
  output.write_text(json.dumps(templates, separators=(",", ":")) + "\n", encoding="utf-8")
49
  print({label: len(items) for label, items in templates.items()})
router/ai/scripts/clip_osm_region_test.py CHANGED
@@ -18,6 +18,7 @@ import networkx as nx
18
  import numpy as np
19
  import osmnx as ox
20
  import requests
 
21
  from PIL import Image
22
  import psycopg
23
  from transformers import CLIPModel, CLIPProcessor
@@ -146,7 +147,7 @@ def _load_gpx_shape_templates() -> dict[str, list[list[tuple[float, float]]]]:
146
  data_dir = Path(__file__).resolve().parents[1] / "data"
147
  templates: dict[str, list[list[tuple[float, float]]]] = {"dog": [], "symbol_star": [], "t_shirt": []}
148
  seen: dict[str, set[tuple[tuple[float, float], ...]]] = {label: set() for label in templates}
149
- for filename in ("gpx_shape_templates.json", "approved_road_shape_templates.json", "tshirt_shape_templates.json", "star_shape_templates.json"):
150
  try:
151
  payload = json.loads((data_dir / filename).read_text(encoding="utf-8"))
152
  except (OSError, ValueError, TypeError):
@@ -786,6 +787,53 @@ def guided_template_perimeter(points: list[tuple[float, float]]) -> float:
786
  return max(total, 1e-6)
787
 
788
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
789
  def guided_geo_waypoints(
790
  graph: nx.MultiDiGraph,
791
  label: str,
@@ -2389,6 +2437,7 @@ def generate_candidate_routes(
2389
  target_distance_m: float,
2390
  tolerance_ratio: float = 0.35,
2391
  selected_label: str | None = None,
 
2392
  ) -> list[dict[str, Any]]:
2393
  nodes = list(graph.nodes)
2394
  if len(nodes) < 8:
@@ -2410,10 +2459,10 @@ def generate_candidate_routes(
2410
  candidate_edge_sets: list[set[tuple[int, int]]] = []
2411
 
2412
  # 1) label-guided candidates: project a rough label outline onto the OSM graph first.
2413
- guided_template = guided_waypoints_for_label(selected_label)
2414
  if guided_template:
2415
  guided_attempts = min(max(route_count * LABEL_GUIDED_ATTEMPT_MULTIPLIER, 36), 180)
2416
- examples = GPX_SHAPE_TEMPLATES.get(selected_label, [])
2417
  shuffled_examples = random.sample(examples, len(examples)) if examples else []
2418
  for attempt in range(guided_attempts):
2419
  try:
 
18
  import numpy as np
19
  import osmnx as ox
20
  import requests
21
+ from scipy.spatial import cKDTree
22
  from PIL import Image
23
  import psycopg
24
  from transformers import CLIPModel, CLIPProcessor
 
147
  data_dir = Path(__file__).resolve().parents[1] / "data"
148
  templates: dict[str, list[list[tuple[float, float]]]] = {"dog": [], "symbol_star": [], "t_shirt": []}
149
  seen: dict[str, set[tuple[tuple[float, float], ...]]] = {label: set() for label in templates}
150
+ for filename in ("gpx_shape_templates.json", "approved_road_shape_templates.json", "tshirt_shape_templates.json", "star_shape_templates.json", "star_shape_templates_all.json"):
151
  try:
152
  payload = json.loads((data_dir / filename).read_text(encoding="utf-8"))
153
  except (OSError, ValueError, TypeError):
 
787
  return max(total, 1e-6)
788
 
789
 
790
+ def rank_road_template_projections(
791
+ graph: nx.MultiDiGraph,
792
+ templates: list[list[tuple[float, float]]],
793
+ target_distance_m: float,
794
+ limit: int = 36,
795
+ ) -> list[list[tuple[float, float]]]:
796
+ """Project every outline cheaply, then reserve road pathfinding for the best fits.
797
+
798
+ The score measures how far outline waypoints would snap to walkable road
799
+ nodes at eight orientations. It is a screening score, not a routed result.
800
+ """
801
+ nodes = list(graph.nodes(data=True))
802
+ if not nodes or not templates:
803
+ return []
804
+ center_lat = float(np.median([float(data["y"]) for _, data in nodes]))
805
+ center_lon = float(np.median([float(data["x"]) for _, data in nodes]))
806
+ meters_per_lon = max(1.0, 111_320.0 * math.cos(math.radians(center_lat)))
807
+ road_xy = np.array([
808
+ ((float(data["x"]) - center_lon) * meters_per_lon,
809
+ (center_lat - float(data["y"])) * 111_320.0)
810
+ for _, data in nodes
811
+ ])
812
+ tree = cKDTree(road_xy)
813
+ ranked = []
814
+ for index, template in enumerate(templates):
815
+ body = template[:-1] if len(template) > 1 and template[0] == template[-1] else template
816
+ if len(body) < 4:
817
+ continue
818
+ points = np.asarray(body, dtype=float)
819
+ perimeter = guided_template_perimeter(template)
820
+ points *= target_distance_m / perimeter
821
+ best = float("inf")
822
+ for angle_index in range(8):
823
+ angle = angle_index * math.pi / 4
824
+ cos_angle, sin_angle = math.cos(angle), math.sin(angle)
825
+ rotated = np.column_stack((
826
+ points[:, 0] * cos_angle - points[:, 1] * sin_angle,
827
+ points[:, 0] * sin_angle + points[:, 1] * cos_angle,
828
+ ))
829
+ distances, nearest = tree.query(rotated)
830
+ repeated = 1.0 - len(set(nearest.tolist())) / len(nearest)
831
+ best = min(best, float(np.mean(distances)) + 150.0 * repeated)
832
+ ranked.append((best, index))
833
+ ranked.sort()
834
+ return [templates[index] for _, index in ranked[:limit]]
835
+
836
+
837
  def guided_geo_waypoints(
838
  graph: nx.MultiDiGraph,
839
  label: str,
 
2437
  target_distance_m: float,
2438
  tolerance_ratio: float = 0.35,
2439
  selected_label: str | None = None,
2440
+ template_examples: list[list[tuple[float, float]]] | None = None,
2441
  ) -> list[dict[str, Any]]:
2442
  nodes = list(graph.nodes)
2443
  if len(nodes) < 8:
 
2459
  candidate_edge_sets: list[set[tuple[int, int]]] = []
2460
 
2461
  # 1) label-guided candidates: project a rough label outline onto the OSM graph first.
2462
+ guided_template = template_examples[0] if template_examples else guided_waypoints_for_label(selected_label)
2463
  if guided_template:
2464
  guided_attempts = min(max(route_count * LABEL_GUIDED_ATTEMPT_MULTIPLIER, 36), 180)
2465
+ examples = template_examples if template_examples is not None else GPX_SHAPE_TEMPLATES.get(selected_label, [])
2466
  shuffled_examples = random.sample(examples, len(examples)) if examples else []
2467
  for attempt in range(guided_attempts):
2468
  try: