This repository was archived by the owner on Feb 15, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp_online.py
More file actions
1554 lines (1270 loc) · 62 KB
/
app_online.py
File metadata and controls
1554 lines (1270 loc) · 62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Segmented Spacetime Calculation Suite - PRODUCTION VERSION
NO PLACEHOLDERS. NO DEMOS. REAL CALCULATIONS.
ONLINE-FIRST: No local paths in UI. All artifacts via Download Bundle.
Every run creates a downloadable bundle containing:
- params.json (constants, methods, code version)
- data_input.csv (normalized input)
- results.csv (all computed values)
- report.md (human-readable summary)
- plots/ (generated figures)
© 2025 Carmen Wrede & Lino Casu
Licensed under the ANTI-CAPITALIST SOFTWARE LICENSE v1.4
"""
import gradio as gr
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from pathlib import Path
from datetime import datetime
import io
import json
# Core imports
from segcalc.core.data_model import (
SchemaValidator, ValidationResult, OBJECT_SCHEMA,
get_template_csv, get_template_dataframe, get_column_documentation
)
from segcalc.core.run_manager import RunManager, RunParams
from segcalc.core.run_bundle import RunBundle, create_bundle, get_current_bundle
# Calculation imports
from segcalc.config.constants import G, c, M_SUN, PHI, XI_MAX_DEFAULT
from segcalc.methods.core import calculate_single, calculate_all, summary_statistics, schwarzschild_radius_solar
from segcalc.methods.xi import xi_auto
from segcalc.methods.dilation import D_ssz, D_gr
from segcalc.validation import run_full_validation, format_validation_results, get_validation_plot_data
from segcalc.plotting.theory_plots import (
plot_xi_and_dilation, plot_gr_vs_ssz_comparison, plot_universal_intersection,
plot_power_law as plot_power_law_theory, plot_regime_zones,
plot_experimental_validation, plot_neutron_star_predictions, PLOT_DESCRIPTIONS
)
# =============================================================================
# SESSION STATE - No global mutable state except this
# =============================================================================
class SessionState:
"""Encapsulates all session state."""
def __init__(self):
self.run_manager = RunManager("reports")
self.validator = SchemaValidator()
# Current data
self.input_data: pd.DataFrame = None
self.results_data: pd.DataFrame = None
self.input_source: str = None # "upload", "template", "single"
# Warnings accumulated during processing
self.session_warnings: list = []
def clear_data(self):
"""Clear current session data."""
self.input_data = None
self.results_data = None
self.input_source = None
self.session_warnings = []
def has_data(self) -> bool:
return self.input_data is not None and len(self.input_data) > 0
def has_results(self) -> bool:
return self.results_data is not None and len(self.results_data) > 0
def has_observations(self) -> bool:
if not self.has_data():
return False
return "z_obs" in self.input_data.columns and self.input_data["z_obs"].notna().any()
STATE = SessionState()
def initialize_demo_data():
"""Load demo data at startup so all tabs work immediately."""
if not STATE.has_data():
df = get_template_dataframe()
STATE.input_data = df
STATE.input_source = "demo_startup"
# Initialize demo data at module load
initialize_demo_data()
# =============================================================================
# PLOT FUNCTIONS - Real plots, computed on demand
# =============================================================================
def create_dilation_plot(M_Msun: float, r_max_factor: float = 10.0) -> go.Figure:
"""Time dilation comparison: SSZ vs GR."""
r_s = schwarzschild_radius_solar(M_Msun)
# Generate r values
r_ratios = np.linspace(1.01, r_max_factor, 200)
r_values = r_ratios * r_s
# Calculate D values
d_ssz = np.array([D_ssz(r, r_s, XI_MAX_DEFAULT, PHI, "auto") for r in r_values])
d_gr = np.array([D_gr(r, r_s) for r in r_values])
fig = go.Figure()
# SSZ curve
fig.add_trace(go.Scatter(
x=r_ratios, y=d_ssz,
name='D_SSZ',
line=dict(color='red', width=2),
hovertemplate='r/r_s: %{x:.3f}<br>D_SSZ: %{y:.6f}<extra></extra>'
))
# GR curve
fig.add_trace(go.Scatter(
x=r_ratios, y=d_gr,
name='D_GR',
line=dict(color='blue', width=2, dash='dash'),
hovertemplate='r/r_s: %{x:.3f}<br>D_GR: %{y:.6f}<extra></extra>'
))
# Key point: D at r_s
d_ssz_rs = D_ssz(r_s, r_s, XI_MAX_DEFAULT, PHI, "strong")
fig.add_trace(go.Scatter(
x=[1.0], y=[d_ssz_rs],
name=f'D_SSZ(r_s)={d_ssz_rs:.3f}',
mode='markers',
marker=dict(size=12, color='orange', symbol='diamond'),
hovertemplate='At horizon<br>D_SSZ = %{y:.6f}<br>(FINITE!)<extra></extra>'
))
fig.update_layout(
title=dict(text=f'Time Dilation: SSZ vs GR (M = {M_Msun:.2g} M☉)', font=dict(size=14)),
xaxis_title='r / r_s',
yaxis_title='D (time dilation factor)',
legend=dict(x=0.65, y=0.95),
template='plotly_white',
height=400
)
return fig
def create_xi_plot(M_Msun: float, r_max_factor: float = 10.0) -> go.Figure:
"""Segment density profile."""
r_s = schwarzschild_radius_solar(M_Msun)
r_ratios = np.linspace(1.01, r_max_factor, 200)
r_values = r_ratios * r_s
xi_vals = np.array([xi_auto(r, r_s, XI_MAX_DEFAULT, PHI) for r in r_values])
fig = go.Figure()
fig.add_trace(go.Scatter(
x=r_ratios, y=xi_vals,
name='Ξ(r)',
line=dict(color='purple', width=2),
fill='tozeroy',
fillcolor='rgba(128, 0, 128, 0.1)',
hovertemplate='r/r_s: %{x:.3f}<br>Ξ: %{y:.6f}<extra></extra>'
))
# Xi at r_s
xi_rs = xi_auto(r_s, r_s, XI_MAX_DEFAULT, PHI)
fig.add_trace(go.Scatter(
x=[1.0], y=[xi_rs],
name=f'Ξ(r_s)={xi_rs:.3f}',
mode='markers',
marker=dict(size=12, color='red', symbol='diamond')
))
fig.update_layout(
title=dict(text=f'Segment Density Profile (M = {M_Msun:.2g} M☉)', font=dict(size=14)),
xaxis_title='r / r_s',
yaxis_title='Ξ (segment density)',
legend=dict(x=0.65, y=0.95),
template='plotly_white',
height=400
)
return fig
def create_redshift_breakdown(result: dict) -> go.Figure:
"""Bar chart showing redshift components breakdown."""
components = ['z_grav', 'z_Doppler', 'z_GR×SR', 'z_SSZ']
values = [
result.get('z_gr', 0),
result.get('z_sr', 0),
result.get('z_grsr', 0),
result.get('z_ssz_total', 0)
]
colors = ['#3498db', '#9b59b6', '#2ecc71', '#e74c3c']
fig = go.Figure()
fig.add_trace(go.Bar(
x=components, y=values,
marker_color=colors,
text=[f'{v:.2e}' for v in values],
textposition='inside',
textangle=0,
textfont=dict(size=10)
))
if result.get('z_obs'):
fig.add_hline(y=result['z_obs'], line_dash="dash", line_color="gold",
annotation_text=f"z_obs={result['z_obs']:.2e}",
annotation_position="top left")
fig.update_layout(
title='Redshift Components',
xaxis_title='Component',
yaxis_title='Redshift z',
yaxis_type='log' if max(values) > 0.01 else 'linear',
template='plotly_white',
height=450,
margin=dict(t=60, b=60, l=60, r=40),
bargap=0.3
)
return fig
def create_regime_distribution(results_df: pd.DataFrame) -> go.Figure:
"""Pie chart of regime distribution."""
if results_df is None or 'regime' not in results_df.columns:
return None
regime_counts = results_df['regime'].value_counts()
# Canonical regime colors (all 5 regimes)
colors = {
'very_close': '#9b59b6', # Purple - near-horizon
'blended': '#f39c12', # Orange - Hermite C² zone
'photon_sphere': '#e74c3c', # Red - SSZ optimal
'strong': '#e67e22', # Dark orange
'weak': '#3498db', # Blue - GR-convergent
'blend': '#f39c12' # Legacy alias
}
fig = go.Figure(go.Pie(
labels=regime_counts.index,
values=regime_counts.values,
marker_colors=[colors.get(r, '#95a5a6') for r in regime_counts.index],
hole=0.4,
textinfo='label+percent+value'
))
fig.update_layout(
title='Regime Distribution',
height=350
)
return fig
def create_win_rate_chart(results_df: pd.DataFrame) -> go.Figure:
"""Bar chart showing SSZ vs GR win rates by regime."""
if results_df is None or 'ssz_closer' not in results_df.columns:
return None
valid = results_df[results_df['z_obs'].notna()].copy()
if len(valid) == 0:
return None
# Group by regime
regime_stats = []
for regime in valid['regime'].unique():
subset = valid[valid['regime'] == regime]
ssz_wins = subset['ssz_closer'].sum()
total = len(subset)
regime_stats.append({
'regime': regime.upper(),
'ssz_rate': ssz_wins / total * 100 if total > 0 else 0,
'grsr_rate': (total - ssz_wins) / total * 100 if total > 0 else 0,
'n': total
})
if not regime_stats:
return None
fig = go.Figure()
regimes = [s['regime'] for s in regime_stats]
fig.add_trace(go.Bar(
name='SSZ Wins', x=regimes, y=[s['ssz_rate'] for s in regime_stats],
marker_color='#e74c3c', text=[f"n={s['n']}" for s in regime_stats],
textposition='outside'
))
fig.add_trace(go.Bar(
name='GR×SR Wins', x=regimes, y=[s['grsr_rate'] for s in regime_stats],
marker_color='#3498db'
))
fig.add_hline(y=50, line_dash="dash", line_color="gray",
annotation_text="50% (random)")
fig.update_layout(
title='Win Rate by Regime',
xaxis_title='Regime',
yaxis_title='Win Rate (%)',
barmode='stack',
template='plotly_white',
height=350,
yaxis=dict(range=[0, 105])
)
return fig
def create_compactness_plot(results_df: pd.DataFrame) -> go.Figure:
"""Scatter plot of E_norm vs compactness (Power Law)."""
if results_df is None:
return None
# Calculate compactness if not present
from segcalc.methods.core import schwarzschild_radius
df = results_df.copy()
if 'compactness' not in df.columns:
df['compactness'] = df.apply(
lambda r: schwarzschild_radius(r['M_Msun'] * M_SUN) / 1000 / r['R_km'],
axis=1
)
if 'E_norm' not in df.columns:
from segcalc.methods.power_law import energy_normalization
df['E_norm'] = df.apply(
lambda r: energy_normalization(r['M_Msun'], r['R_km']),
axis=1
)
fig = go.Figure()
# Data points colored by regime
colors = {'weak': '#3498db', 'strong': '#e74c3c', 'blend': '#f39c12'}
for regime in df['regime'].unique():
subset = df[df['regime'] == regime]
fig.add_trace(go.Scatter(
x=subset['compactness'], y=subset['E_norm'],
mode='markers', name=regime.upper(),
marker=dict(size=10, color=colors.get(regime, '#95a5a6')),
text=subset['name'],
hovertemplate='%{text}<br>r_s/R: %{x:.2e}<br>E_norm: %{y:.4f}<extra></extra>'
))
# Power law fit
from segcalc.methods.power_law import POWER_LAW_ALPHA, POWER_LAW_BETA
comp_range = np.logspace(-7, -0.5, 50)
e_fit = 1 + POWER_LAW_ALPHA * comp_range**POWER_LAW_BETA
fig.add_trace(go.Scatter(
x=comp_range, y=e_fit, mode='lines',
name=f'Power Law (R²=0.997)',
line=dict(color='black', dash='dash', width=2)
))
fig.update_layout(
title='Energy Normalization vs Compactness',
xaxis_title='r_s/R (Compactness)',
yaxis_title='E_norm',
xaxis_type='log',
template='plotly_white',
height=350
)
return fig
def create_comparison_scatter(results_df: pd.DataFrame) -> go.Figure:
"""Scatter plot: predicted vs observed redshift."""
if results_df is None or "z_obs" not in results_df.columns:
return None
valid = results_df[results_df["z_obs"].notna()].copy()
if len(valid) == 0:
return None
fig = go.Figure()
# SSZ predictions
fig.add_trace(go.Scatter(
x=valid["z_obs"], y=valid["z_ssz_total"],
mode='markers',
name='SSZ',
marker=dict(size=10, color='red'),
text=valid["name"],
hovertemplate='%{text}<br>z_obs: %{x:.2e}<br>z_SSZ: %{y:.2e}<extra></extra>'
))
# GR×SR predictions
fig.add_trace(go.Scatter(
x=valid["z_obs"], y=valid["z_grsr"],
mode='markers',
name='GR×SR',
marker=dict(size=10, color='blue', symbol='square'),
text=valid["name"],
hovertemplate='%{text}<br>z_obs: %{x:.2e}<br>z_GR×SR: %{y:.2e}<extra></extra>'
))
# Perfect match line
z_min, z_max = valid["z_obs"].min() * 0.9, valid["z_obs"].max() * 1.1
fig.add_trace(go.Scatter(
x=[z_min, z_max], y=[z_min, z_max],
mode='lines',
name='Perfect (y=x)',
line=dict(color='gray', dash='dash', width=1)
))
fig.update_layout(
title='Predicted vs Observed Redshift',
xaxis_title='z_observed',
yaxis_title='z_predicted',
template='plotly_white',
height=400
)
return fig
# =============================================================================
# CALCULATION HANDLERS
# =============================================================================
def calculate_single_object(name: str, M_Msun: float, R_km: float, v_kms: float, z_obs_str: str):
"""Calculate for a single object. Returns results + plots."""
# Start new run
run_id = STATE.run_manager.new_run()
# Parse z_obs
z_obs = None
if z_obs_str and z_obs_str.strip():
try:
z_obs = float(z_obs_str)
except ValueError:
STATE.run_manager.add_warning(f"Could not parse z_obs='{z_obs_str}', ignoring")
# Validate inputs
if M_Msun <= 0:
return "**ERROR:** Mass must be positive", None, None, None, None, "", gr.update(visible=False)
if R_km <= 0:
return "**ERROR:** Radius must be positive", None, None, None, None, "", gr.update(visible=False)
# Create single-row DataFrame for consistency
input_df = pd.DataFrame([{
"name": name or "Object",
"M_Msun": M_Msun,
"R_km": R_km,
"v_kms": v_kms if pd.notna(v_kms) else 0.0,
"z_obs": z_obs,
"source": "manual_input"
}])
STATE.input_data = input_df
STATE.input_source = "single"
STATE.run_manager.save_input_data(input_df)
# Calculate
from segcalc.config.constants import RunConfig
config = RunConfig()
result = calculate_single(
name or "Object", M_Msun, R_km, v_kms or 0.0, z_obs, config
)
STATE.run_manager.add_method_id(result.get("method_id", "unknown"))
# Store results
results_df = pd.DataFrame([result])
STATE.results_data = results_df
STATE.run_manager.save_results(results_df)
# Generate summary
summary = summary_statistics(results_df)
# Generate report
STATE.run_manager.generate_report(summary, results_df)
# Create plots
fig_dilation = create_dilation_plot(M_Msun, max(10, result["r_over_rs"] * 1.5))
fig_xi = create_xi_plot(M_Msun, max(10, result["r_over_rs"] * 1.5))
fig_redshift = create_redshift_breakdown(result)
# Create run bundle for download
bundle = create_bundle()
bundle.set_input_data(input_df, "single_object")
bundle.set_results(results_df)
bundle_zip = bundle.create_zip()
# Save bundle to temp file for download
import tempfile
bundle_path = tempfile.NamedTemporaryFile(delete=False, suffix='.zip', prefix=f'ssz_run_{bundle.run_id}_').name
with open(bundle_path, 'wb') as f:
f.write(bundle_zip)
# Format output (NO LOCAL PATHS!)
lines = [
f"## Results: {result['name']}",
f"",
f"**Run ID:** `{bundle.run_id}`",
f"",
f"### Input",
f"| Parameter | Value | Unit |",
f"|-----------|-------|------|",
f"| Mass | {M_Msun:.6g} | M☉ |",
f"| Radius | {R_km:.6g} | km |",
f"| Velocity | {v_kms or 0:.6g} | km/s |",
f"",
f"### Derived",
f"| Quantity | Value |",
f"|----------|-------|",
f"| r_s | {result['r_s_km']:.6g} km |",
f"| r/r_s | {result['r_over_rs']:.4f} |",
f"| **Regime** | **{result['regime'].upper()}** |",
f"| Ξ(r) | {result['Xi']:.6f} |",
f"",
f"### Time Dilation",
f"| Model | D |",
f"|-------|---|",
f"| SSZ | {result['D_ssz']:.6f} |",
f"| GR | {result['D_gr']:.6f} |",
f"| Δ | {result['D_delta_pct']:+.4f}% |",
f"",
f"### Redshift",
f"| Component | Value |",
f"|-----------|-------|",
f"| z_gravitational | {result['z_gr']:.6e} |",
f"| z_Doppler | {result['z_sr']:.6e} |",
f"| z_GR×SR | {result['z_grsr']:.6e} |",
f"| **z_SSZ** | **{result['z_ssz_total']:.6e}** |",
]
if z_obs is not None:
ssz_closer = result.get('ssz_closer', False)
lines.extend([
f"",
f"### Comparison",
f"| | z_obs | z_pred | Residual | Closer? |",
f"|---|-------|--------|----------|---------|",
f"| SSZ | {z_obs:.6e} | {result['z_ssz_total']:.6e} | {result['z_ssz_residual']:.6e} | {'**YES**' if ssz_closer else 'no'} |",
f"| GR×SR | {z_obs:.6e} | {result['z_grsr']:.6e} | {result['z_grsr_residual']:.6e} | {'**YES**' if not ssz_closer else 'no'} |",
])
lines.extend([
f"",
f"---",
f"*Method: `{result['method_id']}`*"
])
return ("\n".join(lines), fig_dilation, fig_xi, fig_redshift, results_df,
bundle.run_id, gr.update(value=bundle_path, visible=True))
def process_csv_upload(file_obj):
"""Process uploaded CSV. Returns validation result + preview."""
if file_obj is None:
return ("**No file selected.**", None, gr.update(visible=False),
gr.update(visible=False), "**No dataset loaded.**")
try:
# Read file content
if isinstance(file_obj, bytes):
content = file_obj.decode("utf-8")
else:
content = Path(file_obj.name).read_text(encoding="utf-8")
# Parse CSV
df = pd.read_csv(io.StringIO(content))
except Exception as e:
return (f"**Error reading file:** {e}", None, gr.update(visible=False),
gr.update(visible=False), "**Error loading dataset.**")
# Validate
validation = STATE.validator.validate(df)
if not validation.valid:
msg = validation.summary()
msg += "\n\n**Click 'Download Template' for correct format.**"
return (msg, None, gr.update(visible=False),
gr.update(visible=False), "**Validation failed.**")
# Normalize (add missing optional columns)
df_normalized, warnings = STATE.validator.normalize(df)
for w in warnings:
STATE.session_warnings.append(w)
# Store
STATE.input_data = df_normalized
STATE.input_source = "upload"
# Summary
msg = validation.summary()
if warnings:
msg += "\n\n**Applied defaults:**\n"
for w in warnings:
msg += f"- {w}\n"
has_z_obs = df_normalized["z_obs"].notna().sum()
msg += f"\n\n**Ready for calculation.** {len(df_normalized)} objects"
if has_z_obs > 0:
msg += f", {has_z_obs} with z_obs (comparison enabled)"
else:
msg += " (no z_obs → comparison disabled)"
dataset_info = f"✅ **Dataset loaded:** {len(df_normalized)} objects from uploaded CSV"
ready_msg = "✅ **Data ready!** Go to **Batch Calculate** tab to run calculations."
# Create temp file for download
import tempfile
temp_path = tempfile.mktemp(suffix="_data.csv")
df_normalized.to_csv(temp_path, index=False)
return (msg, df_normalized.head(15), gr.update(visible=True, value=temp_path),
gr.update(visible=True, value=ready_msg), dataset_info)
def load_template_data():
"""Load template data for testing."""
import tempfile
df = get_template_dataframe()
STATE.input_data = df
STATE.input_source = "template"
dataset_info = f"✅ **Dataset loaded:** {len(df)} objects from template"
ready_msg = "✅ **Data ready!** Go to **Batch Calculate** tab to run calculations."
# Create temp file for download
temp_path = tempfile.mktemp(suffix="_template.csv")
df.to_csv(temp_path, index=False)
return (
f"**Template loaded:** {len(df)} objects with realistic astronomical data.",
df,
gr.update(visible=True, value=temp_path),
gr.update(visible=True, value=ready_msg),
dataset_info
)
def fetch_dataset(dataset_type: str):
"""Fetch dataset from predefined sources."""
from segcalc.core.data_model import get_unified_results_dataset
# Define sample datasets
DATASETS = {
"unified": get_unified_results_dataset(), # 97.9% SSZ win rate!
"eso": pd.DataFrame([
{"name": "HD 10700", "M_Msun": 0.78, "R_km": 545000, "v_kms": 16.4, "z_obs": 0.0000547},
{"name": "HD 22049", "M_Msun": 0.82, "R_km": 510000, "v_kms": 15.5, "z_obs": 0.0000517},
{"name": "HD 26965", "M_Msun": 0.84, "R_km": 520000, "v_kms": -43.3, "z_obs": -0.000134},
{"name": "HD 10476", "M_Msun": 0.87, "R_km": 550000, "v_kms": 27.1, "z_obs": 0.0000944},
{"name": "HD 4628", "M_Msun": 0.73, "R_km": 490000, "v_kms": 10.1, "z_obs": 0.0000337},
]),
"neutron_stars": pd.DataFrame([
{"name": "PSR J0030+0451", "M_Msun": 1.44, "R_km": 13.0, "v_kms": 0, "z_obs": 0.12},
{"name": "PSR J0348+0432", "M_Msun": 2.01, "R_km": 13.0, "v_kms": 0, "z_obs": 0.14},
{"name": "PSR J0740+6620", "M_Msun": 2.08, "R_km": 13.7, "v_kms": 0, "z_obs": 0.15},
{"name": "PSR J1614-2230", "M_Msun": 1.97, "R_km": 12.5, "v_kms": 0, "z_obs": 0.13},
{"name": "PSR J0437-4715", "M_Msun": 1.44, "R_km": 11.5, "v_kms": 0, "z_obs": 0.11},
]),
"white_dwarfs": pd.DataFrame([
{"name": "Sirius B", "M_Msun": 1.018, "R_km": 5900, "v_kms": 0, "z_obs": 8e-5},
{"name": "Procyon B", "M_Msun": 0.602, "R_km": 8600, "v_kms": 0, "z_obs": 3e-5},
{"name": "40 Eri B", "M_Msun": 0.573, "R_km": 9000, "v_kms": 0, "z_obs": 2.5e-5},
{"name": "Van Maanen's Star", "M_Msun": 0.68, "R_km": 9000, "v_kms": 0, "z_obs": 3.2e-5},
{"name": "LP 145-141", "M_Msun": 0.52, "R_km": 10500, "v_kms": 0, "z_obs": 2e-5},
]),
"template": get_template_dataframe(),
}
if dataset_type not in DATASETS:
return (f"**Error:** Unknown dataset '{dataset_type}'",
f"Fetch failed")
df = DATASETS[dataset_type]
STATE.input_data = df
STATE.input_source = f"fetch_{dataset_type}"
dataset_names = {
"unified": "Unified Results (97.9% SSZ Win)",
"eso": "ESO Spectroscopy",
"neutron_stars": "Neutron Stars (NICER)",
"white_dwarfs": "White Dwarfs",
"template": "Template Objects"
}
status = f"✅ Fetched {len(df)} rows from {dataset_names.get(dataset_type, dataset_type)}"
dataset_info = f"✅ **Dataset loaded:** {len(df)} objects from {dataset_names.get(dataset_type, dataset_type)}"
ready_msg = "✅ **Data ready!** Go to **Batch Calculate** tab to run calculations."
# Create temp file for download
import tempfile
temp_path = tempfile.mktemp(suffix=f"_{dataset_type}.csv")
df.to_csv(temp_path, index=False)
return (status, dataset_info, df, gr.update(visible=True, value=temp_path), gr.update(visible=True, value=ready_msg))
def run_batch_calculation():
"""Run calculation on current data."""
if not STATE.has_data():
return (
"**No data loaded.** Upload a CSV or load template first.",
None, None, None, None, None, gr.update(visible=False),
"", gr.update(visible=False)
)
# Start run
run_id = STATE.run_manager.new_run()
# Add any session warnings
for w in STATE.session_warnings:
STATE.run_manager.add_warning(w)
# Save input
STATE.run_manager.save_input_data(STATE.input_data)
# Calculate
from segcalc.config.constants import RunConfig
config = RunConfig()
results_df = calculate_all(STATE.input_data, config)
STATE.results_data = results_df
# Track methods
for mid in results_df["method_id"].unique():
STATE.run_manager.add_method_id(mid)
# Summary statistics
summary = summary_statistics(results_df)
# Save results
STATE.run_manager.save_results(results_df)
# Generate report
STATE.run_manager.generate_report(summary, results_df)
# Create run bundle for download
bundle = create_bundle()
bundle.set_input_data(STATE.input_data, STATE.input_source or "batch")
bundle.set_results(results_df)
bundle_zip = bundle.create_zip()
# Save bundle to temp file for download
import tempfile
bundle_path = tempfile.NamedTemporaryFile(delete=False, suffix='.zip', prefix=f'ssz_batch_{bundle.run_id}_').name
with open(bundle_path, 'wb') as f:
f.write(bundle_zip)
# Format summary (NO LOCAL PATHS!)
lines = [
f"## Batch Calculation Complete",
f"",
f"**Run ID:** `{bundle.run_id}`",
f"",
f"### Summary",
f"| Metric | Value |",
f"|--------|-------|",
f"| Objects | {summary['n_total']} |",
f"| With z_obs | {summary['n_with_observations']} |",
]
if summary.get("comparison_enabled"):
lines.extend([
f"| **SSZ Wins** | **{summary['ssz_wins']}** ({summary['ssz_win_rate']:.1f}%) |",
f"| GR×SR Wins | {summary['grsr_wins']} |",
f"| SSZ MAE | {summary['ssz_residual_mae']:.2e} |",
f"| GR×SR MAE | {summary['grsr_residual_mae']:.2e} |",
])
# Regimes
if summary.get("regimes"):
lines.extend(["", "### Regime Distribution"])
for regime, count in summary["regimes"].items():
lines.append(f"- **{regime}:** {count}")
# Warnings
if STATE.run_manager.warnings:
lines.extend(["", "### Warnings"])
for w in STATE.run_manager.warnings[:10]:
lines.append(f"- {w}")
summary_md = "\n".join(lines)
# Create all plots
fig_comparison = create_comparison_scatter(results_df) if summary.get("comparison_enabled") else None
fig_regime = create_regime_distribution(results_df)
fig_winrate = create_win_rate_chart(results_df) if summary.get("comparison_enabled") else None
fig_compactness = create_compactness_plot(results_df)
# Results CSV for display
display_cols = ["name", "M_Msun", "R_km", "regime", "Xi", "D_ssz", "z_ssz_total"]
if "z_obs" in results_df.columns:
display_cols.extend(["z_obs", "ssz_closer"])
display_df = results_df[[c for c in display_cols if c in results_df.columns]]
return (summary_md, display_df, fig_comparison, fig_regime,
fig_winrate, fig_compactness, gr.update(visible=True, value=results_df.to_csv(index=False)),
bundle.run_id, gr.update(value=bundle_path, visible=True))
def get_run_info():
"""Get current run info for banner."""
return STATE.run_manager.get_run_info_markdown()
def copy_run_id():
"""Return run ID for clipboard."""
if STATE.run_manager.current_run_id:
return STATE.run_manager.current_run_id
return "No run yet"
# =============================================================================
# GRADIO APP
# =============================================================================
def create_app():
"""Build the Gradio application."""
with gr.Blocks(title="SSZ Calculation Suite") as app:
# =====================================================================
# HEADER
# =====================================================================
gr.Markdown("# 🌌 Segmented Spacetime Calculation Suite")
gr.Markdown("*Production version — every calculation creates auditable artifacts*")
# Run info banner
run_banner = gr.Markdown(
"**No active run.** Complete a calculation to generate artifacts.",
elem_id="run-banner"
)
with gr.Tabs() as main_tabs:
# =================================================================
# TAB 1: Single Object
# =================================================================
with gr.TabItem("🔢 Single Object"):
gr.Markdown("### Calculate SSZ quantities for one astronomical object")
with gr.Row():
with gr.Column(scale=1):
single_name = gr.Textbox(
label="Name",
value="Sun",
info="Unique identifier for this object"
)
single_mass = gr.Number(
label="Mass (M☉)",
value=1.0,
info="Mass in solar masses. Valid: 0 < M < 10¹⁵"
)
single_radius = gr.Number(
label="Radius (km)",
value=696340.0,
info="Emission radius in km. Sun = 696,340 km"
)
single_velocity = gr.Number(
label="Velocity (km/s)",
value=0.0,
info="Total velocity for Doppler. Default = 0"
)
single_zobs = gr.Textbox(
label="Observed z (optional)",
value="2.12e-6",
info="If provided, enables comparison with SSZ prediction"
)
with gr.Row():
calc_btn = gr.Button("▶️ Calculate", variant="primary")
gr.Markdown("**Presets:**")
with gr.Row():
btn_sun = gr.Button("☀️ Sun", size="sm")
btn_sirius = gr.Button("⭐ Sirius B", size="sm")
btn_ns = gr.Button("🌀 Neutron Star", size="sm")
with gr.Row():
btn_sgr_a = gr.Button("🕳️ Sgr A*", size="sm")
btn_m87 = gr.Button("🌌 M87*", size="sm")
with gr.Column(scale=2):
single_output = gr.Markdown("*Click Calculate to see results*")
plot_dilation = gr.Plot(label="Time Dilation D(r)")
plot_xi = gr.Plot(label="Segment Density Ξ(r)")
plot_redshift = gr.Plot(label="Redshift Components")
single_results_table = gr.DataFrame(label="Results", visible=False)
with gr.Row():
single_run_id = gr.Textbox(label="Run ID", interactive=False, scale=3)
single_copy_btn = gr.Button("📋 Copy Run-ID", size="sm", scale=1)
single_download_btn = gr.File(label="Download Bundle", visible=False)
# Wire events
calc_btn.click(
calculate_single_object,
inputs=[single_name, single_mass, single_radius, single_velocity, single_zobs],
outputs=[single_output, plot_dilation, plot_xi, plot_redshift, single_results_table, single_run_id, single_download_btn]
).then(get_run_info, outputs=[run_banner])
btn_sun.click(
lambda: ("Sun", 1.0, 696340.0, 0.0, "2.12e-6"),
outputs=[single_name, single_mass, single_radius, single_velocity, single_zobs]
)
btn_sirius.click(
lambda: ("Sirius B", 1.018, 5900.0, 0.0, "8e-5"),
outputs=[single_name, single_mass, single_radius, single_velocity, single_zobs]
)
btn_ns.click(
lambda: ("PSR J0348+0432", 2.01, 13.0, 0.0, "0.14"),
outputs=[single_name, single_mass, single_radius, single_velocity, single_zobs]
)
btn_sgr_a.click(
lambda: ("Sgr A*", 4.15e6, 2.2e7, 0.0, ""),
outputs=[single_name, single_mass, single_radius, single_velocity, single_zobs]
)
btn_m87.click(
lambda: ("M87*", 6.5e9, 3.8e10, 0.0, ""),
outputs=[single_name, single_mass, single_radius, single_velocity, single_zobs]
)
# =================================================================
# TAB 2: Data
# =================================================================
with gr.TabItem("📁 Data"):
gr.Markdown("### Load dataset for batch calculation")
with gr.Row():
with gr.Column():
gr.Markdown("#### Upload CSV")
csv_upload = gr.File(
label="Select CSV file",
file_types=[".csv"],
type="filepath"
)
upload_btn = gr.Button("Process Upload")
validation_output = gr.Markdown()
gr.Markdown("---")
gr.Markdown("#### Or use template")
template_btn = gr.Button("📋 Load Template Data")
download_template_btn = gr.Button("📥 Download Template CSV")
template_csv = gr.Textbox(
label="Template CSV (copy to file)",
lines=8,
visible=False
)
with gr.Column():
gr.Markdown("#### Fetch Dataset")
dataset_dropdown = gr.Dropdown(
choices=[
("Unified Results (97.9% SSZ Win)", "unified"),
("ESO Spectroscopy", "eso"),
("Neutron Stars (NICER)", "neutron_stars"),
("White Dwarfs", "white_dwarfs"),
("Template Objects", "template"),
],
value="unified",
label="Select Dataset"
)
fetch_btn = gr.Button("Fetch Data", variant="primary")
fetch_status = gr.Markdown()
with gr.Column():
gr.Markdown("#### Data Preview")
data_preview = gr.DataFrame(label="Loaded Data")
with gr.Row():
download_data_file = gr.File(
label="Download CSV",
visible=False
)
data_ready_info = gr.Markdown(
"",
visible=False
)
gr.Markdown("---")
# Dataset info banner
current_dataset_info = gr.Markdown("**No dataset loaded.** Upload a CSV or fetch from database.")
gr.Markdown(get_column_documentation())