Skip to content

Commit cf84c52

Browse files
author
Gaia Di Lorenzo
committed
fix(experimentalist): finalize when no improvements remain
Signed-off-by: Gaia Di Lorenzo <gdilorenzo@ethz.ch>
1 parent 6170067 commit cf84c52

2 files changed

Lines changed: 83 additions & 1 deletion

File tree

plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/experimentalist/components/loop.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,9 @@ async def _run(self, deps: ExperimentalistDeps) -> ExperimentalistResult:
810810
phase=phase,
811811
config=config,
812812
)
813+
if not improvements:
814+
logger.info("[TERMINATOR] no improvements proposed; finalizing evaluated candidates")
815+
break
813816
new_candidates = [
814817
self._create_agent(
815818
agents_dir=agents_dir,

plugins/nemo-experimentalist/tests/experimentalist/test_loop_failure.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
import pytest
1010
from nemo_experimentalist_plugin.config import EvolutionaryOptimizerConfig
11-
from nemo_experimentalist_plugin.entities import Candidate, ExperimentRun
11+
from nemo_experimentalist_plugin.entities import Candidate, EvaluationResult, ExperimentRun
1212
from nemo_experimentalist_plugin.experimentalist.components import loop as loop_module
1313
from nemo_experimentalist_plugin.experimentalist.components.loop import EvolutionaryOptimizer
14+
from nemo_experimentalist_plugin.experimentalist.components.models import EvolutionTree
15+
from nemo_experimentalist_plugin.experimentalist.components.terminator import TerminationDecision
1416
from nemo_experimentalist_plugin.experimentalist.experimentalist_backend import LocalExperimentalistBackend
1517

1618

@@ -127,3 +129,80 @@ async def run(self, *args, **kwargs):
127129
candidates=[candidate],
128130
config=EvolutionaryOptimizerConfig(),
129131
)
132+
133+
134+
@pytest.mark.asyncio
135+
async def test_no_proposals_finalizes_evaluated_candidates(monkeypatch, tmp_path):
136+
"""An exhausted proposer ends successfully with the best evaluated candidate."""
137+
baseline = Candidate(run_id="run-1", label="agent-0", round=0, optimization="baseline")
138+
tree = EvolutionTree()
139+
tree.add(baseline)
140+
run = ExperimentRun(
141+
id="run-1",
142+
workspace="default",
143+
agent="agent",
144+
config_snapshot={},
145+
status="running",
146+
rounds_completed=0,
147+
)
148+
evaluation = EvaluationResult(id="agent-0-validation", aggregate_metrics={"reward": 1.0})
149+
backend = SimpleNamespace(
150+
client=None,
151+
get_agent_code=AsyncMock(),
152+
persist_evaluation=AsyncMock(),
153+
update_run=AsyncMock(),
154+
persist_result=AsyncMock(),
155+
)
156+
finalized = AsyncMock(return_value=baseline)
157+
158+
monkeypatch.setattr(
159+
loop_module,
160+
"EvaluatorFactory",
161+
lambda: SimpleNamespace(build_evaluator=lambda *args, **kwargs: object()),
162+
)
163+
monkeypatch.setattr(
164+
loop_module,
165+
"DatasetFactory",
166+
lambda: SimpleNamespace(build_dataset=lambda *args, **kwargs: object()),
167+
)
168+
monkeypatch.setattr(EvolutionaryOptimizer, "_init_structure", lambda self: (tmp_path / "agents", tmp_path / "analysis", tmp_path / "results"))
169+
monkeypatch.setattr(EvolutionaryOptimizer, "_detect_last_round", lambda self: None)
170+
monkeypatch.setattr(EvolutionaryOptimizer, "_create_experiment_run", AsyncMock(return_value=run))
171+
monkeypatch.setattr(EvolutionaryOptimizer, "_create_baseline_agent", AsyncMock(return_value=baseline))
172+
monkeypatch.setattr(EvolutionaryOptimizer, "_update_candidate", AsyncMock())
173+
monkeypatch.setattr(EvolutionaryOptimizer, "_evaluate_validation_candidates", AsyncMock(return_value={"agent-0": evaluation}))
174+
monkeypatch.setattr(EvolutionaryOptimizer, "_generate_initial_goal_tree", AsyncMock())
175+
monkeypatch.setattr(EvolutionaryOptimizer, "_select_survivors", AsyncMock(return_value=[baseline]))
176+
monkeypatch.setattr(EvolutionaryOptimizer, "_evaluate_train_candidates", AsyncMock(return_value={"agent-0": evaluation}))
177+
monkeypatch.setattr(EvolutionaryOptimizer, "_analyze_round", AsyncMock(return_value="analysis"))
178+
monkeypatch.setattr(EvolutionaryOptimizer, "_update_goal_tree", AsyncMock())
179+
propose = AsyncMock(return_value=[])
180+
monkeypatch.setattr(EvolutionaryOptimizer, "_propose_improvements", propose)
181+
monkeypatch.setattr(EvolutionaryOptimizer, "_implement_candidates", AsyncMock())
182+
monkeypatch.setattr(EvolutionaryOptimizer, "_finalize", finalized)
183+
monkeypatch.setattr(loop_module.EvolutionTree, "from_dir", lambda path: tree)
184+
185+
optimizer = object.__new__(EvolutionaryOptimizer)
186+
optimizer.working_dir = tmp_path
187+
optimizer.config = EvolutionaryOptimizerConfig()
188+
optimizer.shell = SimpleNamespace(close=AsyncMock())
189+
optimizer.terminator = SimpleNamespace(run=AsyncMock(return_value=TerminationDecision(stop=False)))
190+
optimizer._framework_skills_dirs = []
191+
deps = SimpleNamespace(
192+
backend=backend,
193+
workspace="default",
194+
config=EvolutionaryOptimizerConfig(),
195+
evaluator_type="harbor",
196+
train_dataset=object(),
197+
validation_dataset=object(),
198+
insight=None,
199+
agent=tmp_path / "agent",
200+
agent_spec=None,
201+
task_template=None,
202+
)
203+
204+
result = await optimizer.run(deps)
205+
206+
assert result.winner is baseline
207+
propose.assert_awaited_once()
208+
finalized.assert_awaited_once()

0 commit comments

Comments
 (0)