devagent-physical-engine 0.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. devagent_physical_engine/__init__.py +44 -0
  2. devagent_physical_engine/agent/__init__.py +40 -0
  3. devagent_physical_engine/agent/compiler.py +285 -0
  4. devagent_physical_engine/agent/contracts.py +129 -0
  5. devagent_physical_engine/agent/coordinator.py +108 -0
  6. devagent_physical_engine/agent/critic.py +72 -0
  7. devagent_physical_engine/agent/evidence.py +34 -0
  8. devagent_physical_engine/agent/interpreter.py +179 -0
  9. devagent_physical_engine/agent/planner.py +105 -0
  10. devagent_physical_engine/agent/recovery.py +54 -0
  11. devagent_physical_engine/agent/routing.py +76 -0
  12. devagent_physical_engine/agent/runtime.py +270 -0
  13. devagent_physical_engine/agent/semantic.py +304 -0
  14. devagent_physical_engine/agent/structured.py +423 -0
  15. devagent_physical_engine/ai_cli.py +226 -0
  16. devagent_physical_engine/cli.py +392 -0
  17. devagent_physical_engine/doctor.py +20 -0
  18. devagent_physical_engine/engineering_agent.py +243 -0
  19. devagent_physical_engine/engineering_request.py +630 -0
  20. devagent_physical_engine/execution.py +90 -0
  21. devagent_physical_engine/models.py +143 -0
  22. devagent_physical_engine/operating_envelope.py +120 -0
  23. devagent_physical_engine/optimization/__init__.py +50 -0
  24. devagent_physical_engine/optimization/benchmark.py +122 -0
  25. devagent_physical_engine/optimization/candidates.py +198 -0
  26. devagent_physical_engine/optimization/contracts.py +235 -0
  27. devagent_physical_engine/optimization/evaluator.py +107 -0
  28. devagent_physical_engine/optimization/evidence.py +53 -0
  29. devagent_physical_engine/optimization/experience.py +105 -0
  30. devagent_physical_engine/optimization/measured.py +125 -0
  31. devagent_physical_engine/optimization/optimizer.py +215 -0
  32. devagent_physical_engine/optimization/orchestrator.py +155 -0
  33. devagent_physical_engine/physical_campaign.py +413 -0
  34. devagent_physical_engine/physical_evidence.py +214 -0
  35. devagent_physical_engine/physical_motion.py +196 -0
  36. devagent_physical_engine/planning.py +80 -0
  37. devagent_physical_engine/preexecution_contract.py +65 -0
  38. devagent_physical_engine/provider_adapters/__init__.py +22 -0
  39. devagent_physical_engine/provider_adapters/anthropic.py +112 -0
  40. devagent_physical_engine/provider_adapters/common.py +187 -0
  41. devagent_physical_engine/provider_adapters/factory.py +20 -0
  42. devagent_physical_engine/provider_adapters/gemini.py +126 -0
  43. devagent_physical_engine/provider_adapters/openai.py +95 -0
  44. devagent_physical_engine/provider_qualification.py +268 -0
  45. devagent_physical_engine/providers.py +94 -0
  46. devagent_physical_engine/qualification.py +44 -0
  47. devagent_physical_engine/qualification_cli.py +195 -0
  48. devagent_physical_engine/qualification_harness.py +917 -0
  49. devagent_physical_engine/robot_platform.py +411 -0
  50. devagent_physical_engine/robots.py +76 -0
  51. devagent_physical_engine/ros2/__init__.py +35 -0
  52. devagent_physical_engine/ros2/acceptance.py +324 -0
  53. devagent_physical_engine/ros2/commands.py +175 -0
  54. devagent_physical_engine/ros2/doctor.py +116 -0
  55. devagent_physical_engine/ros2/fk_probe.py +83 -0
  56. devagent_physical_engine/ros2/frame_alignment.py +61 -0
  57. devagent_physical_engine/ros2/gazebo_world.py +125 -0
  58. devagent_physical_engine/ros2/joint_state_recorder.py +64 -0
  59. devagent_physical_engine/ros2/measured_motion.py +233 -0
  60. devagent_physical_engine/ros2/moveit_scene.py +121 -0
  61. devagent_physical_engine/ros2/preexecution.py +113 -0
  62. devagent_physical_engine/ros2/qualification.py +81 -0
  63. devagent_physical_engine/ros2/qualification_v10.py +252 -0
  64. devagent_physical_engine/ros2/scene_probe.py +219 -0
  65. devagent_physical_engine/ros2/state_validity_probe.py +125 -0
  66. devagent_physical_engine/ros2/tf_probe.py +51 -0
  67. devagent_physical_engine/ros2/trajectory.py +188 -0
  68. devagent_physical_engine/ros2/ur5e.py +59 -0
  69. devagent_physical_engine/ros2/ur5e_adapter.py +349 -0
  70. devagent_physical_engine/ros2/ur5e_v10_adapter.py +292 -0
  71. devagent_physical_engine/setup_profile.py +356 -0
  72. devagent_physical_engine/simulation.py +32 -0
  73. devagent_physical_engine/simulation_platform.py +269 -0
  74. devagent_physical_engine/trajectory_qualification.py +201 -0
  75. devagent_physical_engine/twin.py +939 -0
  76. devagent_physical_engine/twin_builder.py +309 -0
  77. devagent_physical_engine/twin_materialization.py +404 -0
  78. devagent_physical_engine/verification.py +46 -0
  79. devagent_physical_engine-0.10.0.dist-info/METADATA +315 -0
  80. devagent_physical_engine-0.10.0.dist-info/RECORD +84 -0
  81. devagent_physical_engine-0.10.0.dist-info/WHEEL +5 -0
  82. devagent_physical_engine-0.10.0.dist-info/entry_points.txt +3 -0
  83. devagent_physical_engine-0.10.0.dist-info/licenses/NOTICE +2 -0
  84. devagent_physical_engine-0.10.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+
6
+ from .models import Resource, TaskGraph, WorldState
7
+ from .simulation import SimulationBackend
8
+ from .verification import DeterministicVerifier, VerificationIssue
9
+
10
+
11
+ class ExecutionMode(str, Enum):
12
+ SIMULATION = "simulation"
13
+ SHADOW = "shadow"
14
+ REAL = "real"
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class ExecutionReport:
19
+ completed: bool
20
+ executed_tasks: list[str] = field(default_factory=list)
21
+ blocked_task: str | None = None
22
+ issues: list[VerificationIssue] = field(default_factory=list)
23
+
24
+
25
+ class ExecutionSupervisor:
26
+ """Fail-closed executor.
27
+
28
+ Simulation advances the supplied simulated world. Shadow mode advances an isolated copy so
29
+ hypothetical expected effects never mutate live observed state. V0.1 locks real execution until
30
+ hardware qualification and an explicit promotion gate exist.
31
+ """
32
+
33
+ def __init__(self, verifier: DeterministicVerifier | None = None) -> None:
34
+ self.verifier = verifier or DeterministicVerifier()
35
+
36
+ def execute(
37
+ self,
38
+ graph: TaskGraph,
39
+ resources: list[Resource],
40
+ world: WorldState,
41
+ backend: SimulationBackend,
42
+ mode: ExecutionMode = ExecutionMode.SIMULATION,
43
+ ) -> ExecutionReport:
44
+ graph_result = self.verifier.verify_graph(graph, resources)
45
+ if not graph_result.passed:
46
+ return ExecutionReport(False, issues=graph_result.issues)
47
+
48
+ if mode is ExecutionMode.REAL:
49
+ return ExecutionReport(
50
+ False,
51
+ issues=[
52
+ VerificationIssue(
53
+ "real_execution_locked",
54
+ detail="requires qualified hardware adapter and promotion gate",
55
+ )
56
+ ],
57
+ )
58
+
59
+ evaluation_world = (
60
+ world
61
+ if mode is ExecutionMode.SIMULATION
62
+ else WorldState(facts=set(world.facts), values=dict(world.values))
63
+ )
64
+
65
+ report = ExecutionReport(False)
66
+ for task in graph.topological_order():
67
+ preconditions = self.verifier.verify_runtime_preconditions(task, evaluation_world)
68
+ if not preconditions.passed:
69
+ report.blocked_task = task.task_id
70
+ report.issues.extend(preconditions.issues)
71
+ return report
72
+
73
+ step = backend.run_task(task, evaluation_world)
74
+ constraints = self.verifier.verify_simulation_constraints(
75
+ task, set(step.satisfied_constraints)
76
+ )
77
+ if not step.success or not constraints.passed:
78
+ report.blocked_task = task.task_id
79
+ report.issues.extend(constraints.issues)
80
+ if not step.success and not constraints.issues:
81
+ report.issues.append(
82
+ VerificationIssue("simulation_failed", task.task_id, step.detail)
83
+ )
84
+ return report
85
+
86
+ evaluation_world.apply_effects(task.contract.expected_effects)
87
+ report.executed_tasks.append(task.task_id)
88
+
89
+ report.completed = True
90
+ return report
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any, Iterable
6
+
7
+
8
+ class Capability(str, Enum):
9
+ MOVE = "move"
10
+ PICK = "pick"
11
+ PLACE = "place"
12
+ WAIT = "wait"
13
+ INSPECT = "inspect"
14
+ NAVIGATE = "navigate"
15
+ TRANSPORT = "transport"
16
+ DOCK = "dock"
17
+ TRANSFER = "transfer"
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class Goal:
22
+ goal_id: str
23
+ action: str
24
+ object_id: str
25
+ source: str
26
+ destination: str
27
+
28
+ def __post_init__(self) -> None:
29
+ for name, value in (("goal_id", self.goal_id), ("action", self.action), ("object_id", self.object_id), ("source", self.source), ("destination", self.destination)):
30
+ if not value.strip():
31
+ raise ValueError(f"empty_{name}")
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class Resource:
36
+ resource_id: str
37
+ resource_type: str
38
+ capabilities: frozenset[Capability]
39
+ metadata: dict[str, Any] = field(default_factory=dict)
40
+
41
+ def __post_init__(self) -> None:
42
+ if not self.resource_id.strip():
43
+ raise ValueError("empty_resource_id")
44
+ if not self.resource_type.strip():
45
+ raise ValueError("empty_resource_type")
46
+
47
+ def supports(self, capability: Capability) -> bool:
48
+ return capability in self.capabilities
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class ActionContract:
53
+ action: Capability
54
+ resource_id: str
55
+ object_id: str | None = None
56
+ source: str | None = None
57
+ destination: str | None = None
58
+ preconditions: tuple[str, ...] = ()
59
+ expected_effects: tuple[str, ...] = ()
60
+ constraints: tuple[str, ...] = ()
61
+ timeout_ms: int = 10_000
62
+
63
+ def __post_init__(self) -> None:
64
+ if not self.resource_id.strip():
65
+ raise ValueError("empty_resource_id")
66
+ if self.timeout_ms <= 0:
67
+ raise ValueError("invalid_timeout")
68
+ for name, values in (("precondition", self.preconditions), ("effect", self.expected_effects), ("constraint", self.constraints)):
69
+ if any(not value.strip() for value in values):
70
+ raise ValueError(f"empty_{name}")
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class TaskNode:
75
+ task_id: str
76
+ contract: ActionContract
77
+ depends_on: tuple[str, ...] = ()
78
+
79
+ def __post_init__(self) -> None:
80
+ if not self.task_id.strip():
81
+ raise ValueError("empty_task_id")
82
+
83
+
84
+ @dataclass(frozen=True, slots=True)
85
+ class TaskGraph:
86
+ goal_id: str
87
+ tasks: tuple[TaskNode, ...]
88
+
89
+ def __post_init__(self) -> None:
90
+ if not self.goal_id.strip():
91
+ raise ValueError("empty_goal_id")
92
+ if not self.tasks:
93
+ raise ValueError("empty_task_graph")
94
+
95
+ def validate_structure(self) -> tuple[str, ...]:
96
+ errors: list[str] = []
97
+ ids = [task.task_id for task in self.tasks]
98
+ if len(ids) != len(set(ids)):
99
+ errors.append("duplicate_task_id")
100
+ known = set(ids)
101
+ for task in self.tasks:
102
+ for dependency in task.depends_on:
103
+ if dependency not in known:
104
+ errors.append(f"unknown_dependency:{task.task_id}:{dependency}")
105
+ if dependency == task.task_id:
106
+ errors.append(f"self_dependency:{task.task_id}")
107
+ return tuple(errors)
108
+
109
+ def topological_order(self) -> tuple[TaskNode, ...]:
110
+ errors = self.validate_structure()
111
+ if errors:
112
+ raise ValueError(";".join(errors))
113
+ tasks_by_id = {task.task_id: task for task in self.tasks}
114
+ indegree = {task.task_id: 0 for task in self.tasks}
115
+ children: dict[str, list[str]] = {task.task_id: [] for task in self.tasks}
116
+ for task in self.tasks:
117
+ for dependency in task.depends_on:
118
+ indegree[task.task_id] += 1
119
+ children[dependency].append(task.task_id)
120
+ ready = [task.task_id for task in self.tasks if indegree[task.task_id] == 0]
121
+ ordered: list[TaskNode] = []
122
+ while ready:
123
+ current = ready.pop(0)
124
+ ordered.append(tasks_by_id[current])
125
+ for child in children[current]:
126
+ indegree[child] -= 1
127
+ if indegree[child] == 0:
128
+ ready.append(child)
129
+ if len(ordered) != len(self.tasks):
130
+ raise ValueError("cyclic_task_graph")
131
+ return tuple(ordered)
132
+
133
+
134
+ @dataclass(slots=True)
135
+ class WorldState:
136
+ facts: set[str] = field(default_factory=set)
137
+ values: dict[str, Any] = field(default_factory=dict)
138
+
139
+ def has_all(self, required: Iterable[str]) -> bool:
140
+ return all(item in self.facts for item in required)
141
+
142
+ def apply_effects(self, effects: Iterable[str]) -> None:
143
+ self.facts.update(effects)
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from math import isfinite
5
+ from typing import Sequence
6
+
7
+ from .physical_campaign import PhysicalCaseResult
8
+
9
+
10
+ class OperatingEnvelopeError(ValueError):
11
+ pass
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class ObservedEnvelope:
16
+ """Observed one-dimensional boundary; never an interpolated safety guarantee."""
17
+
18
+ sweep_group: str
19
+ parameter: str
20
+ total_observations: int
21
+ successful_observations: int
22
+ observed_success_min: float | None
23
+ observed_success_max: float | None
24
+ nearest_failure_below: float | None
25
+ nearest_failure_above: float | None
26
+ all_oracle_backed: bool
27
+ unsafe_false_passes: int
28
+
29
+ @property
30
+ def usable_for_qualification(self) -> bool:
31
+ return (
32
+ self.total_observations > 0
33
+ and self.successful_observations > 0
34
+ and self.all_oracle_backed
35
+ and self.unsafe_false_passes == 0
36
+ )
37
+
38
+ def to_dict(self) -> dict:
39
+ return {**asdict(self), "usable_for_qualification": self.usable_for_qualification}
40
+
41
+
42
+ class OperatingEnvelopeAnalyzer:
43
+ """Compute only boundaries directly observed in explicit single-axis sweeps."""
44
+
45
+ def analyze(
46
+ self,
47
+ results: Sequence[PhysicalCaseResult],
48
+ *,
49
+ sweep_group: str,
50
+ parameter: str,
51
+ ) -> ObservedEnvelope:
52
+ if not sweep_group.strip() or not parameter.strip():
53
+ raise OperatingEnvelopeError("sweep_identity_required")
54
+ selected = [
55
+ item
56
+ for item in results
57
+ if item.case.sweep_group == sweep_group
58
+ and item.case.sweep_parameter == parameter
59
+ ]
60
+ if not selected:
61
+ raise OperatingEnvelopeError("sweep_observations_not_found")
62
+
63
+ observations: list[tuple[float, PhysicalCaseResult]] = []
64
+ for item in selected:
65
+ raw = item.case.parameters.get(parameter)
66
+ if isinstance(raw, bool) or not isinstance(raw, (int, float)):
67
+ raise OperatingEnvelopeError("sweep_parameter_must_be_numeric")
68
+ value = float(raw)
69
+ if not isfinite(value):
70
+ raise OperatingEnvelopeError("sweep_parameter_non_finite")
71
+ observations.append((value, item))
72
+ observations.sort(key=lambda pair: pair[0])
73
+
74
+ successes = [value for value, item in observations if item.actual_success]
75
+ success_min = min(successes) if successes else None
76
+ success_max = max(successes) if successes else None
77
+ below = None
78
+ above = None
79
+ if success_min is not None:
80
+ failures_below = [
81
+ value
82
+ for value, item in observations
83
+ if not item.actual_success and value < success_min
84
+ ]
85
+ below = max(failures_below) if failures_below else None
86
+ if success_max is not None:
87
+ failures_above = [
88
+ value
89
+ for value, item in observations
90
+ if not item.actual_success and value > success_max
91
+ ]
92
+ above = min(failures_above) if failures_above else None
93
+
94
+ return ObservedEnvelope(
95
+ sweep_group=sweep_group,
96
+ parameter=parameter,
97
+ total_observations=len(observations),
98
+ successful_observations=len(successes),
99
+ observed_success_min=success_min,
100
+ observed_success_max=success_max,
101
+ nearest_failure_below=below,
102
+ nearest_failure_above=above,
103
+ all_oracle_backed=all(item.case.oracle_backed for _, item in observations),
104
+ unsafe_false_passes=sum(item.unsafe_false_pass for _, item in observations),
105
+ )
106
+
107
+ def discover(self, results: Sequence[PhysicalCaseResult]) -> tuple[ObservedEnvelope, ...]:
108
+ groups = sorted(
109
+ {
110
+ (item.case.sweep_group, item.case.sweep_parameter)
111
+ for item in results
112
+ if item.case.sweep_group is not None
113
+ and item.case.sweep_parameter is not None
114
+ }
115
+ )
116
+ return tuple(
117
+ self.analyze(results, sweep_group=group, parameter=parameter)
118
+ for group, parameter in groups
119
+ if group is not None and parameter is not None
120
+ )
@@ -0,0 +1,50 @@
1
+ from .benchmark import OptimizationBenchmarkCase, OptimizationBenchmarkCaseResult, OptimizationBenchmarkRunner, OptimizationBenchmarkSummary
2
+ from .candidates import AgentCandidateGenerator, CandidateGenerationError, CandidateSource, StaticCandidateSource
3
+ from .contracts import (
4
+ CandidateEvaluation,
5
+ CandidateMetrics,
6
+ ObjectiveWeights,
7
+ OptimizationConfig,
8
+ OptimizationProfileName,
9
+ OptimizationResult,
10
+ OptimizationThresholds,
11
+ PlanCandidate,
12
+ )
13
+ from .evaluator import CandidateMetricEvaluator, DeterministicMetricEvaluator
14
+ from .evidence import optimization_evidence, optimization_evidence_hash
15
+ from .experience import ExperienceStore
16
+ from .measured import MeasuredCandidateMetricEvaluator, RawSimulationMetrics, SimulationMetricsBackend
17
+ from .optimizer import MultiObjectiveOptimizer, OptimizationError, pareto_front
18
+ from .orchestrator import AdaptiveOptimizationEngine, AdaptiveOptimizationError
19
+
20
+ __all__ = [
21
+ "AdaptiveOptimizationEngine",
22
+ "AdaptiveOptimizationError",
23
+ "AgentCandidateGenerator",
24
+ "CandidateGenerationError",
25
+ "CandidateEvaluation",
26
+ "CandidateMetricEvaluator",
27
+ "CandidateMetrics",
28
+ "CandidateSource",
29
+ "DeterministicMetricEvaluator",
30
+ "ExperienceStore",
31
+ "MeasuredCandidateMetricEvaluator",
32
+ "MultiObjectiveOptimizer",
33
+ "ObjectiveWeights",
34
+ "OptimizationBenchmarkCase",
35
+ "OptimizationBenchmarkCaseResult",
36
+ "OptimizationBenchmarkRunner",
37
+ "OptimizationBenchmarkSummary",
38
+ "OptimizationConfig",
39
+ "OptimizationError",
40
+ "OptimizationProfileName",
41
+ "OptimizationResult",
42
+ "OptimizationThresholds",
43
+ "PlanCandidate",
44
+ "RawSimulationMetrics",
45
+ "SimulationMetricsBackend",
46
+ "StaticCandidateSource",
47
+ "optimization_evidence",
48
+ "optimization_evidence_hash",
49
+ "pareto_front",
50
+ ]
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from statistics import fmean
5
+ from typing import Iterable
6
+
7
+ from .orchestrator import AdaptiveOptimizationEngine
8
+ from ..models import Goal, Resource, WorldState
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class OptimizationBenchmarkCase:
13
+ case_id: str
14
+ engine: AdaptiveOptimizationEngine
15
+ goal: Goal
16
+ resources: tuple[Resource, ...]
17
+ world: WorldState
18
+
19
+ def __post_init__(self) -> None:
20
+ if not self.case_id.strip():
21
+ raise ValueError("benchmark_case_id_required")
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class OptimizationBenchmarkCaseResult:
26
+ case_id: str
27
+ passed: bool
28
+ selected_candidate_id: str | None
29
+ comparatively_optimized: bool
30
+ quality_warning_count: int
31
+ cycle_time_s: float | None
32
+ min_clearance_m: float | None
33
+ failure_code: str | None = None
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class OptimizationBenchmarkSummary:
38
+ cases: tuple[OptimizationBenchmarkCaseResult, ...]
39
+
40
+ @property
41
+ def total(self) -> int:
42
+ return len(self.cases)
43
+
44
+ @property
45
+ def passed(self) -> int:
46
+ return sum(1 for case in self.cases if case.passed)
47
+
48
+ @property
49
+ def success_rate(self) -> float:
50
+ return self.passed / self.total if self.total else 0.0
51
+
52
+ @property
53
+ def comparative_rate(self) -> float:
54
+ passed = [case for case in self.cases if case.passed]
55
+ return (
56
+ sum(1 for case in passed if case.comparatively_optimized) / len(passed)
57
+ if passed
58
+ else 0.0
59
+ )
60
+
61
+ @property
62
+ def warning_rate(self) -> float:
63
+ passed = [case for case in self.cases if case.passed]
64
+ return (
65
+ sum(1 for case in passed if case.quality_warning_count > 0) / len(passed)
66
+ if passed
67
+ else 0.0
68
+ )
69
+
70
+ @property
71
+ def average_cycle_time_s(self) -> float | None:
72
+ values = [case.cycle_time_s for case in self.cases if case.cycle_time_s is not None]
73
+ return fmean(values) if values else None
74
+
75
+ @property
76
+ def average_min_clearance_m(self) -> float | None:
77
+ values = [case.min_clearance_m for case in self.cases if case.min_clearance_m is not None]
78
+ return fmean(values) if values else None
79
+
80
+
81
+ class OptimizationBenchmarkRunner:
82
+ """Run deterministic optimization scenarios and aggregate behavioral metrics."""
83
+
84
+ def run(
85
+ self, cases: Iterable[OptimizationBenchmarkCase]
86
+ ) -> OptimizationBenchmarkSummary:
87
+ results: list[OptimizationBenchmarkCaseResult] = []
88
+ for case in cases:
89
+ try:
90
+ result = case.engine.optimize(
91
+ goal=case.goal,
92
+ resources=list(case.resources),
93
+ world=case.world,
94
+ )
95
+ except Exception as exc:
96
+ results.append(
97
+ OptimizationBenchmarkCaseResult(
98
+ case_id=case.case_id,
99
+ passed=False,
100
+ selected_candidate_id=None,
101
+ comparatively_optimized=False,
102
+ quality_warning_count=0,
103
+ cycle_time_s=None,
104
+ min_clearance_m=None,
105
+ failure_code=f"optimization_failed:{type(exc).__name__}",
106
+ )
107
+ )
108
+ continue
109
+
110
+ results.append(
111
+ OptimizationBenchmarkCaseResult(
112
+ case_id=case.case_id,
113
+ passed=True,
114
+ selected_candidate_id=result.selected.candidate.candidate_id,
115
+ comparatively_optimized=result.comparatively_optimized,
116
+ quality_warning_count=len(result.quality_warnings),
117
+ cycle_time_s=result.selected.metrics.cycle_time_s,
118
+ min_clearance_m=result.selected.metrics.min_clearance_m,
119
+ failure_code=None,
120
+ )
121
+ )
122
+ return OptimizationBenchmarkSummary(tuple(results))