Skip to content

Commit 809cd4f

Browse files
committed
add schedule duration live monitoring notebook
1 parent 1f7d1c5 commit 809cd4f

File tree

1 file changed

+311
-0
lines changed

1 file changed

+311
-0
lines changed
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "623b96d7-7c2d-4797-a18d-6a34c79a5296",
6+
"metadata": {},
7+
"source": [
8+
"### Schedule duration monitor\n",
9+
"\n",
10+
"This notebook will monitor - in live time - the duration of the expeditions in directories GROUP1, GROUP2, ... , GROUP66.\n",
11+
"\n",
12+
"The resultant plot once all cells of the notebook are run will be refreshed every n seconds (prescribable in the `REFRESH` constant below).\n",
13+
"\n",
14+
"CTDs are assumed to take approx. 20 minutes each. 3 days sailing time is added to all groups for the outbound journey from Texel to the first waypoint.\n",
15+
"\n",
16+
"<div class=\"alert alert-warning\">\n",
17+
"This script uses an infinite `while` loop to refresh the plotting indefinitely. To stop the running, the \"Interupt the kernel\" button (square button) should be pressed.\n",
18+
"</div>\n"
19+
]
20+
},
21+
{
22+
"cell_type": "code",
23+
"execution_count": 1,
24+
"id": "64a8edb1-46c3-488f-a0dc-d104d47bbaa5",
25+
"metadata": {},
26+
"outputs": [],
27+
"source": [
28+
"import time\n",
29+
"import os\n",
30+
"import sys\n",
31+
"import yaml\n",
32+
"import numpy as np\n",
33+
"from pathlib import Path\n",
34+
"from matplotlib import pyplot as plt\n",
35+
"from IPython.display import clear_output"
36+
]
37+
},
38+
{
39+
"cell_type": "code",
40+
"execution_count": 2,
41+
"id": "d93d52a2-b55d-43b6-bc2e-a56aecbe8fa7",
42+
"metadata": {},
43+
"outputs": [],
44+
"source": [
45+
"# plot refresh rate\n",
46+
"REFRESH = 30 # [seconds]"
47+
]
48+
},
49+
{
50+
"cell_type": "code",
51+
"execution_count": 3,
52+
"id": "2c61c6b5-e729-46d0-bc9e-2f87a447ee76",
53+
"metadata": {},
54+
"outputs": [],
55+
"source": [
56+
"# config\n",
57+
"BASE_DIR = Path(\"/home/shared/data/virtualship_storage/\")\n",
58+
"SAIL_OUT_TIME = np.timedelta64(3, \"D\") # [days]\n",
59+
"CTD_TIME = np.timedelta64(200, \"m\") # [minutes]\n",
60+
"SHIP_TIME_THRESHOLD = 9 # [days]\n",
61+
"ROTATION_CHANGE = 18 # when to change from 45 to 90 degree rotation in x axis labels"
62+
]
63+
},
64+
{
65+
"cell_type": "code",
66+
"execution_count": 4,
67+
"id": "4a91fae1-46d1-4e49-855a-b0bec7b127d9",
68+
"metadata": {
69+
"jupyter": {
70+
"source_hidden": true
71+
}
72+
},
73+
"outputs": [],
74+
"source": [
75+
"def preprocess(\n",
76+
" base_dir: Path, sail_out_time: np.timedelta64, ctd_time: np.timedelta64\n",
77+
") -> dict:\n",
78+
" \"\"\"\n",
79+
" Reads schedule data from YAML files, calculates the total expedition duration\n",
80+
" for each group, and returns a dictionary of valid groups and their durations.\n",
81+
" \"\"\"\n",
82+
" groups = {}\n",
83+
"\n",
84+
" # group directories 1 to 66 (inclusive)\n",
85+
" for i in range(1, 67):\n",
86+
" group_name = f\"GROUP{i}\"\n",
87+
" group_dir = base_dir / group_name\n",
88+
" schedule_file = group_dir / \"schedule.yaml\"\n",
89+
"\n",
90+
" if not schedule_file.exists():\n",
91+
" groups[group_name] = np.nan\n",
92+
" continue\n",
93+
"\n",
94+
" try:\n",
95+
" with open(schedule_file, \"r\", encoding=\"utf-8\") as f:\n",
96+
" schedule_data = yaml.safe_load(f)\n",
97+
"\n",
98+
" waypoints = schedule_data.get(\"waypoints\", [])\n",
99+
"\n",
100+
" if not waypoints:\n",
101+
" groups[group_name] = np.nan\n",
102+
" continue\n",
103+
"\n",
104+
" start_time_str = waypoints[0].get(\"time\")\n",
105+
" end_time_str = waypoints[-1].get(\"time\")\n",
106+
"\n",
107+
" if not start_time_str or not end_time_str:\n",
108+
" groups[group_name] = np.nan\n",
109+
" else:\n",
110+
" start_time, end_time = (\n",
111+
" np.datetime64(start_time_str),\n",
112+
" np.datetime64(end_time_str),\n",
113+
" )\n",
114+
"\n",
115+
" difference = end_time - start_time\n",
116+
" difference_days = difference.astype(\"timedelta64[D]\") # [days]\n",
117+
"\n",
118+
" total_time_timedelta = difference_days + sail_out_time + ctd_time\n",
119+
" total_time_days = total_time_timedelta.astype(\n",
120+
" \"timedelta64[h]\"\n",
121+
" ) / np.timedelta64(24, \"h\")\n",
122+
"\n",
123+
" groups[group_name] = total_time_days.item()\n",
124+
"\n",
125+
" except Exception as e:\n",
126+
" groups[group_name] = np.nan\n",
127+
" print(f\"Error processing {group_name}: {e}\", file=sys.stderr)\n",
128+
"\n",
129+
" # filter dict to remove groups with NaN\n",
130+
" filter_groups = {key: value for key, value in groups.items() if not np.isnan(value)}\n",
131+
"\n",
132+
" return filter_groups"
133+
]
134+
},
135+
{
136+
"cell_type": "code",
137+
"execution_count": null,
138+
"id": "786e2cb8-1dc0-46c0-a454-37ac9e603329",
139+
"metadata": {
140+
"jupyter": {
141+
"source_hidden": true
142+
}
143+
},
144+
"outputs": [],
145+
"source": [
146+
"def plot(filter_groups: dict, ship_time_threshold: float, rotation_change: int):\n",
147+
" \"\"\"\n",
148+
" Generates a bar plot showing the expedition duration for each group\n",
149+
" relative to the ship time threshold.\n",
150+
" \"\"\"\n",
151+
" groups_keys = list(filter_groups.keys())\n",
152+
" groups_values = list(filter_groups.values())\n",
153+
"\n",
154+
" # bar colors dependent on whether above or below ship time threshold\n",
155+
" bar_colors = [\n",
156+
" \"crimson\" if value > ship_time_threshold else \"mediumseagreen\"\n",
157+
" for value in groups_values\n",
158+
" ]\n",
159+
"\n",
160+
" # fig\n",
161+
" fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(16, 8), dpi=300)\n",
162+
"\n",
163+
" # bars\n",
164+
" ax.bar(\n",
165+
" groups_keys,\n",
166+
" groups_values,\n",
167+
" color=bar_colors,\n",
168+
" edgecolor=\"k\",\n",
169+
" linewidth=1.5,\n",
170+
" zorder=3,\n",
171+
" width=0.7,\n",
172+
" )\n",
173+
"\n",
174+
" # labels and title\n",
175+
" ax.set_ylabel(\"Days\", fontsize=15)\n",
176+
" ax.set_title(\"Expedition Duration\", fontsize=20)\n",
177+
"\n",
178+
" # customise ticks\n",
179+
" ax.set_xticks(ax.get_xticks())\n",
180+
" rotation = 45 if len(groups_values) <= rotation_change else 90\n",
181+
" ax.set_xticklabels(groups_keys, rotation=rotation, ha=\"center\", fontsize=15)\n",
182+
" ax.tick_params(axis=\"y\", labelsize=15)\n",
183+
"\n",
184+
" # set y-limit based on the maximum valid value\n",
185+
" max_duration = np.nanmax(groups_values) if groups_values else 0\n",
186+
" ax.set_ylim(0, max_duration + 0.5)\n",
187+
"\n",
188+
" # grid\n",
189+
" ax.set_facecolor(\"gainsboro\")\n",
190+
" ax.grid(axis=\"y\", linestyle=\"-\", alpha=1.0, color=\"white\")\n",
191+
"\n",
192+
" # horizontal line for threshold days\n",
193+
" ax.axhline(\n",
194+
" y=ship_time_threshold,\n",
195+
" color=\"r\",\n",
196+
" linestyle=\"--\",\n",
197+
" linewidth=2.5,\n",
198+
" label=\"Ship-time limit\",\n",
199+
" zorder=2,\n",
200+
" )\n",
201+
"\n",
202+
" plt.legend(fontsize=15, loc=\"upper left\")\n",
203+
" plt.tight_layout()\n",
204+
" plt.show()"
205+
]
206+
},
207+
{
208+
"cell_type": "code",
209+
"execution_count": 6,
210+
"id": "73a78ef2-1b58-48be-802f-0a447c9549a2",
211+
"metadata": {
212+
"jupyter": {
213+
"source_hidden": true
214+
}
215+
},
216+
"outputs": [],
217+
"source": [
218+
"def periodic_task(interval_seconds: int):\n",
219+
" \"\"\"\n",
220+
" Loop that runs the preprocessing and plotting functions periodically.\n",
221+
" :param interval_seconds: The number of seconds to wait between runs.\n",
222+
" \"\"\"\n",
223+
" while True:\n",
224+
" try:\n",
225+
" clear_output(wait=True)\n",
226+
" print(f\"--- Running task at {time.ctime()} ---\")\n",
227+
"\n",
228+
" data = preprocess(BASE_DIR, SAIL_OUT_TIME, CTD_TIME)\n",
229+
"\n",
230+
" if data:\n",
231+
" plot(data, SHIP_TIME_THRESHOLD, ROTATION_CHANGE)\n",
232+
" else:\n",
233+
" print(\"No valid data found to plot.\")\n",
234+
"\n",
235+
" except KeyboardInterrupt:\n",
236+
" print(\"\\nPeriodic task interrupted by user.\")\n",
237+
" break\n",
238+
" except Exception as e:\n",
239+
" print(f\"\\nAn error occurred during the periodic run: {e}\")\n",
240+
"\n",
241+
" time.sleep(interval_seconds)"
242+
]
243+
},
244+
{
245+
"cell_type": "code",
246+
"execution_count": 7,
247+
"id": "939a0a6b-9261-4e34-80f0-168e9bb133dd",
248+
"metadata": {
249+
"collapsed": true,
250+
"jupyter": {
251+
"outputs_hidden": true
252+
},
253+
"scrolled": true
254+
},
255+
"outputs": [
256+
{
257+
"name": "stdout",
258+
"output_type": "stream",
259+
"text": [
260+
"--- Running task at Wed Nov 12 21:45:02 2025 ---\n",
261+
"No valid data found to plot.\n"
262+
]
263+
},
264+
{
265+
"ename": "KeyboardInterrupt",
266+
"evalue": "",
267+
"output_type": "error",
268+
"traceback": [
269+
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
270+
"\u001b[31mKeyboardInterrupt\u001b[39m Traceback (most recent call last)",
271+
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43mperiodic_task\u001b[49m\u001b[43m(\u001b[49m\u001b[43minterval_seconds\u001b[49m\u001b[43m \u001b[49m\u001b[43m=\u001b[49m\u001b[43m \u001b[49m\u001b[43mREFRESH\u001b[49m\u001b[43m)\u001b[49m\n",
272+
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[6]\u001b[39m\u001b[32m, line 24\u001b[39m, in \u001b[36mperiodic_task\u001b[39m\u001b[34m(interval_seconds)\u001b[39m\n\u001b[32m 21\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m 22\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33mAn error occurred during the periodic run: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00me\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m---> \u001b[39m\u001b[32m24\u001b[39m \u001b[43mtime\u001b[49m\u001b[43m.\u001b[49m\u001b[43msleep\u001b[49m\u001b[43m(\u001b[49m\u001b[43minterval_seconds\u001b[49m\u001b[43m)\u001b[49m\n",
273+
"\u001b[31mKeyboardInterrupt\u001b[39m: "
274+
]
275+
}
276+
],
277+
"source": [
278+
"periodic_task(interval_seconds=REFRESH)"
279+
]
280+
},
281+
{
282+
"cell_type": "code",
283+
"execution_count": null,
284+
"id": "be0a7bfd-dc2a-437a-8fa6-ffb78cdd2687",
285+
"metadata": {},
286+
"outputs": [],
287+
"source": []
288+
}
289+
],
290+
"metadata": {
291+
"kernelspec": {
292+
"display_name": "virtualship",
293+
"language": "python",
294+
"name": "virtualship"
295+
},
296+
"language_info": {
297+
"codemirror_mode": {
298+
"name": "ipython",
299+
"version": 3
300+
},
301+
"file_extension": ".py",
302+
"mimetype": "text/x-python",
303+
"name": "python",
304+
"nbconvert_exporter": "python",
305+
"pygments_lexer": "ipython3",
306+
"version": "3.12.12"
307+
}
308+
},
309+
"nbformat": 4,
310+
"nbformat_minor": 5
311+
}

0 commit comments

Comments
 (0)