Back to home page

EIC code displayed by LXR

 
 

    


Warning, file /AID2E-framework/tests/test_schedulers/test_pandaidds_runner.py was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 """Mocked PanDA/iDDS runner contract tests."""
0002 
0003 import sys
0004 import threading
0005 import types
0006 
0007 import pytest
0008 
0009 from aid2e.schedulers.PanDAiDDS.config import PanDAiDDSRunnerConfig
0010 from aid2e.schedulers.PanDAiDDS.multistage_graph import PanDAMultiStageGraphBuilder
0011 from aid2e.schedulers.PanDAiDDS.multistage import PanDAMultiStageJob, PanDAMultiStageSpec
0012 from aid2e.utilities.runtime_builders import run_trial_workflow
0013 from aid2e.schedulers.PanDAiDDS.runner import (
0014     PanDAiDDSScheduler,
0015     _normalize_all2one_input_datasets,
0016     _remote_python_callable_entrypoint,
0017 )
0018 
0019 
0020 def panda_test_evaluator(context=None, **kwargs):
0021     return {"unused": True}
0022 
0023 
0024 def panda_adapter_test_evaluator(context=None, **kwargs):
0025     context.add_log("remote adapter ran")
0026     context.xcom_push("seen", kwargs.get("label"))
0027     return {
0028         "job_id": context.job_id,
0029         "design": context.design_point,
0030         "label": kwargs.get("label"),
0031         "input_file_names": kwargs.get("input_file_names"),
0032         "xcom": context.xcom,
0033     }
0034 
0035 
0036 def panda_prepare_evaluator(context=None, **kwargs):
0037     return {"prepared": kwargs.get("scale", 1)}
0038 
0039 
0040 def panda_evaluate_from_parent(context=None, **kwargs):
0041     parent = kwargs["parent_results"]
0042     return {"f1": parent["prepared"] + 1, "f2": parent["prepared"] + 2}
0043 
0044 
0045 def panda_chunk_evaluator(context=None, **kwargs):
0046     return {"chunk": kwargs.get("child_key"), "value": kwargs.get("value", 0)}
0047 
0048 
0049 def panda_analyze_chunks(context=None, **kwargs):
0050     parent = kwargs["parent_results"]
0051     return {"total": sum(item["value"] for item in parent.values())}
0052 
0053 
0054 def panda_ana_one2one(context=None, **kwargs):
0055     parent = kwargs["simreco_result"]
0056     return {"analyzed": parent["value"] + 1}
0057 
0058 
0059 def panda_local_final_from_ana(context=None, **kwargs):
0060     ana_result = kwargs["ana_result"]
0061     return {"objective": ana_result.get("total", ana_result.get("analyzed")) + 1}
0062 
0063 
0064 def panda_strict_local_stage(context=None):
0065     return {"objective": 1}
0066 
0067 
0068 def panda_no_context_evaluator(value):
0069     return {"value": value}
0070 
0071 
0072 class DummyContext:
0073     def __init__(self):
0074         self.pushed = {}
0075 
0076     def xcom_push(self, key, value):
0077         self.pushed[key] = value
0078 
0079 
0080 def install_fake_idds(
0081     monkeypatch,
0082     *,
0083     status="finished",
0084     result=None,
0085     details=None,
0086     result_sequence=None,
0087     details_sequence=None,
0088     status_sequence=None,
0089 ):
0090     state = {
0091         "work_kwargs_history": [],
0092         "work_params_history": [],
0093         "result_lookup_history": [],
0094         "work_history": [],
0095     }
0096     result = {"f1": 1.2, "f2": 0.4} if result is None else result
0097     details = {"attempt": 1} if details is None else details
0098 
0099     idds_module = types.ModuleType("idds")
0100     iworkflow_module = types.ModuleType("idds.iworkflow")
0101     workflow_module = types.ModuleType("idds.iworkflow.workflow")
0102     work_module = types.ModuleType("idds.iworkflow.work")
0103 
0104     class FakeWorkflow:
0105         def pre_run(self):
0106             state["workflow_pre_run"] = True
0107 
0108         def prepare(self):
0109             state["workflow_prepare"] = True
0110 
0111         def submit(self):
0112             state["workflow_submitted"] = True
0113             return "req-1"
0114 
0115     class FakeResult:
0116         def __init__(self, index):
0117             self.index = index
0118 
0119         def get_result(self, name, key, verbose=False, with_details=False):
0120             lookup = {
0121                 "name": name,
0122                 "key": key,
0123                 "verbose": verbose,
0124                 "with_details": with_details,
0125             }
0126             state["result_lookup"] = lookup
0127             state["result_lookup_history"].append(lookup)
0128             selected_result = (
0129                 result_sequence[self.index]
0130                 if result_sequence is not None
0131                 else result
0132             )
0133             selected_details = (
0134                 details_sequence[self.index]
0135                 if details_sequence is not None
0136                 else details
0137             )
0138             return selected_result, selected_details
0139 
0140     class FakeWork:
0141         def __init__(self, name, index):
0142             self.name = name
0143             self.index = index
0144             self.internal_id = f"internal-{index + 1}"
0145             self.core_count = None
0146             self.init_async_result_called = False
0147             self.cancelled = False
0148 
0149         def submit(self):
0150             state["work_submitted"] = True
0151             return f"tf-{self.index + 1}"
0152 
0153         def init_async_result(self):
0154             self.init_async_result_called = True
0155 
0156         def get_status(self):
0157             if status_sequence is not None:
0158                 return status_sequence[self.index]
0159             return status
0160 
0161         def is_finished(self, value):
0162             return value == "finished"
0163 
0164         def is_failed(self, value):
0165             return value == "failed"
0166 
0167         def is_terminated(self):
0168             return self.cancelled
0169 
0170         def cancel(self):
0171             self.cancelled = True
0172 
0173         def get_results(self):
0174             return FakeResult(self.index)
0175 
0176     def workflow_def(**kwargs):
0177         state["workflow_kwargs"] = kwargs
0178 
0179         def builder():
0180             return FakeWorkflow()
0181 
0182         return builder
0183 
0184     def work_def(**kwargs):
0185         state["work_kwargs"] = kwargs
0186         state["work_kwargs_history"].append(kwargs)
0187         index = len(state["work_kwargs_history"]) - 1
0188 
0189         def builder(**params):
0190             state["work_params"] = params
0191             state["work_params_history"].append(params)
0192             work = FakeWork(kwargs["name"], index)
0193             state["work"] = work
0194             state["work_history"].append(work)
0195             return work
0196 
0197         return builder
0198 
0199     workflow_module.workflow = workflow_def
0200     work_module.work = work_def
0201 
0202     monkeypatch.setitem(sys.modules, "idds", idds_module)
0203     monkeypatch.setitem(sys.modules, "idds.iworkflow", iworkflow_module)
0204     monkeypatch.setitem(sys.modules, "idds.iworkflow.workflow", workflow_module)
0205     monkeypatch.setitem(sys.modules, "idds.iworkflow.work", work_module)
0206 
0207     return state
0208 
0209 
0210 def test_run_stage_preserves_job_id_and_returns_idds_outputs(monkeypatch, tmp_path):
0211     state = install_fake_idds(monkeypatch, result={"f1": 1.2, "f2": 0.4})
0212     context = DummyContext()
0213     scheduler = PanDAiDDSScheduler(
0214         PanDAiDDSRunnerConfig(
0215             name="user.test.panda",
0216             cloud="US",
0217             queue="BNL",
0218             init_env="setup_panda.sh",
0219             source_dir=str(tmp_path),
0220         )
0221     )
0222 
0223     result = scheduler.run_stage(
0224         "evaluate",
0225         [
0226             {
0227                 "job_id": "evaluate:dtlz2:0001",
0228                 "name": "dtlz2",
0229                 "function": panda_test_evaluator,
0230                 "params": {"context": context, "x": [0.5, 0.5, 0.5]},
0231                 "payload": {"label": "dtlz2"},
0232                 "job_context": context,
0233             }
0234         ],
0235         parallelism_policy={"poll_interval": 0},
0236         working_dir=str(tmp_path),
0237     )
0238 
0239     assert result.success is True
0240     assert result.artifacts == {"objectives": {"f1": 1.2, "f2": 0.4}}
0241     assert len(result.job_statuses) == 1
0242 
0243     status = result.job_statuses[0]
0244     assert status.job_id == "evaluate:dtlz2:0001"
0245     assert status.status == "completed"
0246     assert status.return_code == 0
0247     assert status.outputs == {"objectives": {"f1": 1.2, "f2": 0.4}}
0248     assert status.metrics == {"transform_id": "tf-1"}
0249 
0250     assert context.pushed["objectives"] == {"f1": 1.2, "f2": 0.4}
0251     assert context.pushed["results_details"] == {"attempt": 1}
0252     assert state["work_kwargs"]["func"] is _remote_python_callable_entrypoint
0253     assert state["work_params"]["callable_ref"] == (
0254         "tests.test_schedulers.test_pandaidds_runner:panda_test_evaluator"
0255     )
0256     assert state["work_params"]["op_kwargs"] == {"x": [0.5, 0.5, 0.5]}
0257     assert state["work_params"]["with_context"] is True
0258     assert state["work_params"]["context_payload"]["job_id"] == "evaluate:dtlz2:0001"
0259     assert state["work_params"]["context_payload"]["payload"] == {"label": "dtlz2"}
0260     assert state["work_kwargs"]["name"] == state["work_kwargs"]["job_key"]
0261     assert state["work_kwargs"]["name"].startswith("user.test.a2e.")
0262     assert len(state["work_kwargs"]["name"]) <= 64
0263     assert state["work_kwargs"]["name"].split(".")[-2] == "000001"
0264     assert state["work_kwargs"]["log_dataset_name"] == f'{state["work_kwargs"]["name"]}.log/'
0265 
0266     cached = scheduler.check_status("evaluate:dtlz2:0001")
0267     assert cached.status == "completed"
0268     assert cached.outputs == {"objectives": {"f1": 1.2, "f2": 0.4}}
0269 
0270 
0271 def test_run_stage_marks_failed_idds_work_failed(monkeypatch, tmp_path):
0272     state = install_fake_idds(monkeypatch, status="failed")
0273     scheduler = PanDAiDDSScheduler(
0274         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0275     )
0276 
0277     result = scheduler.run_stage(
0278         "evaluate",
0279         [
0280             {
0281                 "job_id": "evaluate:failed:0001",
0282                 "name": "failed_eval",
0283                 "function": panda_test_evaluator,
0284                 "params": {},
0285             }
0286         ],
0287         parallelism_policy={"poll_interval": 0},
0288         working_dir=str(tmp_path),
0289     )
0290 
0291     assert result.success is False
0292     assert result.error_message == "Some jobs failed in stage 'evaluate'"
0293     assert result.artifacts == {}
0294 
0295     status = result.job_statuses[0]
0296     assert status.job_id == "evaluate:failed:0001"
0297     assert status.status == "failed"
0298     assert status.return_code == -1
0299     assert "failed" in status.stderr
0300     assert status.outputs == {}
0301     assert state["work_params"]["with_context"] is False
0302 
0303     cached = scheduler.check_status("evaluate:failed:0001")
0304     assert cached.status == "failed"
0305     assert cached.return_code == -1
0306 
0307 
0308 
0309 def test_repeated_logical_job_submissions_get_distinct_panda_work_names(monkeypatch, tmp_path):
0310     state = install_fake_idds(monkeypatch, status="running")
0311     scheduler = PanDAiDDSScheduler(
0312         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0313     )
0314     job = {
0315         "job_id": "dtlz2",
0316         "name": "dtlz2",
0317         "function": panda_test_evaluator,
0318         "params": {},
0319     }
0320 
0321     scheduler.submit_job("evaluate", job, str(tmp_path))
0322     scheduler.submit_job("evaluate", job, str(tmp_path))
0323     scheduler.submit_job("evaluate", job, str(tmp_path))
0324 
0325     names = [kwargs["name"] for kwargs in state["work_kwargs_history"]]
0326     assert len(names) == 3
0327     assert len(set(names)) == 3
0328     assert all(name.startswith("user.test.a2e.") for name in names)
0329     assert all(len(name) <= 64 for name in names)
0330     assert names[0].split(".")[-2] == "000001"
0331     assert names[1].split(".")[-2] == "000002"
0332     assert names[2].split(".")[-2] == "000003"
0333     assert [kwargs["job_key"] for kwargs in state["work_kwargs_history"]] == names
0334     assert [kwargs["log_dataset_name"] for kwargs in state["work_kwargs_history"]] == [
0335         f"{names[0]}.log/",
0336         f"{names[1]}.log/",
0337         f"{names[2]}.log/",
0338     ]
0339 
0340 
0341 def test_distinct_scheduler_instances_do_not_reuse_panda_work_names(tmp_path):
0342     first = PanDAiDDSScheduler(
0343         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0344     )
0345     second = PanDAiDDSScheduler(
0346         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0347     )
0348 
0349     first_name = first._next_work_name("evaluate", "dtlz2", "panda_test_evaluator")
0350     second_name = second._next_work_name("evaluate", "dtlz2", "panda_test_evaluator")
0351 
0352     assert first_name != second_name
0353     assert first_name.startswith("user.test.a2e.")
0354     assert second_name.startswith("user.test.a2e.")
0355     assert first_name.split(".")[-2] == "000001"
0356     assert second_name.split(".")[-2] == "000001"
0357     assert len(first_name) <= 64
0358     assert len(second_name) <= 64
0359 
0360 
0361 def test_cancel_job_finds_preserved_job_id(tmp_path):
0362     class CancellableWork:
0363         def __init__(self):
0364             self.cancelled = False
0365 
0366         def is_terminated(self):
0367             return False
0368 
0369         def cancel(self):
0370             self.cancelled = True
0371 
0372     scheduler = PanDAiDDSScheduler(
0373         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0374     )
0375     work = CancellableWork()
0376     scheduler.running_funcs["evaluate"] = {
0377         "evaluate:dtlz2:0001": {"funcs": {"panda_test_evaluator": {"work": work}}}
0378     }
0379 
0380     assert scheduler.cancel_job("evaluate:dtlz2:0001") is True
0381     assert work.cancelled is True
0382 
0383 
0384 def test_remote_python_callable_entrypoint_runs_only_configured_callable():
0385     result = _remote_python_callable_entrypoint(
0386         "tests.test_schedulers.test_pandaidds_runner:panda_adapter_test_evaluator",
0387         {
0388             "task_id": "evaluate:dtlz2",
0389             "job_id": "dtlz2",
0390             "stage_id": "evaluate",
0391             "workflow_id": "wf",
0392             "design_point": {"x": 0.5},
0393             "xcom": {},
0394             "execution_dir": "/tmp/work",
0395             "output_dir": "/tmp/out",
0396         },
0397         {"label": "callable-only"},
0398     )
0399 
0400     assert result["job_id"] == "dtlz2"
0401     assert result["design"] == {"x": 0.5}
0402     assert result["label"] == "callable-only"
0403     assert result["input_file_names"] is None
0404     assert result["xcom"] == {"evaluate:dtlz2:seen": "callable-only"}
0405 
0406 
0407 def test_remote_python_callable_entrypoint_accepts_idds_injected_kwargs():
0408     result = _remote_python_callable_entrypoint(
0409         "tests.test_schedulers.test_pandaidds_runner:panda_adapter_test_evaluator",
0410         {
0411             "task_id": "dtlz2",
0412             "job_id": "dtlz2",
0413             "stage_id": "evaluate",
0414             "workflow_id": "wf",
0415             "design_point": {},
0416             "xcom": {},
0417         },
0418         {"label": "dataset-ana"},
0419         input_file_names=["a.json", "b.json"],
0420     )
0421 
0422     assert result["label"] == "dataset-ana"
0423     assert result["input_file_names"] == ["a.json", "b.json"]
0424 
0425 
0426 def test_all2one_input_dataset_adapter_expands_and_restores_yaml_list():
0427     normalized, groups = _normalize_all2one_input_datasets(
0428         {"input_file_names": ["user.test.sim.a", "user.test.sim.b"]}
0429     )
0430 
0431     assert normalized == {
0432         "input_file_names__aid2e_0": "user.test.sim.a",
0433         "input_file_names__aid2e_1": "user.test.sim.b",
0434     }
0435     assert groups == {
0436         "input_file_names": [
0437             "input_file_names__aid2e_0",
0438             "input_file_names__aid2e_1",
0439         ]
0440     }
0441 
0442     result = _remote_python_callable_entrypoint(
0443         "tests.test_schedulers.test_pandaidds_runner:panda_adapter_test_evaluator",
0444         {
0445             "task_id": "dtlz2",
0446             "job_id": "dtlz2",
0447             "stage_id": "ana",
0448             "workflow_id": "wf",
0449             "design_point": {},
0450             "xcom": {},
0451         },
0452         input_dataset_groups=groups,
0453         input_file_names__aid2e_0=["a.json"],
0454         input_file_names__aid2e_1=["b.json", "c.json"],
0455     )
0456 
0457     assert result["input_file_names"] == ["a.json", "b.json", "c.json"]
0458 
0459 
0460 def test_remote_python_callable_entrypoint_omits_unsupplied_context():
0461     result = _remote_python_callable_entrypoint(
0462         "tests.test_schedulers.test_pandaidds_runner:panda_no_context_evaluator",
0463         {
0464             "task_id": "trial",
0465             "job_id": "trial",
0466             "stage_id": "optimizer",
0467             "workflow_id": "wf",
0468         },
0469         {"value": 4},
0470         with_context=False,
0471     )
0472 
0473     assert result == {"value": 4}
0474 
0475 
0476 def test_panda_multistage_spec_rejects_unknown_dependency_map():
0477     with pytest.raises(ValueError, match="expected 'one2one'"):
0478         PanDAMultiStageSpec.from_payload(
0479             {
0480                 "stages": [
0481                     {
0482                         "name": "simreco",
0483                         "python_callable": panda_prepare_evaluator,
0484                         "dep_map": "scatter",
0485                     }
0486                 ]
0487             }
0488         )
0489 
0490 
0491 def test_panda_multistage_spec_rejects_cycles():
0492     with pytest.raises(ValueError, match="cycle"):
0493         PanDAMultiStageSpec.from_payload(
0494             {
0495                 "stages": [
0496                     {
0497                         "name": "first",
0498                         "python_callable": panda_prepare_evaluator,
0499                         "depends_on": "second",
0500                     },
0501                     {
0502                         "name": "second",
0503                         "python_callable": panda_test_evaluator,
0504                         "depends_on": "first",
0505                     },
0506                 ]
0507             }
0508         )
0509 
0510 
0511 def test_panda_multistage_failed_parent_does_not_release_result_child(tmp_path):
0512     scheduler = PanDAiDDSScheduler(
0513         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0514     )
0515     coordinator = PanDAMultiStageJob(
0516         scheduler=scheduler,
0517         stage_name="evaluate",
0518         job_definition={
0519             "payload": {
0520                 "stages": [
0521                     {"name": "parent", "python_callable": panda_prepare_evaluator},
0522                     {
0523                         "name": "child",
0524                         "python_callable": panda_test_evaluator,
0525                         "depends_on": "parent",
0526                         "dep_type": "results",
0527                     },
0528                 ]
0529             }
0530         },
0531     )
0532     coordinator.works_by_stage["parent"].append(
0533         types.SimpleNamespace(status="failed")
0534     )
0535 
0536     assert coordinator._stage_ready_for_submission(coordinator.spec.by_name["child"]) is False
0537 
0538 
0539 def test_panda_multistage_jobs_run_concurrently_within_batch(monkeypatch, tmp_path):
0540     scheduler = PanDAiDDSScheduler(
0541         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0542     )
0543     barrier = threading.Barrier(2, timeout=2)
0544 
0545     def fake_run(stage_name, job_definition, working_dir, poll_interval):
0546         barrier.wait()
0547         return {
0548             "status": "completed",
0549             "return_code": 0,
0550             "stdout": "",
0551             "stderr": "",
0552             "outputs": {"objectives": {"value": int(job_definition["job_id"]) }},
0553         }
0554 
0555     monkeypatch.setattr(scheduler, "_run_multistage_job", fake_run)
0556     result = scheduler.run_stage(
0557         "optimizer_batch_1_trials",
0558         [
0559             {"job_id": "0", "payload": {"evaluator_type": "panda_multistage"}},
0560             {"job_id": "1", "payload": {"evaluator_type": "panda_multistage"}},
0561         ],
0562         parallelism_policy={"max_concurrent": 2, "poll_interval": 0},
0563         working_dir=str(tmp_path),
0564     )
0565 
0566     assert result.success is True
0567     assert [status.job_id for status in result.job_statuses] == ["0", "1"]
0568 
0569 
0570 def test_panda_multistage_does_not_inject_implicit_stage_kwargs(tmp_path):
0571     scheduler = PanDAiDDSScheduler(
0572         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0573     )
0574 
0575     result = scheduler.run_stage(
0576         "evaluate",
0577         [
0578             {
0579                 "job_id": "logical:strict",
0580                 "name": "strict_stage_job",
0581                 "payload": {
0582                     "evaluator_type": "panda_multistage",
0583                     "stages": [
0584                         {
0585                             "name": "final",
0586                             "python_callable": panda_strict_local_stage,
0587                             "runner": "local",
0588                             "produces_objective": True,
0589                         }
0590                     ],
0591                 },
0592             }
0593         ],
0594         parallelism_policy={"poll_interval": 0},
0595         working_dir=str(tmp_path),
0596     )
0597 
0598     assert result.success is True
0599     assert result.job_statuses[0].outputs == {"objectives": {"objective": 1}}
0600 
0601 
0602 def test_panda_multistage_dataset_dependency_is_managed_by_panda(monkeypatch, tmp_path):
0603     state = install_fake_idds(
0604         monkeypatch,
0605         result_sequence=[{"simreco": "done"}, {"f1": 3, "f2": 4}],
0606         details_sequence=[{"stage": "simreco"}, {"stage": "ana"}],
0607     )
0608     context = DummyContext()
0609     scheduler = PanDAiDDSScheduler(
0610         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0611     )
0612 
0613     result = scheduler.run_stage(
0614         "evaluate",
0615         [
0616             {
0617                 "job_id": "logical:multistage",
0618                 "name": "dataset_stage_job",
0619                 "payload": {
0620                     "evaluator_type": "panda_multistage",
0621                     "stages": [
0622                         {
0623                             "name": "simreco",
0624                             "python_callable": panda_prepare_evaluator,
0625                             "output_file": "my_test.txt",
0626                             "output_dataset": "user.test.simreco.dataset",
0627                             "num_events": 200,
0628                             "num_events_per_job": 100,
0629                         },
0630                         {
0631                             "name": "ana",
0632                             "python_callable": panda_test_evaluator,
0633                             "depends_on": "simreco",
0634                             "dep_type": "datasets",
0635                             "dep_map": "one2one",
0636                             "input_datasets": {
0637                                 "input_file_names": "user.test.simreco.dataset"
0638                             },
0639                             "produces_objective": True,
0640                         },
0641                     ],
0642                 },
0643                 "job_context": context,
0644             }
0645         ],
0646         parallelism_policy={"poll_interval": 0},
0647         working_dir=str(tmp_path),
0648     )
0649 
0650     assert result.success is True
0651     assert result.artifacts == {"objectives": {"f1": 3, "f2": 4}}
0652     assert result.job_statuses[0].outputs == {"objectives": {"f1": 3, "f2": 4}}
0653     assert context.pushed["objectives"] == {"f1": 3, "f2": 4}
0654     assert context.pushed["results_details"] == {"stage": "ana"}
0655 
0656     simreco_kwargs = state["work_kwargs_history"][0]
0657     ana_kwargs = state["work_kwargs_history"][1]
0658     assert simreco_kwargs["output_file_name"] == "my_test.txt"
0659     assert simreco_kwargs["output_dataset_name"] == "user.test.simreco.dataset/"
0660     assert simreco_kwargs["num_events"] == 200
0661     assert simreco_kwargs["num_events_per_job"] == 100
0662     assert ana_kwargs["input_datasets"] == {
0663         "input_file_names": "user.test.simreco.dataset"
0664     }
0665     assert ana_kwargs["parent_internal_id"] == "internal-1"
0666     assert [kwargs["name"] for kwargs in state["work_kwargs_history"]] == [
0667         state["work_kwargs_history"][0]["job_key"],
0668         state["work_kwargs_history"][1]["job_key"],
0669     ]
0670     assert state["result_lookup_history"][-1] == {
0671         "name": state["work_kwargs_history"][1]["name"],
0672         "key": state["work_kwargs_history"][1]["job_key"],
0673         "verbose": True,
0674         "with_details": True,
0675     }
0676 
0677     metrics = result.job_statuses[0].metrics["panda_multistage_stages"]
0678     assert [item["stage"] for item in metrics] == ["simreco", "ana"]
0679     assert [item["internal_id"] for item in metrics] == ["internal-1", "internal-2"]
0680 
0681 
0682 def test_panda_multistage_one2many_local_to_remote(monkeypatch, tmp_path):
0683     state = install_fake_idds(
0684         monkeypatch,
0685         result_sequence=[{"child": "a", "f1": 1}, {"child": "b", "f1": 2}],
0686     )
0687     scheduler = PanDAiDDSScheduler(
0688         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0689     )
0690 
0691     result = scheduler.run_stage(
0692         "evaluate",
0693         [
0694             {
0695                 "job_id": "logical:one2many",
0696                 "name": "one2many_stage_job",
0697                 "payload": {
0698                     "evaluator_type": "panda_multistage",
0699                     "stages": [
0700                         {
0701                             "name": "prepare",
0702                             "python_callable": panda_prepare_evaluator,
0703                             "runner": "local",
0704                             "dep_type": "results",
0705                             "op_kwargs": {"scale": 5},
0706                         },
0707                         {
0708                             "name": "simreco",
0709                             "python_callable": panda_chunk_evaluator,
0710                             "depends_on": "prepare",
0711                             "dep_type": "results",
0712                             "dep_map": "one2many",
0713                             "jobs": [
0714                                 {"key": "a", "op_kwargs": {"value": 1}},
0715                                 {"key": "b", "op_kwargs": {"value": 2}},
0716                             ],
0717                             "parent_result_parameter_name": "prepared_result",
0718                             "produces_objective": True,
0719                         },
0720                     ],
0721                 },
0722             }
0723         ],
0724         parallelism_policy={"poll_interval": 0},
0725         working_dir=str(tmp_path),
0726     )
0727 
0728     assert result.success is True
0729     assert len(state["work_kwargs_history"]) == 2
0730     assert state["work_params_history"][0]["op_kwargs"] == {"value": 1, "prepared_result": {"prepared": 5}}
0731     assert state["work_params_history"][1]["op_kwargs"] == {"value": 2, "prepared_result": {"prepared": 5}}
0732     metrics = result.job_statuses[0].metrics["panda_multistage_stages"]
0733     assert [item["execution"] for item in metrics] == ["local", "panda", "panda"]
0734 
0735 
0736 def test_panda_multistage_submits_remote_children_before_parent_completion(monkeypatch, tmp_path):
0737     state = install_fake_idds(monkeypatch, status_sequence=["running", "running", "running"])
0738     scheduler = PanDAiDDSScheduler(
0739         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0740     )
0741     coordinator = PanDAMultiStageJob(
0742         scheduler=scheduler,
0743         stage_name="evaluate",
0744         job_definition={
0745             "job_id": "logical:submit-first",
0746             "name": "submit_first",
0747             "payload": {
0748                 "evaluator_type": "panda_multistage",
0749                 "stages": [
0750                     {
0751                         "name": "simreco",
0752                         "python_callable": panda_prepare_evaluator,
0753                         "output_file": "my_test.txt",
0754                         "jobs": [
0755                             {"key": "a", "output_dataset": "user.test.simreco.a"},
0756                             {"key": "b", "output_dataset": "user.test.simreco.b"},
0757                         ],
0758                     },
0759                     {
0760                         "name": "ana",
0761                         "python_callable": panda_test_evaluator,
0762                         "depends_on": "simreco",
0763                         "dep_type": "datasets",
0764                         "dep_map": "all2one",
0765                         "input_datasets": {
0766                             "input_file_names": [
0767                                 "user.test.simreco.a",
0768                                 "user.test.simreco.b",
0769                             ]
0770                         },
0771                         "produces_objective": True,
0772                     },
0773                 ],
0774             },
0775         },
0776         working_dir=str(tmp_path),
0777         poll_interval=0,
0778     )
0779 
0780     coordinator._submit_stage(coordinator.spec.stages[0])
0781     assert [work.status for work in coordinator.works_by_stage["simreco"]] == ["running", "running"]
0782 
0783     coordinator._submit_stage(coordinator.spec.stages[1])
0784 
0785     assert len(state["work_kwargs_history"]) == 3
0786     assert state["work_kwargs_history"][2]["parent_internal_id"] == ["internal-1", "internal-2"]
0787     assert state["work_kwargs_history"][2]["input_datasets"] == {
0788         "input_file_names__aid2e_0": "user.test.simreco.a",
0789         "input_file_names__aid2e_1": "user.test.simreco.b",
0790     }
0791     assert state["work_params_history"][2]["input_dataset_groups"] == {
0792         "input_file_names": ["input_file_names__aid2e_0", "input_file_names__aid2e_1"]
0793     }
0794     assert [work.status for work in coordinator.works_by_stage["simreco"]] == ["running", "running"]
0795     assert coordinator.works_by_stage["ana"][0].status == "running"
0796 
0797 
0798 
0799 def test_panda_multistage_all2one_dataset_remote_to_remote(monkeypatch, tmp_path):
0800     state = install_fake_idds(
0801         monkeypatch,
0802         result_sequence=[{"sim": "a"}, {"sim": "b"}, {"f1": 3, "f2": 4}],
0803     )
0804     scheduler = PanDAiDDSScheduler(
0805         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0806     )
0807 
0808     result = scheduler.run_stage(
0809         "evaluate",
0810         [
0811             {
0812                 "job_id": "logical:all2one",
0813                 "name": "all2one_stage_job",
0814                 "payload": {
0815                     "evaluator_type": "panda_multistage",
0816                     "stages": [
0817                         {
0818                             "name": "simreco",
0819                             "python_callable": panda_prepare_evaluator,
0820                             "output_file": "my_test.txt",
0821                             "jobs": [
0822                                 {"key": "a", "output_dataset": "user.test.simreco.a"},
0823                                 {"key": "b", "output_dataset": "user.test.simreco.b"},
0824                             ],
0825                         },
0826                         {
0827                             "name": "ana",
0828                             "python_callable": panda_test_evaluator,
0829                             "depends_on": "simreco",
0830                             "dep_type": "datasets",
0831                             "dep_map": "all2one",
0832                             "input_datasets": {
0833                                 "input_file_names": [
0834                                     "user.test.simreco.a",
0835                                     "user.test.simreco.b",
0836                                 ]
0837                             },
0838                             "produces_objective": True,
0839                         },
0840                     ],
0841                 },
0842             }
0843         ],
0844         parallelism_policy={"poll_interval": 0},
0845         working_dir=str(tmp_path),
0846     )
0847 
0848     assert result.success is True
0849     assert result.artifacts == {"objectives": {"f1": 3, "f2": 4}}
0850     assert len(state["work_kwargs_history"]) == 3
0851     assert state["work_kwargs_history"][0]["output_dataset_name"] == "user.test.simreco.a/"
0852     assert state["work_kwargs_history"][1]["output_dataset_name"] == "user.test.simreco.b/"
0853     assert state["work_kwargs_history"][2]["input_datasets"] == {
0854         "input_file_names__aid2e_0": "user.test.simreco.a",
0855         "input_file_names__aid2e_1": "user.test.simreco.b",
0856     }
0857     assert state["work_kwargs_history"][2]["parent_internal_id"] == ["internal-1", "internal-2"]
0858     metrics = result.job_statuses[0].metrics["panda_multistage_stages"]
0859     assert [item["execution"] for item in metrics] == ["panda", "panda", "panda"]
0860 
0861 
0862 def test_panda_multistage_one2one_result_local_to_remote(monkeypatch, tmp_path):
0863     state = install_fake_idds(monkeypatch, result_sequence=[{"f1": 6, "f2": 7}])
0864     scheduler = PanDAiDDSScheduler(
0865         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
0866     )
0867 
0868     result = scheduler.run_stage(
0869         "evaluate",
0870         [
0871             {
0872                 "job_id": "logical:one2one-local-remote",
0873                 "name": "one2one_local_remote",
0874                 "payload": {
0875                     "evaluator_type": "panda_multistage",
0876                     "stages": [
0877                         {
0878                             "name": "prepare",
0879                             "python_callable": panda_prepare_evaluator,
0880                             "runner": "local",
0881                             "dep_type": "results",
0882                             "op_kwargs": {"scale": 4},
0883                         },
0884                         {
0885                             "name": "ana",
0886                             "python_callable": panda_evaluate_from_parent,
0887                             "depends_on": "prepare",
0888                             "dep_type": "results",
0889                             "dep_map": "one2one",
0890                             "produces_objective": True,
0891                         },
0892                     ],
0893                 },
0894             }
0895         ],
0896         parallelism_policy={"poll_interval": 0},
0897         working_dir=str(tmp_path),
0898     )
0899 
0900     assert result.success is True
0901     assert state["work_params_history"][0]["op_kwargs"]["parent_results"] == {"prepared": 4}
0902     metrics = result.job_statuses[0].metrics["panda_multistage_stages"]
0903     assert [item["execution"] for item in metrics] == ["local", "panda"]
0904 
0905 
0906 def test_panda_multistage_graph_builder_builds_all2one_dataset_payload():
0907     payload = PanDAMultiStageGraphBuilder().build_payload(
0908         [
0909             {
0910                 "name": "simreco",
0911                 "jobs": [
0912                     {
0913                         "name": "sim_a",
0914                         "payload": {
0915                             "python_callable": panda_prepare_evaluator,
0916                             "output_file": "my_test.txt",
0917                             "output_dataset": "user.test.sim.a",
0918                             "op_kwargs": {"particle": "pi+"},
0919                         },
0920                     },
0921                     {
0922                         "name": "sim_b",
0923                         "payload": {
0924                             "python_callable": panda_prepare_evaluator,
0925                             "output_file": "my_test.txt",
0926                             "output_dataset": "user.test.sim.b",
0927                             "op_kwargs": {"particle": "kaon+"},
0928                         },
0929                     },
0930                 ],
0931             },
0932             {
0933                 "name": "ana",
0934                 "jobs": [
0935                     {
0936                         "name": "ana_all",
0937                         "payload": {
0938                             "python_callable": panda_test_evaluator,
0939                             "depends_on": {"stage": "simreco", "jobs": "*"},
0940                             "dep_type": "datasets",
0941                             "dep_map": "many2one",
0942                             "input_datasets": {
0943                                 "input_file_names": ["user.test.sim.a", "user.test.sim.b"]
0944                             },
0945                             "produces_objective": True,
0946                         },
0947                     }
0948                 ],
0949             },
0950         ],
0951         final_stage="ana",
0952         trial_index=7,
0953     )
0954 
0955     assert payload["evaluator_type"] == "panda_multistage"
0956     assert payload["final_stage"] == "ana"
0957     assert payload["trial_index"] == 7
0958     assert [stage["name"] for stage in payload["stages"]] == ["simreco", "ana"]
0959     assert payload["stages"][0]["python_callable"] is panda_prepare_evaluator
0960     assert payload["stages"][0]["jobs"] == [
0961         {
0962             "key": "sim_a",
0963             "op_kwargs": {"particle": "pi+"},
0964             "output_file": "my_test.txt",
0965             "output_dataset": "user.test.sim.a",
0966         },
0967         {
0968             "key": "sim_b",
0969             "op_kwargs": {"particle": "kaon+"},
0970             "output_file": "my_test.txt",
0971             "output_dataset": "user.test.sim.b",
0972         },
0973     ]
0974     assert payload["stages"][1]["depends_on"] == {"stage": "simreco", "jobs": "*"}
0975     assert payload["stages"][1]["dep_map"] == "many2one"
0976     assert payload["stages"][1]["jobs"] == [
0977         {
0978             "key": "ana_all",
0979             "input_datasets": {
0980                 "input_file_names": ["user.test.sim.a", "user.test.sim.b"]
0981             },
0982         }
0983     ]
0984 
0985 
0986 
0987 def test_panda_runner_converts_optimizer_trial_wrapper_to_multistage_graph(monkeypatch, tmp_path):
0988     design_file = tmp_path / "design.params"
0989     design_file.write_text(
0990         "design_space:\n"
0991         "  design_parameters:\n"
0992         "    test_variables:\n"
0993         "      parameters:\n"
0994         "        x1:\n"
0995         "          value: 0.5\n"
0996         "          bounds: [0.0, 1.0]\n"
0997     )
0998     config_file = tmp_path / "trial_graph.yml"
0999     config_file.write_text(
1000         f"""
1001 problem:
1002   name: "trial graph smoke"
1003   problem_type: "DTLZ2"
1004   output_location: "{tmp_path / 'out'}"
1005   work_location: "{tmp_path / 'work'}"
1006   design_parameters_file: "{design_file}"
1007   objectives:
1008     - name: "objective"
1009       direction: "minimize"
1010 
1011 optimizer:
1012   name: "ax"
1013   type: "bayesian"
1014   parameters:
1015     n_iterations: 1
1016     n_initial_samples: 1
1017     batch_size: 1
1018 
1019 scheduler:
1020   runner_type: "PanDAiDDSRunner"
1021   parameters:
1022     name: "user.test.panda"
1023     source_dir: "{tmp_path}"
1024 
1025 workflows:
1026   workflows:
1027     - name: "trial_graph_eval"
1028       branches:
1029         - name: "main"
1030           stages:
1031             - name: "simreco"
1032               jobs:
1033                 - name: "sim_a"
1034                   command: ""
1035                   payload:
1036                     evaluator_type: "python"
1037                     python_callable: "tests.test_schedulers.test_pandaidds_runner:panda_prepare_evaluator"
1038                     output_file: "my_test.txt"
1039                     output_dataset: "user.test.trial.sim.a"
1040                 - name: "sim_b"
1041                   command: ""
1042                   payload:
1043                     evaluator_type: "python"
1044                     python_callable: "tests.test_schedulers.test_pandaidds_runner:panda_prepare_evaluator"
1045                     output_file: "my_test.txt"
1046                     output_dataset: "user.test.trial.sim.b"
1047             - name: "ana"
1048               jobs:
1049                 - name: "ana_all"
1050                   command: ""
1051                   payload:
1052                     evaluator_type: "python"
1053                     python_callable: "tests.test_schedulers.test_pandaidds_runner:panda_test_evaluator"
1054                     depends_on: "simreco"
1055                     dep_type: "datasets"
1056                     dep_map: "all2one"
1057                     input_datasets:
1058                       input_file_names:
1059                         - "user.test.trial.sim.a"
1060                         - "user.test.trial.sim.b"
1061             - name: "final"
1062               scheduler:
1063                 runner_type: "JobLibRunner"
1064                 parameters:
1065                   n_jobs: 1
1066               jobs:
1067                 - name: "final_objective"
1068                   command: ""
1069                   payload:
1070                     evaluator_type: "python"
1071                     python_callable: "tests.test_schedulers.test_pandaidds_runner:panda_local_final_from_ana"
1072                     depends_on: "ana"
1073                     dep_type: "results"
1074                     dep_map: "one2one"
1075                     parent_result_parameter_name: "ana_result"
1076                     final: true
1077 """
1078     )
1079     state = install_fake_idds(
1080         monkeypatch,
1081         result_sequence=[{"sim": "a"}, {"sim": "b"}, {"total": 5}],
1082     )
1083     scheduler = PanDAiDDSScheduler(
1084         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
1085     )
1086 
1087     result = scheduler.run_stage(
1088         "optimizer_batch_1_trials",
1089         [
1090             {
1091                 "job_id": "0",
1092                 "name": "trial_0",
1093                 "function": run_trial_workflow,
1094                 "params": {
1095                     "config_path": str(config_file),
1096                     "run_dir": str(tmp_path / "out"),
1097                     "run_work_dir": str(tmp_path / "work"),
1098                     "trial_index": 0,
1099                     "design_point": {"x1": 0.2},
1100                     "workflow_name": None,
1101                     "log_level": "WARNING",
1102                 },
1103             }
1104         ],
1105         parallelism_policy={"poll_interval": 0},
1106         working_dir=str(tmp_path / "work"),
1107     )
1108 
1109     assert result.success is True
1110     assert result.job_statuses[0].outputs == {"objectives": {"objective": 6}}
1111     assert len(state["work_kwargs_history"]) == 3
1112     assert state["work_kwargs_history"][0]["output_dataset_name"] == "user.test.trial.sim.a/"
1113     assert state["work_kwargs_history"][1]["output_dataset_name"] == "user.test.trial.sim.b/"
1114     assert state["work_kwargs_history"][2]["parent_internal_id"] == ["internal-1", "internal-2"]
1115     assert state["work_kwargs_history"][2]["input_datasets"] == {
1116         "input_file_names__aid2e_0": "user.test.trial.sim.a",
1117         "input_file_names__aid2e_1": "user.test.trial.sim.b",
1118     }
1119 
1120 
1121 def test_panda_multistage_graph_job_runs_stage_records(monkeypatch, tmp_path):
1122     state = install_fake_idds(
1123         monkeypatch,
1124         result_sequence=[{"sim": "a"}, {"sim": "b"}, {"f1": 8, "f2": 9}],
1125     )
1126     scheduler = PanDAiDDSScheduler(
1127         PanDAiDDSRunnerConfig(name="user.test.panda", source_dir=str(tmp_path))
1128     )
1129 
1130     result = scheduler.run_stage(
1131         "evaluate",
1132         [
1133             {
1134                 "job_id": "logical:graph",
1135                 "name": "graph_job",
1136                 "payload": {
1137                     "evaluator_type": "panda_multistage_graph",
1138                     "final_stage": "ana",
1139                     "stage_records": [
1140                         {
1141                             "name": "simreco",
1142                             "jobs": [
1143                                 {
1144                                     "name": "sim_a",
1145                                     "payload": {
1146                                         "python_callable": panda_prepare_evaluator,
1147                                         "output_file": "my_test.txt",
1148                                         "output_dataset": "user.test.graph.sim.a",
1149                                     },
1150                                 },
1151                                 {
1152                                     "name": "sim_b",
1153                                     "payload": {
1154                                         "python_callable": panda_prepare_evaluator,
1155                                         "output_file": "my_test.txt",
1156                                         "output_dataset": "user.test.graph.sim.b",
1157                                     },
1158                                 },
1159                             ],
1160                         },
1161                         {
1162                             "name": "ana",
1163                             "jobs": [
1164                                 {
1165                                     "name": "ana_all",
1166                                     "payload": {
1167                                         "python_callable": panda_test_evaluator,
1168                                         "depends_on": "simreco",
1169                                         "dep_type": "datasets",
1170                                         "dep_map": "all2one",
1171                                         "input_datasets": {
1172                                             "input_file_names": [
1173                                                 "user.test.graph.sim.a",
1174                                                 "user.test.graph.sim.b",
1175                                             ]
1176                                         },
1177                                         "produces_objective": True,
1178                                     },
1179                                 }
1180                             ],
1181                         },
1182                     ],
1183                 },
1184             }
1185         ],
1186         parallelism_policy={"poll_interval": 0},
1187         working_dir=str(tmp_path),
1188     )
1189 
1190     assert result.success is True
1191     assert result.artifacts == {"objectives": {"f1": 8, "f2": 9}}
1192     assert len(state["work_kwargs_history"]) == 3
1193     assert state["work_kwargs_history"][2]["parent_internal_id"] == ["internal-1", "internal-2"]
1194     assert state["work_kwargs_history"][2]["input_datasets"] == {
1195         "input_file_names__aid2e_0": "user.test.graph.sim.a",
1196         "input_file_names__aid2e_1": "user.test.graph.sim.b",
1197     }
1198     metrics = result.job_statuses[0].metrics["panda_multistage_stages"]
1199     assert [item["stage"] for item in metrics] == ["simreco", "simreco", "ana"]
1200 
1201 
1202 def test_dtlz2_dataset_simreco_writes_message_output_file(tmp_path, monkeypatch):
1203     from examples.evaluators.dtlz2_panda import panda_stage_simreco
1204 
1205     context = types.SimpleNamespace(
1206         design_point={
1207             "DTLZ2_variables.x1": 0.2,
1208             "DTLZ2_variables.x2": 0.3,
1209             "DTLZ2_variables.x3": 0.4,
1210             "DTLZ2_variables.x4": 0.5,
1211             "DTLZ2_variables.x5": 0.6,
1212         }
1213     )
1214     monkeypatch.chdir(tmp_path)
1215 
1216     result = panda_stage_simreco(
1217         context,
1218         particle="pi+",
1219         eta_point=0.1,
1220         output_file_name="my_test.txt",
1221         output_dataset_name="user.test.aid2e.dtlz2.simreco.eta_0p1_pi",
1222         num_events=200,
1223         num_events_per_job=100,
1224     )
1225 
1226     assert result["particle"] == "pi+"
1227     assert "simreco produced xyz=" in result["message"]
1228     output_path = tmp_path / "my_test.txt"
1229     assert output_path.is_file()
1230 
1231     import json
1232 
1233     data = json.loads(output_path.read_text())
1234     assert data["xyz"] == result["xyz"]
1235     assert data["message"] == result["message"]
1236     assert data["particle"] == "pi+"
1237     assert data["eta_point"] == 0.1
1238     assert not (tmp_path / "user.test.aid2e.dtlz2.simreco.eta_0p1_pi_000001.my_test.txt").exists()
1239     assert not (tmp_path / "user.test.aid2e.dtlz2.simreco.eta_0p1_pi_000002.my_test.txt").exists()
1240 
1241 
1242 def test_dtlz2_dataset_ana_reads_and_prints_simreco_messages(tmp_path, capsys):
1243     from examples.evaluators.dtlz2_panda import panda_stage_ana
1244 
1245     first = tmp_path / "first.json"
1246     second = tmp_path / "second.json"
1247     first.write_text('{"xyz": 1.25, "message": "first simreco message"}\n')
1248     second.write_text('{"xyz": 2.75, "message": "second simreco message"}\n')
1249 
1250     result = panda_stage_ana(
1251         types.SimpleNamespace(design_point={}),
1252         input_file_names=[str(first), str(second)],
1253     )
1254 
1255     captured = capsys.readouterr()
1256     assert result == {"xyz": 4.0, "n_inputs": 2}
1257     assert f"ana read from {first}: first simreco message" in captured.out
1258     assert f"ana read from {second}: second simreco message" in captured.out