feat: 自动布线优先使用空闲路径

dev
Zhaowenlong 3 weeks ago
parent 4b40e2f81d
commit be007035be

@ -25,6 +25,7 @@ DEFAULT_OPTIONS = {
"terminal_exit_length": 20.0,
"lane_axis": "auto",
"lane_spacing": 10.0,
"segment_reuse_penalty": 200.0,
# 线槽网络相关参数。
"use_routing_network": True,
"network_entry_max_distance": 1000.0,
@ -791,6 +792,8 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non
end_key,
bend_penalty=float(opts.get("bend_penalty", 0.0) or 0.0),
kind_cost_factors=opts.get("carrier_kind_cost_factors", {}),
segment_usage_costs=opts.get("segment_usage_costs", {}),
segment_reuse_penalty=float(opts.get("segment_reuse_penalty", 0.0) or 0.0),
)
path_keys = path_result.get("path", []) if isinstance(path_result, dict) else []
if not path_keys:
@ -1424,18 +1427,22 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la
missing_endpoint_uuids = set()
lane_indexes_by_pair = {}
lane_indexes_by_segment = {}
segment_usage_costs = {}
def add_status(status):
key = str(status or "").strip() or "Unknown"
report["route_status_counts"][key] = report["route_status_counts"].get(key, 0) + 1
def create_route(route_lane_index, item, start_terminal, end_terminal, endpoint_metadata):
route_options = dict(options or {})
if isinstance(item, dict) and "__segment_usage_costs" in item:
route_options["segment_usage_costs"] = item.get("__segment_usage_costs", {})
return route_eplan_connection_between_terminals(
doc,
start_terminal,
end_terminal,
route_index=route_lane_index,
options=options,
options=route_options,
wire_uuid=_wire_item_value(item, "wire_id", "wire_uuid", "id"),
wire_label=_wire_item_value(item, "wire_label", "wire_mark"),
net_uuid=_wire_item_value(item, "net_uuid"),
@ -1492,7 +1499,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la
}
result = create_route(
route_lane_index,
item,
dict(item, __segment_usage_costs=segment_usage_costs),
start_terminal,
end_terminal,
endpoint_metadata,
@ -1507,7 +1514,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la
try:
result = create_route(
final_lane_index,
item,
dict(item, __segment_usage_costs=segment_usage_costs),
start_terminal,
end_terminal,
endpoint_metadata,
@ -1548,6 +1555,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la
lane_indexes_by_segment.get(segment_key, 0),
int(result.get("lane", {}).get("index", 0) or 0) + 1,
)
segment_usage_costs[segment_key] = segment_usage_costs.get(segment_key, 0) + 1
if result["route_status"] == "CollisionWarning":
report["collision_warnings"] += 1
add_status(result["route_status"])

@ -2270,7 +2270,23 @@ def _carrier_track_payload(carrier):
}
def shortest_path_with_carriers(network, start_key, end_key, bend_penalty=0.0, kind_cost_factors=None):
def _segment_usage_key(carrier, from_key, to_key):
carrier_name = getattr(carrier, "Name", "") if carrier is not None else ""
return (
carrier_name,
tuple(sorted((from_key, to_key))),
)
def shortest_path_with_carriers(
network,
start_key,
end_key,
bend_penalty=0.0,
kind_cost_factors=None,
segment_usage_costs=None,
segment_reuse_penalty=0.0,
):
"""Dijkstra search with a small extra cost when route direction changes."""
if start_key is None or end_key is None:
return None
@ -2343,8 +2359,17 @@ def shortest_path_with_carriers(network, start_key, end_key, bend_penalty=0.0, k
bend_cost = 0.0
if previous_direction is not None and direction != previous_direction:
bend_cost = float(bend_penalty or 0.0)
usage_cost = 0.0
if segment_usage_costs:
usage_count = float(segment_usage_costs.get(_segment_usage_key(carrier, key, next_key), 0.0) or 0.0)
usage_cost = usage_count * float(segment_reuse_penalty or 0.0)
next_state = (next_key, direction)
next_cost = cost + float(weight) * _carrier_cost_factor(carrier, kind_cost_factors) + bend_cost
next_cost = (
cost
+ float(weight) * _carrier_cost_factor(carrier, kind_cost_factors)
+ bend_cost
+ usage_cost
)
if next_cost < distances.get(next_state, float("inf")):
distances[next_state] = next_cost
previous[next_state] = {

@ -1967,6 +1967,74 @@ class AutoRoutingTest(unittest.TestCase):
self.assertTrue(any(abs(point.x - 10.0) <= 0.001 for point in second_wire.Points[1:-1]))
self.assertFalse(all(abs(point.x) <= 0.001 for point in second_wire.Points[1:-1]))
def test_route_eplan_connections_prefers_unused_alternate_route_segments(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
_terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0))
_terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0))
_terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0))
_terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0))
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(100, 0, 20)],
label="Direct Duct",
project_uuid="project-1",
kind="WireDuct",
)
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(0, 40, 20)],
label="Left Bridge",
project_uuid="project-1",
kind="WireDuct",
)
routing_network.create_route_carrier(
doc,
[app.Vector(0, 40, 20), app.Vector(100, 40, 20)],
label="Alternate Duct",
project_uuid="project-1",
kind="WireDuct",
)
routing_network.create_route_carrier(
doc,
[app.Vector(100, 40, 20), app.Vector(100, 0, 20)],
label="Right Bridge",
project_uuid="project-1",
kind="WireDuct",
)
payload = {
"project_uuid": "project-1",
"wires": [
{
"wire_id": "wire-a",
"start_terminal_uuid": "terminal-start-a",
"end_terminal_uuid": "terminal-end-a",
},
{
"wire_id": "wire-b",
"start_terminal_uuid": "terminal-start-b",
"end_terminal_uuid": "terminal-end-b",
},
],
}
report = auto_routing.route_eplan_connections_from_payload(doc, payload)
first_labels = [
segment["carrier"]["label"]
for segment in report["routes"][0]["route_track"]["segments"]
]
second_labels = [
segment["carrier"]["label"]
for segment in report["routes"][1]["route_track"]["segments"]
]
self.assertIn("Direct Duct", first_labels)
self.assertIn("Alternate Duct", second_labels)
self.assertNotIn("Direct Duct", second_labels)
def test_route_eplan_connections_report_includes_collision_samples(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()

Loading…
Cancel
Save