Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
CHANGED
|
@@ -382,6 +382,9 @@ _ICON_PATHS = {
|
|
| 382 |
'<path d="M15 8l2.5 5a2.5 2.5 0 0 0 5 0L20 8"/><path d="M9 20h6"/>',
|
| 383 |
"list": '<path d="M8 6h12M8 12h12M8 18h12"/><path d="M4 6h.01M4 12h.01M4 18h.01"/>',
|
| 384 |
"bars": '<path d="M4 20V10M10 20V4M16 20v-7M4 20h16"/>',
|
|
|
|
|
|
|
|
|
|
| 385 |
}
|
| 386 |
|
| 387 |
def icon_svg(name, size=18):
|
|
@@ -393,16 +396,16 @@ def section_header(icon, text, level=3):
|
|
| 393 |
return f'<h{level} class="hort-heading">{icon_svg(icon)}<span>{text}</span></h{level}>'
|
| 394 |
|
| 395 |
def build_metric_html(df_stats, logs_df):
|
| 396 |
-
"""Live
|
| 397 |
showing fixed placeholder copy."""
|
| 398 |
if df_stats is None or df_stats.empty:
|
| 399 |
trains_n, conflicts_n, avg_delay, on_time_pct = "-", "-", "-", "-"
|
| 400 |
flag_conflicts = False
|
| 401 |
else:
|
| 402 |
trains_n = str(len(df_stats))
|
| 403 |
-
avg_delay = f"{df_stats['Total Delay'].mean():.1f}
|
| 404 |
on_time = int((df_stats['Type'] == 'On Time').sum())
|
| 405 |
-
on_time_pct =
|
| 406 |
if logs_df is not None and not logs_df.empty:
|
| 407 |
conflicts = sum(1 for v in logs_df.iloc[:, 0] if isinstance(v, str) and v.startswith("Iter"))
|
| 408 |
else:
|
|
@@ -410,15 +413,28 @@ def build_metric_html(df_stats, logs_df):
|
|
| 410 |
conflicts_n = str(conflicts)
|
| 411 |
flag_conflicts = conflicts > 0
|
| 412 |
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
return f'''
|
| 417 |
-
<div class="
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
</div>
|
| 423 |
'''
|
| 424 |
|
|
@@ -1158,24 +1174,16 @@ html, body, gradio-app {
|
|
| 1158 |
overflow: visible !important;
|
| 1159 |
position: relative;
|
| 1160 |
}
|
| 1161 |
-
/*
|
| 1162 |
-
|
| 1163 |
-
Fixed + pointer-events:none so it never repaints on scroll. */
|
| 1164 |
.gradio-container::before {
|
| 1165 |
content: "";
|
| 1166 |
position: fixed;
|
| 1167 |
inset: 0;
|
| 1168 |
z-index: 0;
|
| 1169 |
pointer-events: none;
|
| 1170 |
-
background:
|
| 1171 |
-
radial-gradient(ellipse 900px 420px at 50% -8%, rgba(61,220,132,0.05), transparent 70%),
|
| 1172 |
-
repeating-linear-gradient(
|
| 1173 |
-
180deg,
|
| 1174 |
-
rgba(231,236,231,0.014) 0px,
|
| 1175 |
-
rgba(231,236,231,0.014) 1px,
|
| 1176 |
-
transparent 1px,
|
| 1177 |
-
transparent 3px
|
| 1178 |
-
);
|
| 1179 |
}
|
| 1180 |
|
| 1181 |
.gradio-container h1, .gradio-container h2, .gradio-container h3, .gradio-container h4,
|
|
@@ -1185,6 +1193,17 @@ html, body, gradio-app {
|
|
| 1185 |
color: var(--hort-text) !important;
|
| 1186 |
font-family: 'JetBrains Mono', 'IBM Plex Mono', 'SF Mono', monospace !important;
|
| 1187 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1188 |
/* Prose (paragraph copy) reads better in a humanist sans than all-mono -
|
| 1189 |
headings, labels, data and buttons stay mono for the technical voice. */
|
| 1190 |
.gradio-container .prose p, .gradio-container .markdown p, .gradio-container .gr-markdown p {
|
|
@@ -1195,141 +1214,132 @@ html, body, gradio-app {
|
|
| 1195 |
}
|
| 1196 |
|
| 1197 |
/* ---------------------------------------------------------------------
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
|
|
|
|
|
|
| 1201 |
--------------------------------------------------------------------- */
|
| 1202 |
-
.
|
| 1203 |
-
margin: 26px 0 22px 0;
|
| 1204 |
-
border: 1px solid var(--hort-border);
|
| 1205 |
-
border-radius: 4px;
|
| 1206 |
-
background: var(--hort-bg-1);
|
| 1207 |
-
overflow: hidden;
|
| 1208 |
-
/* Faint top bezel highlight - the one piece of real depth this theme
|
| 1209 |
-
allows itself, echoing a physical console panel catching light. */
|
| 1210 |
-
box-shadow: inset 0 1px 0 rgba(231,236,231,0.05);
|
| 1211 |
-
}
|
| 1212 |
-
.term-titlebar {
|
| 1213 |
display: flex;
|
| 1214 |
align-items: center;
|
| 1215 |
-
gap:
|
| 1216 |
-
padding:
|
|
|
|
| 1217 |
border-bottom: 1px solid var(--hort-border);
|
| 1218 |
-
background: rgba(255,255,255,0.015);
|
| 1219 |
}
|
| 1220 |
-
.
|
| 1221 |
-
width:
|
| 1222 |
-
height:
|
| 1223 |
-
border-radius:
|
| 1224 |
background: var(--hort-cyan);
|
|
|
|
|
|
|
|
|
|
| 1225 |
flex: 0 0 auto;
|
| 1226 |
-
box-shadow: 0 0
|
| 1227 |
-
animation: term-pulse 2.6s ease-in-out infinite;
|
| 1228 |
-
}
|
| 1229 |
-
@keyframes term-pulse {
|
| 1230 |
-
0%, 100% { opacity: 1; }
|
| 1231 |
-
50% { opacity: .45; }
|
| 1232 |
-
}
|
| 1233 |
-
@media (prefers-reduced-motion: reduce) {
|
| 1234 |
-
.term-dot { animation: none; }
|
| 1235 |
-
}
|
| 1236 |
-
.term-path {
|
| 1237 |
-
font-size: 12px;
|
| 1238 |
-
color: var(--hort-muted) !important;
|
| 1239 |
-
letter-spacing: .02em;
|
| 1240 |
}
|
| 1241 |
-
.
|
| 1242 |
-
.
|
| 1243 |
-
font-
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
font-weight: 500;
|
| 1247 |
-
}
|
| 1248 |
-
.hero-title {
|
| 1249 |
-
font-size: clamp(30px, 4vw, 46px);
|
| 1250 |
-
line-height: 1.1;
|
| 1251 |
-
margin: 14px 0 16px;
|
| 1252 |
-
color: var(--hort-text) !important;
|
| 1253 |
-
font-weight: 700;
|
| 1254 |
letter-spacing: -0.01em;
|
| 1255 |
-
|
| 1256 |
-
}
|
| 1257 |
-
.hero-subtitle {
|
| 1258 |
-
max-width: 700px;
|
| 1259 |
-
font-family: 'Space Grotesk', 'Helvetica Neue', Arial, sans-serif !important;
|
| 1260 |
-
font-size: 15px;
|
| 1261 |
-
line-height: 1.75;
|
| 1262 |
-
color: var(--hort-muted) !important;
|
| 1263 |
-
}
|
| 1264 |
-
.hero-chips {
|
| 1265 |
-
display: flex;
|
| 1266 |
-
flex-wrap: wrap;
|
| 1267 |
-
gap: 8px;
|
| 1268 |
-
margin-top: 24px;
|
| 1269 |
}
|
| 1270 |
-
.
|
|
|
|
| 1271 |
display: inline-flex;
|
| 1272 |
align-items: center;
|
| 1273 |
-
|
| 1274 |
-
|
| 1275 |
-
|
| 1276 |
-
|
| 1277 |
-
color: var(--hort-muted) !important;
|
| 1278 |
-
font-size: 11px;
|
| 1279 |
-
font-weight: 500;
|
| 1280 |
-
letter-spacing: .03em;
|
| 1281 |
-
transition: border-color .18s ease, color .18s ease;
|
| 1282 |
}
|
| 1283 |
-
.
|
| 1284 |
-
|
| 1285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1286 |
}
|
| 1287 |
-
.hero-chip .br { color: var(--hort-cyan); }
|
| 1288 |
|
| 1289 |
-
/*
|
| 1290 |
-
|
| 1291 |
-
.
|
| 1292 |
display: grid;
|
| 1293 |
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
background: var(--hort-bg-1);
|
| 1297 |
-
box-shadow: inset 0 1px 0 rgba(231,236,231,0.04);
|
| 1298 |
-
margin: 4px 0 30px 0;
|
| 1299 |
-
}
|
| 1300 |
-
.metric-item {
|
| 1301 |
-
padding: 16px 22px;
|
| 1302 |
-
border-right: 1px solid var(--hort-border);
|
| 1303 |
}
|
| 1304 |
-
.
|
| 1305 |
-
|
| 1306 |
-
|
| 1307 |
-
|
| 1308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1309 |
}
|
| 1310 |
-
.
|
| 1311 |
-
.
|
| 1312 |
-
|
| 1313 |
-
|
| 1314 |
-
|
| 1315 |
-
font-weight: 700;
|
| 1316 |
-
margin-top: 6px;
|
| 1317 |
-
text-shadow: 0 0 14px rgba(61,220,132,0.20);
|
| 1318 |
}
|
| 1319 |
-
|
| 1320 |
-
|
| 1321 |
-
text-shadow: 0 0 14px rgba(255,177,0,0.24);
|
| 1322 |
}
|
| 1323 |
-
|
| 1324 |
-
|
| 1325 |
-
|
| 1326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1327 |
}
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
|
|
|
|
|
|
|
|
|
| 1332 |
}
|
|
|
|
|
|
|
|
|
|
| 1333 |
|
| 1334 |
/* Section-header icon + label, replacing emoji glyphs. Bolder stroke reads
|
| 1335 |
closer to a Phosphor "Bold" weight than a generic thin-line icon set. */
|
|
@@ -1357,7 +1367,7 @@ html, body, gradio-app {
|
|
| 1357 |
overflow: visible !important;
|
| 1358 |
background: var(--hort-bg-1) !important;
|
| 1359 |
border: 1px solid var(--hort-border) !important;
|
| 1360 |
-
border-radius:
|
| 1361 |
padding: 24px !important;
|
| 1362 |
box-shadow: inset 0 1px 0 rgba(231,236,231,0.04) !important;
|
| 1363 |
backdrop-filter: none !important;
|
|
@@ -1400,7 +1410,7 @@ html, body, gradio-app {
|
|
| 1400 |
.gradio-container textarea,
|
| 1401 |
.gradio-container select,
|
| 1402 |
.gradio-container button {
|
| 1403 |
-
border-radius:
|
| 1404 |
font-family: 'JetBrains Mono', 'IBM Plex Mono', 'SF Mono', monospace !important;
|
| 1405 |
}
|
| 1406 |
.gradio-container input,
|
|
@@ -1422,7 +1432,7 @@ html, body, gradio-app {
|
|
| 1422 |
Keep dropdowns native. Do not style Gradio's internal combobox/listbox/menu/button
|
| 1423 |
elements; doing so can make the menu jump, flicker, or open in the wrong place. */
|
| 1424 |
.hort-listbox {
|
| 1425 |
-
border-radius:
|
| 1426 |
}
|
| 1427 |
.hort-listbox label,
|
| 1428 |
.hort-listbox .label-wrap {
|
|
@@ -1455,7 +1465,7 @@ html, body, gradio-app {
|
|
| 1455 |
.gradio-container fieldset {
|
| 1456 |
background: transparent !important;
|
| 1457 |
border: 1px solid var(--hort-border) !important;
|
| 1458 |
-
border-radius:
|
| 1459 |
}
|
| 1460 |
.hort-radio fieldset {
|
| 1461 |
padding: 6px !important;
|
|
@@ -1468,7 +1478,7 @@ html, body, gradio-app {
|
|
| 1468 |
gap: 8px;
|
| 1469 |
margin: 0 !important;
|
| 1470 |
padding: 10px 14px !important;
|
| 1471 |
-
border-radius:
|
| 1472 |
background: transparent !important;
|
| 1473 |
border: 1px solid transparent !important;
|
| 1474 |
color: var(--hort-muted) !important;
|
|
@@ -1483,7 +1493,7 @@ html, body, gradio-app {
|
|
| 1483 |
.hort-radio .selected {
|
| 1484 |
background: var(--hort-cyan-bg) !important;
|
| 1485 |
border: 1px solid rgba(61,220,132,0.35) !important;
|
| 1486 |
-
border-radius:
|
| 1487 |
box-shadow: 0 0 12px rgba(61,220,132,0.08) !important;
|
| 1488 |
color: var(--hort-cyan) !important;
|
| 1489 |
font-weight: 700 !important;
|
|
@@ -1574,7 +1584,7 @@ html, body, gradio-app {
|
|
| 1574 |
overflow: hidden !important;
|
| 1575 |
border: 1px solid var(--hort-border) !important;
|
| 1576 |
border-style: solid !important;
|
| 1577 |
-
border-radius:
|
| 1578 |
background: var(--hort-bg-3) !important;
|
| 1579 |
color: var(--hort-text) !important;
|
| 1580 |
box-shadow: none !important;
|
|
@@ -1619,7 +1629,7 @@ html, body, gradio-app {
|
|
| 1619 |
align-items: center !important;
|
| 1620 |
overflow: hidden !important;
|
| 1621 |
border: 1px solid var(--hort-border) !important;
|
| 1622 |
-
border-radius:
|
| 1623 |
background: var(--hort-bg-3) !important;
|
| 1624 |
color: var(--hort-text) !important;
|
| 1625 |
box-shadow: none !important;
|
|
@@ -1678,7 +1688,7 @@ html, body, gradio-app {
|
|
| 1678 |
border: 1px solid var(--hort-border) !important;
|
| 1679 |
box-shadow: none !important;
|
| 1680 |
background: var(--hort-bg-1) !important;
|
| 1681 |
-
border-radius:
|
| 1682 |
overflow: auto !important;
|
| 1683 |
}
|
| 1684 |
/* Do not clip uploaded CSV matrices. Gradio virtualizes dataframe rows inside
|
|
@@ -1702,16 +1712,14 @@ html, body, gradio-app {
|
|
| 1702 |
|
| 1703 |
.gradio-container button {
|
| 1704 |
font-weight: 600 !important;
|
| 1705 |
-
font-family: '
|
| 1706 |
-
|
| 1707 |
-
letter-spacing: .04em;
|
| 1708 |
-
font-size: 13px !important;
|
| 1709 |
}
|
| 1710 |
.gradio-container button.primary {
|
| 1711 |
color: #0A0C0A !important;
|
| 1712 |
background: var(--hort-cyan) !important;
|
| 1713 |
border: 1px solid var(--hort-cyan) !important;
|
| 1714 |
-
border-radius:
|
| 1715 |
box-shadow: 0 0 0 rgba(61,220,132,0) !important;
|
| 1716 |
transition: background-color .15s ease, transform .1s ease, box-shadow .2s ease !important;
|
| 1717 |
}
|
|
@@ -1724,14 +1732,15 @@ html, body, gradio-app {
|
|
| 1724 |
transform: scale(0.98);
|
| 1725 |
}
|
| 1726 |
|
| 1727 |
-
/* Underline tabs
|
|
|
|
| 1728 |
.gradio-container .tab-nav {
|
| 1729 |
-
background:
|
| 1730 |
border: none !important;
|
| 1731 |
border-bottom: 1px solid var(--hort-border) !important;
|
| 1732 |
border-radius: 0 !important;
|
| 1733 |
padding: 0 !important;
|
| 1734 |
-
gap:
|
| 1735 |
}
|
| 1736 |
.gradio-container .tab-nav button {
|
| 1737 |
border-radius: 0 !important;
|
|
@@ -1740,35 +1749,35 @@ html, body, gradio-app {
|
|
| 1740 |
border: none !important;
|
| 1741 |
border-bottom: 2px solid transparent !important;
|
| 1742 |
margin-bottom: -1px;
|
| 1743 |
-
|
| 1744 |
-
letter-spacing: .03em;
|
| 1745 |
transition: color .15s ease;
|
| 1746 |
}
|
| 1747 |
.gradio-container .tab-nav button:hover {
|
| 1748 |
color: var(--hort-text) !important;
|
| 1749 |
}
|
| 1750 |
.gradio-container .tab-nav button.selected {
|
| 1751 |
-
color: var(--hort-
|
| 1752 |
background: transparent !important;
|
| 1753 |
border-bottom: 2px solid var(--hort-cyan) !important;
|
| 1754 |
-
box-shadow:
|
| 1755 |
-
font-weight:
|
| 1756 |
}
|
| 1757 |
.hort-main-tabs > .tab-nav,
|
| 1758 |
.gradio-container .hort-main-tabs .tab-nav {
|
| 1759 |
-
margin-bottom:
|
| 1760 |
overflow: visible !important;
|
|
|
|
| 1761 |
}
|
| 1762 |
.hort-main-tabs > .tab-nav button,
|
| 1763 |
.gradio-container .hort-main-tabs .tab-nav button {
|
| 1764 |
min-height: 44px !important;
|
| 1765 |
-
padding: 10px
|
| 1766 |
-
font-size:
|
| 1767 |
-
font-weight:
|
| 1768 |
}
|
| 1769 |
|
| 1770 |
.plot-container, .js-plotly-plot, .plotly, .gr-plot {
|
| 1771 |
-
border-radius:
|
| 1772 |
overflow: hidden !important;
|
| 1773 |
}
|
| 1774 |
|
|
@@ -1795,12 +1804,11 @@ footer { display: none !important; }
|
|
| 1795 |
}
|
| 1796 |
|
| 1797 |
@media (max-width: 900px) {
|
| 1798 |
-
.
|
| 1799 |
}
|
| 1800 |
@media (max-width: 560px) {
|
| 1801 |
.panel-card, .chart-card, .results-card { padding: 16px !important; }
|
| 1802 |
-
.
|
| 1803 |
-
.metric-value { font-size: 18px; }
|
| 1804 |
}
|
| 1805 |
"""
|
| 1806 |
|
|
@@ -1850,91 +1858,88 @@ def make_exact_file_input():
|
|
| 1850 |
|
| 1851 |
with gr.Blocks(title="Rail Delay Lab", theme=HORT_THEME, css=CUSTOM_CSS) as demo:
|
| 1852 |
gr.HTML(f"""
|
| 1853 |
-
<
|
| 1854 |
-
<div class="
|
| 1855 |
-
|
| 1856 |
-
|
|
|
|
|
|
|
|
|
|
| 1857 |
</div>
|
| 1858 |
-
<div class="
|
| 1859 |
-
|
| 1860 |
-
<div class="hero-title">Rail Delay Lab</div>
|
| 1861 |
-
<div class="hero-subtitle">
|
| 1862 |
-
An interactive railway simulation for exploring train capacity, delay propagation, conflict resolution, and timetable reliability.
|
| 1863 |
-
</div>
|
| 1864 |
-
<div class="hero-chips">
|
| 1865 |
-
<span class="hero-chip"><span class="br">[</span>Timetable Planning<span class="br">]</span></span>
|
| 1866 |
-
<span class="hero-chip"><span class="br">[</span>Delay Scenarios<span class="br">]</span></span>
|
| 1867 |
-
<span class="hero-chip"><span class="br">[</span>Space-Time Visuals<span class="br">]</span></span>
|
| 1868 |
-
<span class="hero-chip"><span class="br">[</span>Capacity Sweet-Spot<span class="br">]</span></span>
|
| 1869 |
-
</div>
|
| 1870 |
-
</div>
|
| 1871 |
-
</section>
|
| 1872 |
""")
|
| 1873 |
|
| 1874 |
with gr.Tabs(elem_classes=["hort-main-tabs"]):
|
| 1875 |
with gr.TabItem("Delay Propagation and Conflict Resolution"):
|
| 1876 |
gr.Markdown("## Delay Propagation and Conflict Resolution")
|
| 1877 |
-
gr.Markdown("Upload CSV files to populate the tables below, or edit them manually
|
| 1878 |
-
|
| 1879 |
-
|
| 1880 |
-
|
| 1881 |
-
|
| 1882 |
-
|
| 1883 |
-
|
| 1884 |
-
|
| 1885 |
-
|
| 1886 |
-
|
| 1887 |
-
|
| 1888 |
-
|
| 1889 |
-
|
| 1890 |
-
|
| 1891 |
-
|
| 1892 |
-
|
| 1893 |
-
|
| 1894 |
-
|
| 1895 |
-
|
| 1896 |
-
|
| 1897 |
-
|
| 1898 |
-
|
| 1899 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1900 |
)
|
| 1901 |
-
|
| 1902 |
-
|
| 1903 |
-
|
| 1904 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1905 |
|
| 1906 |
-
with gr.Group(elem_classes=["panel-card"]):
|
| 1907 |
-
gr.HTML(section_header("target", "View & Optimization"))
|
| 1908 |
-
with gr.Row(elem_classes=["selector-strip"]):
|
| 1909 |
-
dd_view = gr.Dropdown(
|
| 1910 |
-
choices=["All Scenarios", "Nominal Only", "Conflicted Only", "Resolved Only"],
|
| 1911 |
-
value="All Scenarios",
|
| 1912 |
-
label="Select View",
|
| 1913 |
-
interactive=True,
|
| 1914 |
-
filterable=False,
|
| 1915 |
-
allow_custom_value=False,
|
| 1916 |
-
elem_id="view_listbox",
|
| 1917 |
-
elem_classes=["hort-listbox", "hort-view-select"]
|
| 1918 |
-
)
|
| 1919 |
-
dd_selected_train = gr.Dropdown(
|
| 1920 |
-
choices=["None", "101", "102"],
|
| 1921 |
-
value="None",
|
| 1922 |
-
label="Select Train",
|
| 1923 |
-
interactive=True,
|
| 1924 |
-
filterable=False,
|
| 1925 |
-
allow_custom_value=False,
|
| 1926 |
-
elem_id="train_listbox",
|
| 1927 |
-
elem_classes=["hort-listbox", "hort-train-select"]
|
| 1928 |
-
)
|
| 1929 |
-
txt_max_buffer = gr.Number(value=15.0, label="Max Buffer Cap (min)", step=1.0)
|
| 1930 |
-
|
| 1931 |
-
with gr.Column(scale=2):
|
| 1932 |
with gr.Group(elem_classes=["panel-card"]):
|
| 1933 |
gr.HTML(section_header("train", "Train Requests"))
|
| 1934 |
file_trains = make_exact_file_input()
|
| 1935 |
-
t_trains = gr.Dataframe(value=d_trains, label="Train Requests Data", interactive=True, max_height=
|
| 1936 |
file_trains.upload(fn=load_train_csv, inputs=file_trains, outputs=[t_trains, dd_selected_train])
|
| 1937 |
-
btn = gr.Button("Simulate", variant="primary", size="lg")
|
| 1938 |
|
| 1939 |
with gr.Group(elem_classes=["chart-card"]):
|
| 1940 |
gr.HTML(section_header("chart", "Operational Space-Time Diagram"))
|
|
@@ -1958,38 +1963,49 @@ with gr.Blocks(title="Rail Delay Lab", theme=HORT_THEME, css=CUSTOM_CSS) as demo
|
|
| 1958 |
gr.Markdown("## Economic Capacity Analysis")
|
| 1959 |
gr.Markdown("Evaluate the capacity/reliability trade-off, TAC revenue, performance-regime penalties, and the best operating sweet spot.")
|
| 1960 |
|
| 1961 |
-
with gr.Row():
|
| 1962 |
-
with gr.Column(scale=
|
| 1963 |
-
|
| 1964 |
-
|
| 1965 |
-
|
| 1966 |
-
|
| 1967 |
-
|
| 1968 |
-
|
| 1969 |
-
|
| 1970 |
-
|
| 1971 |
-
|
| 1972 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1973 |
|
| 1974 |
-
with gr.Group(elem_classes=["panel-card"]):
|
| 1975 |
-
gr.HTML(section_header("dice", "Delay Injection Strategy"))
|
| 1976 |
-
delay_mode = gr.Radio(
|
| 1977 |
-
choices=["Predefined (from Delays Tab)", "Randomized"],
|
| 1978 |
-
value="Predefined (from Delays Tab)",
|
| 1979 |
-
label="Delay Mode",
|
| 1980 |
-
elem_classes=["hort-radio"]
|
| 1981 |
-
)
|
| 1982 |
-
with gr.Row():
|
| 1983 |
-
rand_prob = gr.Slider(0, 100, value=25, step=5, label="Probability of Delay per Train (%)")
|
| 1984 |
-
min_delay = gr.Number(value=5, label="Min Random Delay (min)")
|
| 1985 |
-
max_delay = gr.Number(value=15, label="Max Random Delay (min)")
|
| 1986 |
-
|
| 1987 |
-
with gr.Group(elem_classes=["panel-card"]):
|
| 1988 |
-
gr.HTML(section_header("scale", "Primary Delay Responsibility"))
|
| 1989 |
-
gr.Markdown("_Sets, **before** the sweet-spot search runs, what share of every primary delay is attributed to the Infrastructure Manager (IM) vs the Railway Undertaking (RU) - applied the same way whether the delay is Predefined or Randomized above. IM-caused delays are a penalty for IM; RU-caused delays are income for IM. Secondary delays always stay a cost for IM (PR Penalty)._")
|
| 1990 |
-
im_resp_share = gr.Slider(0, 100, value=50, step=5, label="IM Responsibility Share for Primary Delays (%)")
|
| 1991 |
-
|
| 1992 |
-
with gr.Column(scale=2):
|
| 1993 |
with gr.Group(elem_classes=["panel-card"]):
|
| 1994 |
gr.HTML(section_header("train", "Fleet Mix"))
|
| 1995 |
t_fleet = gr.Dataframe(
|
|
@@ -1997,20 +2013,12 @@ with gr.Blocks(title="Rail Delay Lab", theme=HORT_THEME, css=CUSTOM_CSS) as demo
|
|
| 1997 |
value=[["High-Speed", 130, 2, 1], ["Regional", 100, 4, 2], ["Freight", 80, 5, 3]],
|
| 1998 |
interactive=True,
|
| 1999 |
label="Fleet Composition",
|
| 2000 |
-
max_height=
|
| 2001 |
wrap=True,
|
| 2002 |
show_row_numbers=True,
|
| 2003 |
buttons=[]
|
| 2004 |
)
|
| 2005 |
|
| 2006 |
-
with gr.Group(elem_classes=["panel-card"]):
|
| 2007 |
-
gr.HTML(section_header("coin", "Economic & Search Parameters"))
|
| 2008 |
-
with gr.Row():
|
| 2009 |
-
max_trains_in = gr.Number(value=25, label="Max Trains to Evaluate")
|
| 2010 |
-
num_tac = gr.Number(value=115.84, label="TAC Fee (€ per train)")
|
| 2011 |
-
num_pr = gr.Number(value=7.09, label="PR Penalty (€ per min)")
|
| 2012 |
-
btn_econ = gr.Button("Calculate Sweet Spot", variant="primary", size="lg")
|
| 2013 |
-
|
| 2014 |
with gr.Group(elem_classes=["results-card"]):
|
| 2015 |
gr.HTML(section_header("bars", "Economic Capacity Outputs"))
|
| 2016 |
with gr.Row():
|
|
|
|
| 382 |
'<path d="M15 8l2.5 5a2.5 2.5 0 0 0 5 0L20 8"/><path d="M9 20h6"/>',
|
| 383 |
"list": '<path d="M8 6h12M8 12h12M8 18h12"/><path d="M4 6h.01M4 12h.01M4 18h.01"/>',
|
| 384 |
"bars": '<path d="M4 20V10M10 20V4M16 20v-7M4 20h16"/>',
|
| 385 |
+
"alert": '<path d="M10.3 3.86 1.8 18a1 1 0 0 0 .9 1.5h18.6a1 1 0 0 0 .9-1.5L13.7 3.86a1 1 0 0 0-1.72 0Z"/>'
|
| 386 |
+
'<path d="M12 9v4"/><path d="M12 16.3h.01"/>',
|
| 387 |
+
"check-circle": '<circle cx="12" cy="12" r="9"/><path d="M8.5 12.5l2.5 2.5 5-5"/>',
|
| 388 |
}
|
| 389 |
|
| 390 |
def icon_svg(name, size=18):
|
|
|
|
| 396 |
return f'<h{level} class="hort-heading">{icon_svg(icon)}<span>{text}</span></h{level}>'
|
| 397 |
|
| 398 |
def build_metric_html(df_stats, logs_df):
|
| 399 |
+
"""Live KPI row: recomputed after every simulation run instead of
|
| 400 |
showing fixed placeholder copy."""
|
| 401 |
if df_stats is None or df_stats.empty:
|
| 402 |
trains_n, conflicts_n, avg_delay, on_time_pct = "-", "-", "-", "-"
|
| 403 |
flag_conflicts = False
|
| 404 |
else:
|
| 405 |
trains_n = str(len(df_stats))
|
| 406 |
+
avg_delay = f"{df_stats['Total Delay'].mean():.1f}"
|
| 407 |
on_time = int((df_stats['Type'] == 'On Time').sum())
|
| 408 |
+
on_time_pct = str(round(on_time / len(df_stats) * 100))
|
| 409 |
if logs_df is not None and not logs_df.empty:
|
| 410 |
conflicts = sum(1 for v in logs_df.iloc[:, 0] if isinstance(v, str) and v.startswith("Iter"))
|
| 411 |
else:
|
|
|
|
| 413 |
conflicts_n = str(conflicts)
|
| 414 |
flag_conflicts = conflicts > 0
|
| 415 |
|
| 416 |
+
conflict_icon_cls = "kpi-icon is-flagged" if flag_conflicts else "kpi-icon"
|
| 417 |
+
conflict_val_cls = "kpi-value is-flagged" if flag_conflicts else "kpi-value"
|
| 418 |
+
|
| 419 |
+
def card(icon, icon_cls, label_text, value_cls, value_html, caption):
|
| 420 |
+
return f'''
|
| 421 |
+
<div class="kpi-card">
|
| 422 |
+
<div class="kpi-head">
|
| 423 |
+
<div class="{icon_cls}">{icon_svg(icon, 15)}</div>
|
| 424 |
+
<span class="kpi-label">{label_text}</span>
|
| 425 |
+
</div>
|
| 426 |
+
<div>
|
| 427 |
+
<div class="{value_cls}">{value_html}</div>
|
| 428 |
+
<div class="kpi-caption">{caption}</div>
|
| 429 |
+
</div>
|
| 430 |
+
</div>'''
|
| 431 |
+
|
| 432 |
return f'''
|
| 433 |
+
<div class="kpi-grid">
|
| 434 |
+
{card("train", "kpi-icon", "Trains simulated", "kpi-value", trains_n, "from Train Requests")}
|
| 435 |
+
{card("alert", conflict_icon_cls, "Conflicts resolved", conflict_val_cls, conflicts_n, "auto-resolved")}
|
| 436 |
+
{card("clock", "kpi-icon is-neutral", "Avg total delay", "kpi-value", f'{avg_delay}<span class="unit"> min</span>', "across all trains")}
|
| 437 |
+
{card("check-circle", "kpi-icon", "On-time trains", "kpi-value", f'{on_time_pct}<span class="unit">%</span>', "within buffer cap")}
|
| 438 |
</div>
|
| 439 |
'''
|
| 440 |
|
|
|
|
| 1174 |
overflow: visible !important;
|
| 1175 |
position: relative;
|
| 1176 |
}
|
| 1177 |
+
/* Faint ambient glow pooling behind the top bar - depth without the CRT
|
| 1178 |
+
scanline texture, which read more "novelty terminal" than commercial
|
| 1179 |
+
product chrome. Fixed + pointer-events:none so it never repaints on scroll. */
|
| 1180 |
.gradio-container::before {
|
| 1181 |
content: "";
|
| 1182 |
position: fixed;
|
| 1183 |
inset: 0;
|
| 1184 |
z-index: 0;
|
| 1185 |
pointer-events: none;
|
| 1186 |
+
background: radial-gradient(ellipse 900px 420px at 50% -8%, rgba(61,220,132,0.04), transparent 70%);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1187 |
}
|
| 1188 |
|
| 1189 |
.gradio-container h1, .gradio-container h2, .gradio-container h3, .gradio-container h4,
|
|
|
|
| 1193 |
color: var(--hort-text) !important;
|
| 1194 |
font-family: 'JetBrains Mono', 'IBM Plex Mono', 'SF Mono', monospace !important;
|
| 1195 |
}
|
| 1196 |
+
/* The Soft theme renders every field label as a solid primary-hue chip
|
| 1197 |
+
(a Gradio default, not something we set) - strip it back to plain
|
| 1198 |
+
small muted text so labels read as UI copy, not colored badges. */
|
| 1199 |
+
.gradio-container [data-testid="block-info"] {
|
| 1200 |
+
background: transparent !important;
|
| 1201 |
+
color: var(--hort-muted) !important;
|
| 1202 |
+
padding: 0 !important;
|
| 1203 |
+
font-size: 12.5px !important;
|
| 1204 |
+
font-weight: 500 !important;
|
| 1205 |
+
box-shadow: none !important;
|
| 1206 |
+
}
|
| 1207 |
/* Prose (paragraph copy) reads better in a humanist sans than all-mono -
|
| 1208 |
headings, labels, data and buttons stay mono for the technical voice. */
|
| 1209 |
.gradio-container .prose p, .gradio-container .markdown p, .gradio-container .gr-markdown p {
|
|
|
|
| 1214 |
}
|
| 1215 |
|
| 1216 |
/* ---------------------------------------------------------------------
|
| 1217 |
+
App shell: a slim brand strip + the tab nav directly beneath it, both
|
| 1218 |
+
on the same surface, standing in for a real product top bar. Soft
|
| 1219 |
+
14px cards below it instead of last build's sharp terminal edges -
|
| 1220 |
+
the corner-radius scale for THIS build is: bar/sidebar flush (0),
|
| 1221 |
+
inputs/buttons 8px, cards 14px. Held everywhere, no mixing.
|
| 1222 |
--------------------------------------------------------------------- */
|
| 1223 |
+
.brand-strip {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1224 |
display: flex;
|
| 1225 |
align-items: center;
|
| 1226 |
+
gap: 14px;
|
| 1227 |
+
padding: 13px 6px;
|
| 1228 |
+
background: var(--hort-bg-1);
|
| 1229 |
border-bottom: 1px solid var(--hort-border);
|
|
|
|
| 1230 |
}
|
| 1231 |
+
.brand-mark {
|
| 1232 |
+
width: 24px;
|
| 1233 |
+
height: 24px;
|
| 1234 |
+
border-radius: 7px;
|
| 1235 |
background: var(--hort-cyan);
|
| 1236 |
+
display: flex;
|
| 1237 |
+
align-items: center;
|
| 1238 |
+
justify-content: center;
|
| 1239 |
flex: 0 0 auto;
|
| 1240 |
+
box-shadow: 0 0 14px rgba(61,220,132,0.35);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1241 |
}
|
| 1242 |
+
.brand-mark svg { color: #0A0C0A; }
|
| 1243 |
+
.brand-name {
|
| 1244 |
+
font-family: 'JetBrains Mono', monospace;
|
| 1245 |
+
font-weight: 600;
|
| 1246 |
+
font-size: 14px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1247 |
letter-spacing: -0.01em;
|
| 1248 |
+
color: var(--hort-text) !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1249 |
}
|
| 1250 |
+
.brand-spacer { flex: 1 1 auto; }
|
| 1251 |
+
.status-pill {
|
| 1252 |
display: inline-flex;
|
| 1253 |
align-items: center;
|
| 1254 |
+
gap: 6px;
|
| 1255 |
+
background: var(--hort-cyan-bg);
|
| 1256 |
+
border-radius: 999px;
|
| 1257 |
+
padding: 5px 10px 5px 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1258 |
}
|
| 1259 |
+
.status-dot {
|
| 1260 |
+
width: 6px; height: 6px;
|
| 1261 |
+
border-radius: 50%;
|
| 1262 |
+
background: var(--hort-cyan);
|
| 1263 |
+
box-shadow: 0 0 5px rgba(61,220,132,0.8);
|
| 1264 |
+
}
|
| 1265 |
+
.status-pill span.label { font-size: 11px; font-weight: 600; color: var(--hort-cyan); }
|
| 1266 |
+
.avatar-chip {
|
| 1267 |
+
width: 28px; height: 28px;
|
| 1268 |
+
border-radius: 50%;
|
| 1269 |
+
background: var(--hort-bg-3);
|
| 1270 |
+
border: 1px solid var(--hort-border);
|
| 1271 |
+
display: flex; align-items: center; justify-content: center;
|
| 1272 |
+
font-size: 10.5px;
|
| 1273 |
+
color: var(--hort-muted);
|
| 1274 |
}
|
|
|
|
| 1275 |
|
| 1276 |
+
/* KPI row: real stat cards (icon + label, big number, context line)
|
| 1277 |
+
instead of the previous bracket-hairline readout. */
|
| 1278 |
+
.kpi-grid {
|
| 1279 |
display: grid;
|
| 1280 |
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 1281 |
+
gap: 16px;
|
| 1282 |
+
margin: 0 0 22px 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1283 |
}
|
| 1284 |
+
.kpi-card {
|
| 1285 |
+
background: var(--hort-bg-1);
|
| 1286 |
+
border: 1px solid var(--hort-border);
|
| 1287 |
+
border-radius: 14px;
|
| 1288 |
+
padding: 18px 20px;
|
| 1289 |
+
box-shadow: 0 20px 40px -28px rgba(0,0,0,0.6), inset 0 1px 0 rgba(231,236,231,0.03);
|
| 1290 |
+
display: flex;
|
| 1291 |
+
flex-direction: column;
|
| 1292 |
+
gap: 12px;
|
| 1293 |
+
}
|
| 1294 |
+
.kpi-head { display: flex; align-items: center; gap: 10px; }
|
| 1295 |
+
.kpi-icon {
|
| 1296 |
+
width: 30px; height: 30px;
|
| 1297 |
+
border-radius: 8px;
|
| 1298 |
+
display: flex; align-items: center; justify-content: center;
|
| 1299 |
+
flex: 0 0 auto;
|
| 1300 |
+
background: var(--hort-cyan-bg);
|
| 1301 |
+
}
|
| 1302 |
+
.kpi-icon.is-flagged { background: var(--hort-amber-bg); }
|
| 1303 |
+
.kpi-icon.is-neutral { background: rgba(231,236,231,0.06); }
|
| 1304 |
+
.kpi-icon svg { color: var(--hort-cyan); }
|
| 1305 |
+
.kpi-icon.is-flagged svg { color: var(--hort-amber); }
|
| 1306 |
+
.kpi-icon.is-neutral svg { color: var(--hort-muted); }
|
| 1307 |
+
.kpi-label { font-size: 12.5px; color: var(--hort-muted); }
|
| 1308 |
+
.kpi-value {
|
| 1309 |
+
font-family: 'JetBrains Mono', monospace;
|
| 1310 |
+
color: var(--hort-text) !important;
|
| 1311 |
+
font-size: 26px;
|
| 1312 |
+
font-weight: 600;
|
| 1313 |
}
|
| 1314 |
+
.kpi-value.is-flagged { color: var(--hort-amber) !important; }
|
| 1315 |
+
.kpi-value .unit { font-size: 14px; font-weight: 500; color: var(--hort-muted); }
|
| 1316 |
+
.kpi-caption { font-size: 11.5px; color: var(--hort-muted); margin-top: 2px; }
|
| 1317 |
+
@media (max-width: 900px) {
|
| 1318 |
+
.kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
|
|
|
|
|
|
|
|
| 1319 |
}
|
| 1320 |
+
@media (max-width: 480px) {
|
| 1321 |
+
.kpi-grid { grid-template-columns: 1fr; }
|
|
|
|
| 1322 |
}
|
| 1323 |
+
|
| 1324 |
+
/* Sidebar (run configuration) + main content columns. */
|
| 1325 |
+
.app-sidebar {
|
| 1326 |
+
background: var(--hort-bg-1);
|
| 1327 |
+
border-right: 1px solid var(--hort-border);
|
| 1328 |
+
padding: 22px 20px !important;
|
| 1329 |
+
display: flex !important;
|
| 1330 |
+
flex-direction: column !important;
|
| 1331 |
}
|
| 1332 |
+
.app-sidebar .side-label {
|
| 1333 |
+
font-size: 11px;
|
| 1334 |
+
font-weight: 600;
|
| 1335 |
+
color: var(--hort-muted) !important;
|
| 1336 |
+
text-transform: uppercase;
|
| 1337 |
+
letter-spacing: .04em;
|
| 1338 |
+
margin: 4px 0 2px;
|
| 1339 |
}
|
| 1340 |
+
.sidebar-spacer { flex: 1 1 auto; min-height: 12px; }
|
| 1341 |
+
.app-main { padding: 24px 28px !important; }
|
| 1342 |
+
.data-row-card { min-height: 0; }
|
| 1343 |
|
| 1344 |
/* Section-header icon + label, replacing emoji glyphs. Bolder stroke reads
|
| 1345 |
closer to a Phosphor "Bold" weight than a generic thin-line icon set. */
|
|
|
|
| 1367 |
overflow: visible !important;
|
| 1368 |
background: var(--hort-bg-1) !important;
|
| 1369 |
border: 1px solid var(--hort-border) !important;
|
| 1370 |
+
border-radius: 14px !important;
|
| 1371 |
padding: 24px !important;
|
| 1372 |
box-shadow: inset 0 1px 0 rgba(231,236,231,0.04) !important;
|
| 1373 |
backdrop-filter: none !important;
|
|
|
|
| 1410 |
.gradio-container textarea,
|
| 1411 |
.gradio-container select,
|
| 1412 |
.gradio-container button {
|
| 1413 |
+
border-radius: 8px !important;
|
| 1414 |
font-family: 'JetBrains Mono', 'IBM Plex Mono', 'SF Mono', monospace !important;
|
| 1415 |
}
|
| 1416 |
.gradio-container input,
|
|
|
|
| 1432 |
Keep dropdowns native. Do not style Gradio's internal combobox/listbox/menu/button
|
| 1433 |
elements; doing so can make the menu jump, flicker, or open in the wrong place. */
|
| 1434 |
.hort-listbox {
|
| 1435 |
+
border-radius: 8px !important;
|
| 1436 |
}
|
| 1437 |
.hort-listbox label,
|
| 1438 |
.hort-listbox .label-wrap {
|
|
|
|
| 1465 |
.gradio-container fieldset {
|
| 1466 |
background: transparent !important;
|
| 1467 |
border: 1px solid var(--hort-border) !important;
|
| 1468 |
+
border-radius: 8px !important;
|
| 1469 |
}
|
| 1470 |
.hort-radio fieldset {
|
| 1471 |
padding: 6px !important;
|
|
|
|
| 1478 |
gap: 8px;
|
| 1479 |
margin: 0 !important;
|
| 1480 |
padding: 10px 14px !important;
|
| 1481 |
+
border-radius: 6px !important;
|
| 1482 |
background: transparent !important;
|
| 1483 |
border: 1px solid transparent !important;
|
| 1484 |
color: var(--hort-muted) !important;
|
|
|
|
| 1493 |
.hort-radio .selected {
|
| 1494 |
background: var(--hort-cyan-bg) !important;
|
| 1495 |
border: 1px solid rgba(61,220,132,0.35) !important;
|
| 1496 |
+
border-radius: 6px !important;
|
| 1497 |
box-shadow: 0 0 12px rgba(61,220,132,0.08) !important;
|
| 1498 |
color: var(--hort-cyan) !important;
|
| 1499 |
font-weight: 700 !important;
|
|
|
|
| 1584 |
overflow: hidden !important;
|
| 1585 |
border: 1px solid var(--hort-border) !important;
|
| 1586 |
border-style: solid !important;
|
| 1587 |
+
border-radius: 6px !important;
|
| 1588 |
background: var(--hort-bg-3) !important;
|
| 1589 |
color: var(--hort-text) !important;
|
| 1590 |
box-shadow: none !important;
|
|
|
|
| 1629 |
align-items: center !important;
|
| 1630 |
overflow: hidden !important;
|
| 1631 |
border: 1px solid var(--hort-border) !important;
|
| 1632 |
+
border-radius: 6px !important;
|
| 1633 |
background: var(--hort-bg-3) !important;
|
| 1634 |
color: var(--hort-text) !important;
|
| 1635 |
box-shadow: none !important;
|
|
|
|
| 1688 |
border: 1px solid var(--hort-border) !important;
|
| 1689 |
box-shadow: none !important;
|
| 1690 |
background: var(--hort-bg-1) !important;
|
| 1691 |
+
border-radius: 10px !important;
|
| 1692 |
overflow: auto !important;
|
| 1693 |
}
|
| 1694 |
/* Do not clip uploaded CSV matrices. Gradio virtualizes dataframe rows inside
|
|
|
|
| 1712 |
|
| 1713 |
.gradio-container button {
|
| 1714 |
font-weight: 600 !important;
|
| 1715 |
+
font-family: 'Space Grotesk', 'Helvetica Neue', Arial, sans-serif !important;
|
| 1716 |
+
font-size: 14px !important;
|
|
|
|
|
|
|
| 1717 |
}
|
| 1718 |
.gradio-container button.primary {
|
| 1719 |
color: #0A0C0A !important;
|
| 1720 |
background: var(--hort-cyan) !important;
|
| 1721 |
border: 1px solid var(--hort-cyan) !important;
|
| 1722 |
+
border-radius: 8px !important;
|
| 1723 |
box-shadow: 0 0 0 rgba(61,220,132,0) !important;
|
| 1724 |
transition: background-color .15s ease, transform .1s ease, box-shadow .2s ease !important;
|
| 1725 |
}
|
|
|
|
| 1732 |
transform: scale(0.98);
|
| 1733 |
}
|
| 1734 |
|
| 1735 |
+
/* Underline tabs - the second tier of the top bar, directly under the
|
| 1736 |
+
brand strip (same background, hairline bottom border). */
|
| 1737 |
.gradio-container .tab-nav {
|
| 1738 |
+
background: var(--hort-bg-1) !important;
|
| 1739 |
border: none !important;
|
| 1740 |
border-bottom: 1px solid var(--hort-border) !important;
|
| 1741 |
border-radius: 0 !important;
|
| 1742 |
padding: 0 !important;
|
| 1743 |
+
gap: 22px;
|
| 1744 |
}
|
| 1745 |
.gradio-container .tab-nav button {
|
| 1746 |
border-radius: 0 !important;
|
|
|
|
| 1749 |
border: none !important;
|
| 1750 |
border-bottom: 2px solid transparent !important;
|
| 1751 |
margin-bottom: -1px;
|
| 1752 |
+
font-family: 'Space Grotesk', 'Helvetica Neue', Arial, sans-serif !important;
|
|
|
|
| 1753 |
transition: color .15s ease;
|
| 1754 |
}
|
| 1755 |
.gradio-container .tab-nav button:hover {
|
| 1756 |
color: var(--hort-text) !important;
|
| 1757 |
}
|
| 1758 |
.gradio-container .tab-nav button.selected {
|
| 1759 |
+
color: var(--hort-text) !important;
|
| 1760 |
background: transparent !important;
|
| 1761 |
border-bottom: 2px solid var(--hort-cyan) !important;
|
| 1762 |
+
box-shadow: none !important;
|
| 1763 |
+
font-weight: 600 !important;
|
| 1764 |
}
|
| 1765 |
.hort-main-tabs > .tab-nav,
|
| 1766 |
.gradio-container .hort-main-tabs .tab-nav {
|
| 1767 |
+
margin-bottom: 22px !important;
|
| 1768 |
overflow: visible !important;
|
| 1769 |
+
padding: 0 6px !important;
|
| 1770 |
}
|
| 1771 |
.hort-main-tabs > .tab-nav button,
|
| 1772 |
.gradio-container .hort-main-tabs .tab-nav button {
|
| 1773 |
min-height: 44px !important;
|
| 1774 |
+
padding: 10px 2px !important;
|
| 1775 |
+
font-size: 14px !important;
|
| 1776 |
+
font-weight: 500 !important;
|
| 1777 |
}
|
| 1778 |
|
| 1779 |
.plot-container, .js-plotly-plot, .plotly, .gr-plot {
|
| 1780 |
+
border-radius: 10px !important;
|
| 1781 |
overflow: hidden !important;
|
| 1782 |
}
|
| 1783 |
|
|
|
|
| 1804 |
}
|
| 1805 |
|
| 1806 |
@media (max-width: 900px) {
|
| 1807 |
+
.app-sidebar { border-right: none; border-bottom: 1px solid var(--hort-border); }
|
| 1808 |
}
|
| 1809 |
@media (max-width: 560px) {
|
| 1810 |
.panel-card, .chart-card, .results-card { padding: 16px !important; }
|
| 1811 |
+
.kpi-value { font-size: 20px; }
|
|
|
|
| 1812 |
}
|
| 1813 |
"""
|
| 1814 |
|
|
|
|
| 1858 |
|
| 1859 |
with gr.Blocks(title="Rail Delay Lab", theme=HORT_THEME, css=CUSTOM_CSS) as demo:
|
| 1860 |
gr.HTML(f"""
|
| 1861 |
+
<div class="brand-strip">
|
| 1862 |
+
<div class="brand-mark">{icon_svg('train', 13)}</div>
|
| 1863 |
+
<span class="brand-name">Rail Delay Lab</span>
|
| 1864 |
+
<div class="brand-spacer"></div>
|
| 1865 |
+
<div class="status-pill">
|
| 1866 |
+
<span class="status-dot"></span>
|
| 1867 |
+
<span class="label">Live</span>
|
| 1868 |
</div>
|
| 1869 |
+
<div class="avatar-chip">OP</div>
|
| 1870 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1871 |
""")
|
| 1872 |
|
| 1873 |
with gr.Tabs(elem_classes=["hort-main-tabs"]):
|
| 1874 |
with gr.TabItem("Delay Propagation and Conflict Resolution"):
|
| 1875 |
gr.Markdown("## Delay Propagation and Conflict Resolution")
|
| 1876 |
+
gr.Markdown("Upload CSV files to populate the tables below, or edit them manually, then run the resolver.")
|
| 1877 |
+
|
| 1878 |
+
with gr.Row(equal_height=True):
|
| 1879 |
+
with gr.Column(scale=0, min_width=300, elem_classes=["app-sidebar"]):
|
| 1880 |
+
gr.HTML('<p class="side-label">Infrastructure</p>')
|
| 1881 |
+
txt_spacing = gr.Number(value=5.0, label="Min Safety Spacing (km)", step=0.1)
|
| 1882 |
+
|
| 1883 |
+
gr.HTML('<p class="side-label">Delay Injection</p>')
|
| 1884 |
+
main_delay_mode = gr.Radio(
|
| 1885 |
+
choices=["Use Uploaded/Inserted Delay", "Randomized (Monte Carlo)"],
|
| 1886 |
+
value="Use Uploaded/Inserted Delay",
|
| 1887 |
+
label="Simulation Delay Mode",
|
| 1888 |
+
elem_classes=["hort-radio"]
|
| 1889 |
+
)
|
| 1890 |
+
main_rand_prob = gr.Slider(0, 100, value=25, step=5, label="Delay Probability (%)")
|
| 1891 |
+
main_min_delay = gr.Number(value=5, label="Min Delay (min)")
|
| 1892 |
+
main_max_delay = gr.Number(value=15, label="Max Delay (min)")
|
| 1893 |
+
|
| 1894 |
+
gr.HTML('<p class="side-label">View & Optimization</p>')
|
| 1895 |
+
with gr.Column(elem_classes=["selector-strip"]):
|
| 1896 |
+
dd_view = gr.Dropdown(
|
| 1897 |
+
choices=["All Scenarios", "Nominal Only", "Conflicted Only", "Resolved Only"],
|
| 1898 |
+
value="All Scenarios",
|
| 1899 |
+
label="Select View",
|
| 1900 |
+
interactive=True,
|
| 1901 |
+
filterable=False,
|
| 1902 |
+
allow_custom_value=False,
|
| 1903 |
+
elem_id="view_listbox",
|
| 1904 |
+
elem_classes=["hort-listbox", "hort-view-select"]
|
| 1905 |
+
)
|
| 1906 |
+
dd_selected_train = gr.Dropdown(
|
| 1907 |
+
choices=["None", "101", "102"],
|
| 1908 |
+
value="None",
|
| 1909 |
+
label="Select Train",
|
| 1910 |
+
interactive=True,
|
| 1911 |
+
filterable=False,
|
| 1912 |
+
allow_custom_value=False,
|
| 1913 |
+
elem_id="train_listbox",
|
| 1914 |
+
elem_classes=["hort-listbox", "hort-train-select"]
|
| 1915 |
)
|
| 1916 |
+
txt_max_buffer = gr.Number(value=15.0, label="Max Buffer Cap (min)", step=1.0)
|
| 1917 |
+
|
| 1918 |
+
gr.HTML('<div class="sidebar-spacer"></div>')
|
| 1919 |
+
btn = gr.Button("Simulate", variant="primary", size="lg")
|
| 1920 |
+
|
| 1921 |
+
with gr.Column(scale=1, elem_classes=["app-main"]):
|
| 1922 |
+
metric_strip = gr.HTML(build_metric_html(None, None))
|
| 1923 |
+
|
| 1924 |
+
with gr.Row():
|
| 1925 |
+
with gr.Column():
|
| 1926 |
+
with gr.Group(elem_classes=["panel-card", "data-row-card"]):
|
| 1927 |
+
gr.HTML(section_header("building", "Infrastructure Data"))
|
| 1928 |
+
file_infra = make_exact_file_input()
|
| 1929 |
+
t_stations = gr.Dataframe(value=d_stations, label="Stations Data", interactive=True, max_height=300, wrap=True, show_row_numbers=True, buttons=[])
|
| 1930 |
+
file_infra.upload(fn=load_csv, inputs=file_infra, outputs=t_stations)
|
| 1931 |
+
with gr.Column():
|
| 1932 |
+
with gr.Group(elem_classes=["panel-card", "data-row-card"]):
|
| 1933 |
+
gr.HTML(section_header("clock", "Delay Data"))
|
| 1934 |
+
file_delays = make_exact_file_input()
|
| 1935 |
+
t_delays = gr.Dataframe(value=d_delays, label="Delay Data", interactive=True, max_height=300, wrap=True, show_row_numbers=True, buttons=[])
|
| 1936 |
+
file_delays.upload(fn=load_csv, inputs=file_delays, outputs=t_delays)
|
| 1937 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1938 |
with gr.Group(elem_classes=["panel-card"]):
|
| 1939 |
gr.HTML(section_header("train", "Train Requests"))
|
| 1940 |
file_trains = make_exact_file_input()
|
| 1941 |
+
t_trains = gr.Dataframe(value=d_trains, label="Train Requests Data", interactive=True, max_height=320, wrap=True, show_row_numbers=True, buttons=[])
|
| 1942 |
file_trains.upload(fn=load_train_csv, inputs=file_trains, outputs=[t_trains, dd_selected_train])
|
|
|
|
| 1943 |
|
| 1944 |
with gr.Group(elem_classes=["chart-card"]):
|
| 1945 |
gr.HTML(section_header("chart", "Operational Space-Time Diagram"))
|
|
|
|
| 1963 |
gr.Markdown("## Economic Capacity Analysis")
|
| 1964 |
gr.Markdown("Evaluate the capacity/reliability trade-off, TAC revenue, performance-regime penalties, and the best operating sweet spot.")
|
| 1965 |
|
| 1966 |
+
with gr.Row(equal_height=True):
|
| 1967 |
+
with gr.Column(scale=0, min_width=300, elem_classes=["app-sidebar"]):
|
| 1968 |
+
gr.HTML('<p class="side-label">Infrastructure</p>')
|
| 1969 |
+
econ_txt_spacing = gr.Number(value=5.0, label="Min Safety Spacing (km)", step=0.1)
|
| 1970 |
+
|
| 1971 |
+
gr.HTML('<p class="side-label">Delay Injection Strategy</p>')
|
| 1972 |
+
delay_mode = gr.Radio(
|
| 1973 |
+
choices=["Predefined (from Delays Tab)", "Randomized"],
|
| 1974 |
+
value="Predefined (from Delays Tab)",
|
| 1975 |
+
label="Delay Mode",
|
| 1976 |
+
elem_classes=["hort-radio"]
|
| 1977 |
+
)
|
| 1978 |
+
rand_prob = gr.Slider(0, 100, value=25, step=5, label="Probability of Delay per Train (%)")
|
| 1979 |
+
min_delay = gr.Number(value=5, label="Min Random Delay (min)")
|
| 1980 |
+
max_delay = gr.Number(value=15, label="Max Random Delay (min)")
|
| 1981 |
+
|
| 1982 |
+
gr.HTML('<p class="side-label">Primary Delay Responsibility</p>')
|
| 1983 |
+
gr.Markdown("_Share of every primary delay attributed to the Infrastructure Manager (IM) vs the Railway Undertaking (RU), applied before the search runs._")
|
| 1984 |
+
im_resp_share = gr.Slider(0, 100, value=50, step=5, label="IM Responsibility Share (%)")
|
| 1985 |
+
|
| 1986 |
+
gr.HTML('<p class="side-label">Economic & Search Parameters</p>')
|
| 1987 |
+
max_trains_in = gr.Number(value=25, label="Max Trains to Evaluate")
|
| 1988 |
+
num_tac = gr.Number(value=115.84, label="TAC Fee (€ per train)")
|
| 1989 |
+
num_pr = gr.Number(value=7.09, label="PR Penalty (€ per min)")
|
| 1990 |
+
|
| 1991 |
+
gr.HTML('<div class="sidebar-spacer"></div>')
|
| 1992 |
+
btn_econ = gr.Button("Calculate Sweet Spot", variant="primary", size="lg")
|
| 1993 |
+
|
| 1994 |
+
with gr.Column(scale=1, elem_classes=["app-main"]):
|
| 1995 |
+
with gr.Row():
|
| 1996 |
+
with gr.Column():
|
| 1997 |
+
with gr.Group(elem_classes=["panel-card", "data-row-card"]):
|
| 1998 |
+
gr.HTML(section_header("building", "Infrastructure Data"))
|
| 1999 |
+
econ_file_infra = make_exact_file_input()
|
| 2000 |
+
econ_t_stations = gr.Dataframe(value=d_stations, label="Stations Data", interactive=True, max_height=300, wrap=True, show_row_numbers=True, buttons=[])
|
| 2001 |
+
econ_file_infra.upload(fn=load_csv, inputs=econ_file_infra, outputs=econ_t_stations)
|
| 2002 |
+
with gr.Column():
|
| 2003 |
+
with gr.Group(elem_classes=["panel-card", "data-row-card"]):
|
| 2004 |
+
gr.HTML(section_header("clock", "Delay Data"))
|
| 2005 |
+
econ_file_delays = make_exact_file_input()
|
| 2006 |
+
econ_t_delays = gr.Dataframe(value=d_delays, label="Delay Data", interactive=True, max_height=300, wrap=True, show_row_numbers=True, buttons=[])
|
| 2007 |
+
econ_file_delays.upload(fn=load_csv, inputs=econ_file_delays, outputs=econ_t_delays)
|
| 2008 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2009 |
with gr.Group(elem_classes=["panel-card"]):
|
| 2010 |
gr.HTML(section_header("train", "Fleet Mix"))
|
| 2011 |
t_fleet = gr.Dataframe(
|
|
|
|
| 2013 |
value=[["High-Speed", 130, 2, 1], ["Regional", 100, 4, 2], ["Freight", 80, 5, 3]],
|
| 2014 |
interactive=True,
|
| 2015 |
label="Fleet Composition",
|
| 2016 |
+
max_height=320,
|
| 2017 |
wrap=True,
|
| 2018 |
show_row_numbers=True,
|
| 2019 |
buttons=[]
|
| 2020 |
)
|
| 2021 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2022 |
with gr.Group(elem_classes=["results-card"]):
|
| 2023 |
gr.HTML(section_header("bars", "Economic Capacity Outputs"))
|
| 2024 |
with gr.Row():
|