Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 48 additions & 14 deletions src/virtualship/cli/_run.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""do_expedition function."""

import glob
import logging
import os
import shutil
Expand All @@ -14,11 +15,13 @@
ScheduleProblem,
simulate_schedule,
)
from virtualship.models import Schedule
from virtualship.models.checkpoint import Checkpoint
from virtualship.make_realistic.problems.simulator import ProblemSimulator
from virtualship.models import Checkpoint, Schedule
from virtualship.utils import (
CHECKPOINT,
PROBLEMS_ENCOUNTERED_DIR,
_get_expedition,
_save_checkpoint,
expedition_cost,
get_instrument_class,
)
Expand All @@ -35,7 +38,10 @@
logging.getLogger("copernicusmarine").setLevel("ERROR")


def _run(expedition_dir: str | Path, from_data: Path | None = None) -> None:
# TODO: prob-level needs to be parsed from CLI args; currently set to 1 override for testing purposes
def _run(
expedition_dir: str | Path, from_data: Path | None = None, prob_level: int = 1
) -> None:
"""
Perform an expedition, providing terminal feedback and file output.

Expand Down Expand Up @@ -73,16 +79,16 @@ def _run(expedition_dir: str | Path, from_data: Path | None = None) -> None:

expedition = _get_expedition(expedition_dir)

# Verify instruments_config file is consistent with schedule
# verify instruments_config file is consistent with schedule
expedition.instruments_config.verify(expedition)

# load last checkpoint
checkpoint = _load_checkpoint(expedition_dir)
if checkpoint is None:
checkpoint = Checkpoint(past_schedule=Schedule(waypoints=[]))

# verify that schedule and checkpoint match
checkpoint.verify(expedition.schedule)
# verify that schedule and checkpoint match, and that problems have been resolved
checkpoint.verify(expedition.schedule, expedition_dir)

print("\n---- WAYPOINT VERIFICATION ----")

Expand All @@ -96,17 +102,16 @@ def _run(expedition_dir: str | Path, from_data: Path | None = None) -> None:
projection=projection,
expedition=expedition,
)

# handle cases where user defined schedule is incompatible (i.e. not enough time between waypoints, not problems)
if isinstance(schedule_results, ScheduleProblem):
print(
f"SIMULATION PAUSED: update your schedule (`virtualship plan`) and continue the expedition by executing the `virtualship run` command again.\nCheckpoint has been saved to {expedition_dir.joinpath(CHECKPOINT)}."
)
_save_checkpoint(
Checkpoint(
past_schedule=Schedule(
waypoints=expedition.schedule.waypoints[
: schedule_results.failed_waypoint_i
]
)
past_schedule=expedition.schedule,
failed_waypoint_i=schedule_results.failed_waypoint_i,
),
expedition_dir,
)
Expand All @@ -124,12 +129,30 @@ def _run(expedition_dir: str | Path, from_data: Path | None = None) -> None:

print("\n--- MEASUREMENT SIMULATIONS ---")

# identify problems
# TODO: prob_level needs to be parsed from CLI args
problem_simulator = ProblemSimulator(
expedition.schedule, prob_level, expedition_dir
)
problems = problem_simulator.select_problems()

# simulate measurements
print("\nSimulating measurements. This may take a while...\n")

# TODO: logic for getting simulations to carry on from last checkpoint! Building on .zarr files already created...

instruments_in_expedition = expedition.get_instruments()

for itype in instruments_in_expedition:
#! TODO: move this to before the loop; determine problem selection based on instruments_in_expedition to ensure only relevant problems are selected; and then instrument problems are propagated to within the loop
# TODO: instrument-specific problems at different waypoints are where see if can get time savings by not re-simulating everything from scratch... but if it's too complex than just leave for now
# propagate problems
if problems:
problem_simulator.execute(
problems=problems,
instrument_type=itype,
)

# get instrument class
instrument_class = get_instrument_class(itype)
if instrument_class is None:
Expand Down Expand Up @@ -158,6 +181,13 @@ def _run(expedition_dir: str | Path, from_data: Path | None = None) -> None:
print(
f"Your measurements can be found in the '{expedition_dir}/results' directory."
)

if problems:
print("\n----- RECORD OF PROBLEMS ENCOUNTERED ------")
print(
f"\nA record of problems encountered during the expedition is saved in: {expedition_dir.joinpath(PROBLEMS_ENCOUNTERED_DIR)}"
)

print("\n------------- END -------------\n")

# end timing
Expand All @@ -174,9 +204,13 @@ def _load_checkpoint(expedition_dir: Path) -> Checkpoint | None:
return None


def _save_checkpoint(checkpoint: Checkpoint, expedition_dir: Path) -> None:
file_path = expedition_dir.joinpath(CHECKPOINT)
checkpoint.to_yaml(file_path)
def _load_hashes(expedition_dir: Path) -> set[str]:
hashes_path = expedition_dir.joinpath(PROBLEMS_ENCOUNTERED_DIR)
if not hashes_path.exists():
return set()
hash_files = glob.glob(str(hashes_path / "problem_*.txt"))
hashes = {Path(f).stem.split("_")[1] for f in hash_files}
return hashes


def _write_expedition_cost(expedition, schedule_results, expedition_dir):
Expand Down
6 changes: 6 additions & 0 deletions src/virtualship/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,9 @@ class CopernicusCatalogueError(Exception):
"""Error raised when a relevant product is not found in the Copernicus Catalogue."""

pass


class ProblemEncountered(Exception):
"""Error raised when a problem is encountered during simulation."""

pass
8 changes: 6 additions & 2 deletions src/virtualship/expedition/simulate_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import ClassVar
from typing import TYPE_CHECKING, ClassVar

import pyproj

Expand All @@ -21,6 +21,9 @@
Waypoint,
)

if TYPE_CHECKING:
pass


@dataclass
class ScheduleOk:
Expand Down Expand Up @@ -124,7 +127,8 @@ def simulate(self) -> ScheduleOk | ScheduleProblem:
print(
f"Waypoint {wp_i + 1} could not be reached in time. Current time: {self._time}. Waypoint time: {waypoint.time}."
"\n\nHave you ensured that your schedule includes sufficient time for taking measurements, e.g. CTD casts (in addition to the time it takes to sail between waypoints)?\n"
"**Note**, the `virtualship plan` tool will not account for measurement times when verifying the schedule, only the time it takes to sail between waypoints.\n"
"**Hint #1**, the `virtualship plan` tool will not account for measurement times when verifying the schedule, only the time it takes to sail between waypoints.\n"
"**Hint #2**: if you previously encountered any unforeseen delays (e.g. equipment failure, pre-departure delays) during your expedition, you will need to adjust the timings of **all** waypoints after the affected waypoint, not just the next one."
)
return ScheduleProblem(self._time, wp_i)
else:
Expand Down
Loading
Loading