Peymanasmari commited on
Commit
f44bef4
·
verified ·
1 Parent(s): 42d28f2

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +286 -278
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 summary strip: recomputed after every simulation run instead of
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} min"
404
  on_time = int((df_stats['Type'] == 'On Time').sum())
405
- on_time_pct = f"{round(on_time / len(df_stats) * 100)}%"
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
- conflict_class = "metric-value is-flagged" if flag_conflicts else "metric-value"
414
- def label(text):
415
- return f'<div class="metric-label"><span class="br">[</span> {text} <span class="br">]</span></div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  return f'''
417
- <div class="metric-strip">
418
- <div class="metric-item">{label("TRAINS SIMULATED")}<div class="metric-value">{trains_n}</div></div>
419
- <div class="metric-item">{label("CONFLICTS RESOLVED")}<div class="{conflict_class}">{conflicts_n}</div></div>
420
- <div class="metric-item">{label("AVG TOTAL DELAY")}<div class="metric-value">{avg_delay}</div></div>
421
- <div class="metric-item">{label("ON-TIME TRAINS")}<div class="metric-value">{on_time_pct}</div></div>
422
  </div>
423
  '''
424
 
@@ -1158,24 +1174,16 @@ html, body, gradio-app {
1158
  overflow: visible !important;
1159
  position: relative;
1160
  }
1161
- /* Barely-there scanline texture plus a faint ambient phosphor glow pooling
1162
- near the top of the page - thematic depth (CRT console), not decoration.
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
- Hero: a terminal window - titlebar with a live-status dot, prompt-style
1199
- eyebrow, bracket tags. Sharp corners throughout (radius 4px max), the
1200
- one shape rule this build holds to everywhere.
 
 
1201
  --------------------------------------------------------------------- */
1202
- .hero-card {
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: 8px;
1216
- padding: 9px 14px;
 
1217
  border-bottom: 1px solid var(--hort-border);
1218
- background: rgba(255,255,255,0.015);
1219
  }
1220
- .term-dot {
1221
- width: 7px;
1222
- height: 7px;
1223
- border-radius: 50%;
1224
  background: var(--hort-cyan);
 
 
 
1225
  flex: 0 0 auto;
1226
- box-shadow: 0 0 6px rgba(61,220,132,0.8);
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
- .hero-body { padding: 30px 32px 32px; }
1242
- .hero-kicker {
1243
- font-size: 12px;
1244
- letter-spacing: .08em;
1245
- color: var(--hort-cyan) !important;
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
- text-shadow: 0 0 28px rgba(61,220,132,0.18);
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
- .hero-chip {
 
1271
  display: inline-flex;
1272
  align-items: center;
1273
- padding: 6px 10px;
1274
- border-radius: 4px;
1275
- background: transparent;
1276
- border: 1px solid var(--hort-border);
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
- .hero-chip:hover {
1284
- border-color: rgba(61,220,132,0.4);
1285
- color: var(--hort-text) !important;
 
 
 
 
 
 
 
 
 
 
 
 
1286
  }
1287
- .hero-chip .br { color: var(--hort-cyan); }
1288
 
1289
- /* Console readout: flat hairline stat row, bracketed labels, phosphor
1290
- numbers. Grid (not flex-wrap) so the column count snaps cleanly. */
1291
- .metric-strip {
1292
  display: grid;
1293
  grid-template-columns: repeat(4, minmax(0, 1fr));
1294
- border: 1px solid var(--hort-border);
1295
- border-radius: 4px;
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
- .metric-item:last-child { border-right: none; }
1305
- .metric-label {
1306
- color: var(--hort-muted) !important;
1307
- font-size: 11px;
1308
- letter-spacing: .06em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1309
  }
1310
- .metric-label .br { color: var(--hort-muted); opacity: .5; }
1311
- .metric-value {
1312
- color: var(--hort-cyan) !important;
1313
- font-size: 24px;
1314
- line-height: 1.3;
1315
- font-weight: 700;
1316
- margin-top: 6px;
1317
- text-shadow: 0 0 14px rgba(61,220,132,0.20);
1318
  }
1319
- .metric-value.is-flagged {
1320
- color: var(--hort-amber) !important;
1321
- text-shadow: 0 0 14px rgba(255,177,0,0.24);
1322
  }
1323
- @media (max-width: 860px) {
1324
- .metric-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1325
- .metric-item:nth-child(even) { border-right: none; }
1326
- .metric-item:nth-child(-n+2) { border-bottom: 1px solid var(--hort-border); }
 
 
 
 
1327
  }
1328
- @media (max-width: 460px) {
1329
- .metric-strip { grid-template-columns: 1fr; }
1330
- .metric-item { border-right: none !important; border-bottom: 1px solid var(--hort-border); }
1331
- .metric-item:last-child { border-bottom: none; }
 
 
 
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: 4px !important;
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: 4px !important;
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: 4px !important;
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: 4px !important;
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: 3px !important;
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: 3px !important;
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: 3px !important;
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: 3px !important;
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: 4px !important;
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: 'JetBrains Mono', 'IBM Plex Mono', 'SF Mono', monospace !important;
1706
- text-transform: uppercase;
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: 4px !important;
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, terminal-caps labels - not pill buttons. */
 
1728
  .gradio-container .tab-nav {
1729
- background: transparent !important;
1730
  border: none !important;
1731
  border-bottom: 1px solid var(--hort-border) !important;
1732
  border-radius: 0 !important;
1733
  padding: 0 !important;
1734
- gap: 4px;
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
- text-transform: uppercase;
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-cyan) !important;
1752
  background: transparent !important;
1753
  border-bottom: 2px solid var(--hort-cyan) !important;
1754
- box-shadow: 0 1px 8px rgba(61,220,132,0.35) !important;
1755
- font-weight: 700 !important;
1756
  }
1757
  .hort-main-tabs > .tab-nav,
1758
  .gradio-container .hort-main-tabs .tab-nav {
1759
- margin-bottom: 26px !important;
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 16px !important;
1766
- font-size: 13px !important;
1767
- font-weight: 600 !important;
1768
  }
1769
 
1770
  .plot-container, .js-plotly-plot, .plotly, .gr-plot {
1771
- border-radius: 4px !important;
1772
  overflow: hidden !important;
1773
  }
1774
 
@@ -1795,12 +1804,11 @@ footer { display: none !important; }
1795
  }
1796
 
1797
  @media (max-width: 900px) {
1798
- .hero-title { font-size: 30px; }
1799
  }
1800
  @media (max-width: 560px) {
1801
  .panel-card, .chart-card, .results-card { padding: 16px !important; }
1802
- .hero-subtitle { font-size: 13px; }
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
- <section class="hero-card">
1854
- <div class="term-titlebar">
1855
- <span class="term-dot"></span>
1856
- <span class="term-path">~/rail-delay-lab</span>
 
 
 
1857
  </div>
1858
- <div class="hero-body">
1859
- <div class="hero-kicker">&gt; railway_capacity_decision_lab</div>
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. Then select a Train to minimize its delay.")
1878
- metric_strip = gr.HTML(build_metric_html(None, None))
1879
-
1880
- with gr.Row():
1881
- with gr.Column(scale=1):
1882
- with gr.Group(elem_classes=["panel-card"]):
1883
- gr.HTML(section_header("building", "Infrastructure"))
1884
- file_infra = make_exact_file_input()
1885
- t_stations = gr.Dataframe(value=d_stations, label="Stations Data", interactive=True, max_height=560, wrap=True, show_row_numbers=True, buttons=[])
1886
- file_infra.upload(fn=load_csv, inputs=file_infra, outputs=t_stations)
1887
- txt_spacing = gr.Number(value=5.0, label="Min Safety Spacing (km)", step=0.1)
1888
-
1889
- with gr.Group(elem_classes=["panel-card"]):
1890
- gr.HTML(section_header("clock", "Delay Injection"))
1891
- file_delays = make_exact_file_input()
1892
- t_delays = gr.Dataframe(value=d_delays, label="Delay Data", interactive=True, max_height=520, wrap=True, show_row_numbers=True, buttons=[])
1893
- file_delays.upload(fn=load_csv, inputs=file_delays, outputs=t_delays)
1894
-
1895
- main_delay_mode = gr.Radio(
1896
- choices=["Use Uploaded/Inserted Delay", "Randomized (Monte Carlo)"],
1897
- value="Use Uploaded/Inserted Delay",
1898
- label="Simulation Delay Mode",
1899
- elem_classes=["hort-radio"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1900
  )
1901
- with gr.Row():
1902
- main_rand_prob = gr.Slider(0, 100, value=25, step=5, label="Delay Probability (%)")
1903
- main_min_delay = gr.Number(value=5, label="Min Delay (min)")
1904
- main_max_delay = gr.Number(value=15, label="Max Delay (min)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=680, wrap=True, show_row_numbers=True, buttons=[])
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=1):
1963
- with gr.Group(elem_classes=["panel-card"]):
1964
- gr.HTML(section_header("building", "Infrastructure & Delays"))
1965
- econ_file_infra = make_exact_file_input()
1966
- econ_t_stations = gr.Dataframe(value=d_stations, label="Stations Data", interactive=True, max_height=560, wrap=True, show_row_numbers=True, buttons=[])
1967
- econ_file_infra.upload(fn=load_csv, inputs=econ_file_infra, outputs=econ_t_stations)
1968
- econ_txt_spacing = gr.Number(value=5.0, label="Min Safety Spacing (km)", step=0.1)
1969
-
1970
- econ_file_delays = make_exact_file_input()
1971
- econ_t_delays = gr.Dataframe(value=d_delays, label="Delay Data", interactive=True, max_height=520, wrap=True, show_row_numbers=True, buttons=[])
1972
- econ_file_delays.upload(fn=load_csv, inputs=econ_file_delays, outputs=econ_t_delays)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=420,
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 &amp; 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 &amp; 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():