Skip to content
Open
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
108 changes: 80 additions & 28 deletions src/virtualship/make_realistic/problems/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
from pathlib import Path
from typing import TYPE_CHECKING

from rich import box
from rich.console import Console
from rich.live import Live
from rich.spinner import Spinner
from rich.table import Table
from yaspin import yaspin

from virtualship.instruments.types import InstrumentType
Expand Down Expand Up @@ -35,7 +40,7 @@
"pre_departure": "Hang on! There could be a pre-departure problem in-port...",
"during_expedition": "Oh no, a problem has occurred during the expedition, at waypoint {waypoint}...!",
"schedule_problems": "This problem will cause a delay of {delay_duration} hours {problem_wp}. The next waypoint therefore cannot be reached in time. Please account for this in your schedule (`virtualship plan` or directly in {expedition_yaml}), then continue the expedition by executing the `virtualship run` command again.\n",
"problem_avoided": "Phew! You had enough contingency time scheduled to avoid delays from this problem. The expedition can carry on shortly...\n",
"problem_avoided": "Phew! You had enough contingency time scheduled to avoid delays from this problem.\n",
}


Expand Down Expand Up @@ -186,7 +191,7 @@ def execute(
problems: dict[str, list[GeneralProblem | InstrumentProblem] | None],
instrument_type_validation: InstrumentType | None,
problems_dir: Path,
log_delay: float = 7.0,
log_delay: float = 4.0,
):
"""
Execute the selected problems, returning messaging and delay times.
Expand Down Expand Up @@ -310,33 +315,18 @@ def _log_problem(
time.sleep(log_delay)
spinner.ok("💥 ")

print("\nPROBLEM ENCOUNTERED: " + problem.message + "\n")

result_msg = "\nRESULT: " + LOG_MESSAGING["schedule_problems"].format(
delay_duration=problem.delay_duration.total_seconds() / 3600.0,
problem_wp=(
"in-port"
if problem_waypoint_i is None
else f"at waypoint {problem_waypoint_i + 1}"
),
expedition_yaml=EXPEDITION,
)

self._hash_to_json(
problem,
problem_hash,
problem_waypoint_i,
hash_fpath,
)

# check if enough contingency time has been scheduled to avoid delay affecting future waypoints
with yaspin(text="Assessing impact on expedition schedule..."):
time.sleep(5.0)

has_contingency = self._has_contingency(problem, problem_waypoint_i)

if has_contingency:
print(LOG_MESSAGING["problem_avoided"])
impact_str = LOG_MESSAGING["problem_avoided"]
result_str = "The expedition will carry on shortly as planned."

# update problem json to resolved = True
with open(hash_fpath, encoding="utf-8") as f:
Expand All @@ -345,20 +335,19 @@ def _log_problem(
with open(hash_fpath, "w", encoding="utf-8") as f_out:
json.dump(problem_json, f_out, indent=4)

with yaspin(): # time to read message before simulation continues
time.sleep(7.0)
return

else:
affected = (
"in-port"
if problem_waypoint_i is None
else f"at waypoint {problem_waypoint_i + 1}"
)
print(
f"\nNot enough contingency time scheduled to mitigate delay of {problem.delay_duration.total_seconds() / 3600.0} hours occuring {affected} (future waypoint(s) would be reached too late).\n"

impact_str = f"Not enough contingency time scheduled to mitigate delay of {problem.delay_duration.total_seconds() / 3600.0} hours occuring {affected} (future waypoint(s) would be reached too late).\n"
result_str = LOG_MESSAGING["schedule_problems"].format(
delay_duration=problem.delay_duration.total_seconds() / 3600.0,
problem_wp=affected,
expedition_yaml=EXPEDITION,
)
print(result_msg)

# save checkpoint
checkpoint = Checkpoint(
Expand All @@ -369,8 +358,18 @@ def _log_problem(
) # failed waypoint index then becomes the one after the one where the problem occurred; as this is when scheduling issues would be run into; for pre-departure problems this is the first waypoint
_save_checkpoint(checkpoint, self.expedition_dir)

# pause simulation
sys.exit(0)
# display tabular output in
self._tabular_outputter(
problem_str=problem.message,
impact_str=impact_str,
result_str=result_str,
has_contingency=has_contingency,
)

if has_contingency:
return # continue expedition as normal
else:
sys.exit(0) # pause simulation

def _has_contingency(
self,
Expand Down Expand Up @@ -434,3 +433,56 @@ def _cache_original_schedule(schedule: Schedule, path: Path | str):
schedule_original = Checkpoint(past_schedule=schedule)
schedule_original.to_yaml(path)
print(f"\nOriginal schedule cached to {path}.\n")

@staticmethod
def _tabular_outputter(problem_str, impact_str, result_str, has_contingency: bool):
"""Display the problem, impact, and result in a live-updating table. Sleep times are included to increase readability and engagement for user."""
console = Console()
console.print() # line break before table

col_kwargs = dict(ratio=1, no_wrap=False, max_width=None, justify="left")

def make_table(problem, impact, result, col_kwargs, colour_results=False):
table = Table(box=box.SIMPLE, expand=True)
table.add_column("Problem Encountered", **col_kwargs)
table.add_column("Impact on schedule", **col_kwargs)

if colour_results:
style = "green1" if has_contingency else "red1"
table.add_column("Result", style=style, **col_kwargs)
else:
table.add_column("Result", **col_kwargs)

table.add_row(problem, impact, result)
return table

empty_spinner = Spinner("dots", text="")
impact_spinner = Spinner("dots", text="Assessing impact on schedule...")

with Live(console=console, refresh_per_second=10) as live:
# stage 0: empty table
table = make_table(empty_spinner, empty_spinner, empty_spinner, col_kwargs)
live.update(table)
time.sleep(3.0)

# stage 1: show problem
table = make_table(problem_str, empty_spinner, empty_spinner, col_kwargs)
live.update(table)
time.sleep(3.0)

# stage 2: spinner in "Impact on schedule" column
table = make_table(problem_str, impact_spinner, empty_spinner, col_kwargs)
live.update(table)
time.sleep(7.0)

# stage 3: table with problem and impact-investigation complete
table = make_table(problem_str, impact_str, empty_spinner, col_kwargs)
live.update(table)
time.sleep(4.0)

# stage 4: complete table with problem, impact, and result (give final outcome colour based on fail/success)
table = make_table(
problem_str, impact_str, result_str, col_kwargs, colour_results=True
)
live.update(table)
time.sleep(3.0)