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.
- devagent_physical_engine/__init__.py +44 -0
- devagent_physical_engine/agent/__init__.py +40 -0
- devagent_physical_engine/agent/compiler.py +285 -0
- devagent_physical_engine/agent/contracts.py +129 -0
- devagent_physical_engine/agent/coordinator.py +108 -0
- devagent_physical_engine/agent/critic.py +72 -0
- devagent_physical_engine/agent/evidence.py +34 -0
- devagent_physical_engine/agent/interpreter.py +179 -0
- devagent_physical_engine/agent/planner.py +105 -0
- devagent_physical_engine/agent/recovery.py +54 -0
- devagent_physical_engine/agent/routing.py +76 -0
- devagent_physical_engine/agent/runtime.py +270 -0
- devagent_physical_engine/agent/semantic.py +304 -0
- devagent_physical_engine/agent/structured.py +423 -0
- devagent_physical_engine/ai_cli.py +226 -0
- devagent_physical_engine/cli.py +392 -0
- devagent_physical_engine/doctor.py +20 -0
- devagent_physical_engine/engineering_agent.py +243 -0
- devagent_physical_engine/engineering_request.py +630 -0
- devagent_physical_engine/execution.py +90 -0
- devagent_physical_engine/models.py +143 -0
- devagent_physical_engine/operating_envelope.py +120 -0
- devagent_physical_engine/optimization/__init__.py +50 -0
- devagent_physical_engine/optimization/benchmark.py +122 -0
- devagent_physical_engine/optimization/candidates.py +198 -0
- devagent_physical_engine/optimization/contracts.py +235 -0
- devagent_physical_engine/optimization/evaluator.py +107 -0
- devagent_physical_engine/optimization/evidence.py +53 -0
- devagent_physical_engine/optimization/experience.py +105 -0
- devagent_physical_engine/optimization/measured.py +125 -0
- devagent_physical_engine/optimization/optimizer.py +215 -0
- devagent_physical_engine/optimization/orchestrator.py +155 -0
- devagent_physical_engine/physical_campaign.py +413 -0
- devagent_physical_engine/physical_evidence.py +214 -0
- devagent_physical_engine/physical_motion.py +196 -0
- devagent_physical_engine/planning.py +80 -0
- devagent_physical_engine/preexecution_contract.py +65 -0
- devagent_physical_engine/provider_adapters/__init__.py +22 -0
- devagent_physical_engine/provider_adapters/anthropic.py +112 -0
- devagent_physical_engine/provider_adapters/common.py +187 -0
- devagent_physical_engine/provider_adapters/factory.py +20 -0
- devagent_physical_engine/provider_adapters/gemini.py +126 -0
- devagent_physical_engine/provider_adapters/openai.py +95 -0
- devagent_physical_engine/provider_qualification.py +268 -0
- devagent_physical_engine/providers.py +94 -0
- devagent_physical_engine/qualification.py +44 -0
- devagent_physical_engine/qualification_cli.py +195 -0
- devagent_physical_engine/qualification_harness.py +917 -0
- devagent_physical_engine/robot_platform.py +411 -0
- devagent_physical_engine/robots.py +76 -0
- devagent_physical_engine/ros2/__init__.py +35 -0
- devagent_physical_engine/ros2/acceptance.py +324 -0
- devagent_physical_engine/ros2/commands.py +175 -0
- devagent_physical_engine/ros2/doctor.py +116 -0
- devagent_physical_engine/ros2/fk_probe.py +83 -0
- devagent_physical_engine/ros2/frame_alignment.py +61 -0
- devagent_physical_engine/ros2/gazebo_world.py +125 -0
- devagent_physical_engine/ros2/joint_state_recorder.py +64 -0
- devagent_physical_engine/ros2/measured_motion.py +233 -0
- devagent_physical_engine/ros2/moveit_scene.py +121 -0
- devagent_physical_engine/ros2/preexecution.py +113 -0
- devagent_physical_engine/ros2/qualification.py +81 -0
- devagent_physical_engine/ros2/qualification_v10.py +252 -0
- devagent_physical_engine/ros2/scene_probe.py +219 -0
- devagent_physical_engine/ros2/state_validity_probe.py +125 -0
- devagent_physical_engine/ros2/tf_probe.py +51 -0
- devagent_physical_engine/ros2/trajectory.py +188 -0
- devagent_physical_engine/ros2/ur5e.py +59 -0
- devagent_physical_engine/ros2/ur5e_adapter.py +349 -0
- devagent_physical_engine/ros2/ur5e_v10_adapter.py +292 -0
- devagent_physical_engine/setup_profile.py +356 -0
- devagent_physical_engine/simulation.py +32 -0
- devagent_physical_engine/simulation_platform.py +269 -0
- devagent_physical_engine/trajectory_qualification.py +201 -0
- devagent_physical_engine/twin.py +939 -0
- devagent_physical_engine/twin_builder.py +309 -0
- devagent_physical_engine/twin_materialization.py +404 -0
- devagent_physical_engine/verification.py +46 -0
- devagent_physical_engine-0.10.0.dist-info/METADATA +315 -0
- devagent_physical_engine-0.10.0.dist-info/RECORD +84 -0
- devagent_physical_engine-0.10.0.dist-info/WHEEL +5 -0
- devagent_physical_engine-0.10.0.dist-info/entry_points.txt +3 -0
- devagent_physical_engine-0.10.0.dist-info/licenses/NOTICE +2 -0
- 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))
|