From 5f3e7ff0c704e8ec9b518379c0208d466916cd09 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:27:17 +0100 Subject: [PATCH 1/2] new logic to replace an incompatbile waypoint and instrument problem with a general problem instead --- .../make_realistic/problems/simulator.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/virtualship/make_realistic/problems/simulator.py b/src/virtualship/make_realistic/problems/simulator.py index ee206e9a..dcffbbff 100644 --- a/src/virtualship/make_realistic/problems/simulator.py +++ b/src/virtualship/make_realistic/problems/simulator.py @@ -181,8 +181,26 @@ def select_problems( else: if available_idxs: wp_select = random.choice(available_idxs) + + # fmt: off + # check waypoint actually deploys the instrument associated with the problem...if not, replace it with a general (non-instrument related) problem + # rather than a different waypoint, because it's possible no applicable waypoint is still available + wp_instruments = self.expedition.schedule.waypoints[wp_select].instrument + if isinstance(problem, InstrumentProblem) and problem.instrument_type not in wp_instruments: + available_general = [p for p in GENERAL_PROBLEMS if not p.pre_departure and p not in selected_problems] + + if not available_general: + unassigned_problems.append(problem) + continue + + replacement = random.choice(available_general) + problem_idx = selected_problems.index(problem) + selected_problems[problem_idx] = replacement + # fmt: on + waypoint_idxs.append(wp_select) available_idxs.remove(wp_select) # each waypoint only used once + else: unassigned_problems.append( problem From 10fc1d9e202c6da4862f207c10e77f4806f535a5 Mon Sep 17 00:00:00 2001 From: j-atkins <106238905+j-atkins@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:27:37 +0100 Subject: [PATCH 2/2] update tests --- .../make_realistic/problems/test_simulator.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/make_realistic/problems/test_simulator.py b/tests/make_realistic/problems/test_simulator.py index 6a3f5558..ccd1de18 100644 --- a/tests/make_realistic/problems/test_simulator.py +++ b/tests/make_realistic/problems/test_simulator.py @@ -267,3 +267,54 @@ def test_post_expedition_report(tmp_path): assert problem.message in content, ( "Problem messages in report should match those of selected problems." ) + + +def test_instrument_problems_only_selected_when_instruments_present(tmp_path): + expedition = _make_simple_expedition(num_waypoints=3, no_instruments=True) + instruments_in_expedition = expedition.get_instruments() + assert len(instruments_in_expedition) == 0, "Expedition should have no instruments" + + simulator = ProblemSimulator(expedition, str(tmp_path)) + problems = simulator.select_problems( + instruments_in_expedition, difficulty_level="hard" + ) + + has_instrument_problems = any( + isinstance(cls, InstrumentProblem) for cls in problems["problem_class"] + ) + assert not has_instrument_problems, ( + "Should not select instrument problems when no instruments are present" + ) + + +def test_instrument_not_present_doesnt_select_instrument_problem(tmp_path): + expedition = _make_simple_expedition(num_waypoints=3, no_instruments=True) + + # prescribe instruments at waypoints, for this test case each should only be present at one waypoint + expedition.schedule.waypoints[0].instrument = [InstrumentType.CTD] + expedition.schedule.waypoints[1].instrument = [ + InstrumentType.ARGO_FLOAT, + InstrumentType.DRIFTER, + ] + + instruments_in_expedition = expedition.get_instruments() + simulator = ProblemSimulator(expedition, str(tmp_path)) + + # run many iterations of randomly selecting problems and check that if an instrument problem is selected, the associated instrument is actually present at the selected waypoint + for _ in range(int(1e4)): + problems = simulator.select_problems( + instruments_in_expedition, difficulty_level="hard" + ) + + for problem, wp_i in zip( + problems["problem_class"], problems["waypoint_i"], strict=False + ): + if isinstance(problem, InstrumentProblem): + wp_instruments = expedition.schedule.waypoints[wp_i].instrument + assert problem.instrument_type in wp_instruments, ( + "Instrument problem should only be selected if the instrument is present at the selected waypoint" + ) + + # any incompatible waypoint x instrument problem combinations should have been replaced by a general problem + else: + assert isinstance(problem, GeneralProblem)