Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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)