-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpalomas_orrery.py
3259 lines (2853 loc) · 158 KB
/
palomas_orrery.py
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
#Paloma's Orrery - Solar System Visualization Tool
# Import necessary libraries
import tkinter as tk
from tkinter import ttk
from astroquery.jplhorizons import Horizons
import numpy as np
from datetime import datetime, timedelta
import calendar
import plotly.graph_objs as go
import webbrowser
import os
import warnings
from astropy.utils.exceptions import ErfaWarning
from astropy.time import Time
import traceback
from tkinter import scrolledtext
import threading
import time # Used here for simulation purposes
import subprocess
import sys
import math
from constants import (
planetary_params,
parent_planets,
color_map,
note_text,
INFO,
hover_text_sun_and_corona,
)
from visualization_utils import format_hover_text, add_hover_toggle_buttons
from save_utils import save_plot
# At the very top of the file, after imports:
from shutdown_handler import PlotlyShutdownHandler, create_monitored_thread, show_figure_safely
# Create a global shutdown handler instance
shutdown_handler = PlotlyShutdownHandler()
# Initialize the main window
root = tk.Tk()
root.title("Paloma's Orrery - Updated January 25, 2025")
# Define 'today' once after initializing the main window
today = datetime.today()
# root.configure(bg="lightblue") # Set the background color of the root window
# Define a standard font and button width
BUTTON_FONT = ("Arial", 10, "normal") # You can adjust the font as needed
BUTTON_WIDTH = 17 # Number of characters wide
class ScrollableFrame(tk.Frame):
"""
A scrollable frame that can contain multiple widgets with a vertical scrollbar.
"""
def __init__(self, container, *args, **kwargs):
super().__init__(container, *args, **kwargs)
# Canvas and Scrollbar
self.canvas = tk.Canvas(self, bg='SystemButtonFace')
self.scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.scrollbar.set)
# Layout
self.canvas.pack(side="left", fill="both", expand=True)
self.scrollbar.pack(side="right", fill="y")
# Scrollable Frame
self.scrollable_frame = tk.Frame(self.canvas, bg='SystemButtonFace')
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
# Bind mousewheel to the canvas
self.canvas.bind("<Enter>", self._on_enter)
self.canvas.bind("<Leave>", self._on_leave)
# Update scroll region when the canvas size changes
self.canvas.bind(
"<Configure>",
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
)
def _on_mousewheel(self, event):
if event.delta:
self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
elif event.num == 4:
self.canvas.yview_scroll(-1, "units")
elif event.num == 5:
self.canvas.yview_scroll(1, "units")
def _on_enter(self, event):
# Bind the mousewheel events
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
self.canvas.bind_all("<Button-4>", self._on_mousewheel) # Linux
self.canvas.bind_all("<Button-5>", self._on_mousewheel) # Linux
def _on_leave(self, event):
# Unbind mousewheel events
self.canvas.unbind_all("<MouseWheel>")
self.canvas.unbind_all("<Button-4>")
self.canvas.unbind_all("<Button-5>")
def _on_enter(self, event):
# Bind the mouse wheel events when the cursor enters a widget
event.widget.bind_all("<MouseWheel>", self._on_mousewheel)
event.widget.bind_all("<Button-4>", self._on_mousewheel)
event.widget.bind_all("<Button-5>", self._on_mousewheel)
def _on_leave(self, event):
# Unbind the mouse wheel events when the cursor leaves a widget
event.widget.unbind_all("<MouseWheel>")
event.widget.unbind_all("<Button-4>")
event.widget.unbind_all("<Button-5>")
# Define controls_frame
controls_frame = tk.Frame(root)
controls_frame.grid(row=0, column=1, padx=(5, 10), pady=(10, 10), sticky='n')
# Suppress ErfaWarning messages
warnings.simplefilter('ignore', ErfaWarning)
DEFAULT_MARKER_SIZE = 6
HORIZONS_MAX_DATE = datetime(2199, 12, 29, 0, 0, 0)
CENTER_MARKER_SIZE = 10 # For central objects like the Sun
# Constants
LIGHT_MINUTES_PER_AU = 8.3167 # Approximate light-minutes per Astronomical Unit
CORE_AU = 0.00093 # Core in AU, or approximately 0.2 Solar radii
RADIATIVE_ZONE_AU = 0.00325 # Radiative zone in AU, or approximately 0.7 Solar radii
SOLAR_RADIUS_AU = 0.00465047 # Sun's radius in AU
INNER_LIMIT_OORT_CLOUD_AU = 2000 # Inner Oort cloud inner boundary in AU.
INNER_OORT_CLOUD_AU = 20000 # Inner Oort cloud outer boundary in AU.
OUTER_OORT_CLOUD_AU = 100000 # Oort cloud outer boundary in AU.
GRAVITATIONAL_INFLUENCE_AU = 126000 # Sun's gravitational influence in AU.
CHROMOSPHERE_RADII = 1.5 # The Chromosphere extends from about 1 to 1.5 solar radii or about 0.00465 - 0.0070 AU
INNER_CORONA_RADII = 3 # Inner corona extends to 2 to 3 solar radii or about 0.01 AU
OUTER_CORONA_RADII = 50 # Outer corona extends up to 50 solar radii or about 0.2 AU, more typically 10 to 20 solar radii
TERMINATION_SHOCK_AU = 94 # Termination shock where the solar wind slows to subsonic speeds.
HELIOPAUSE_RADII = 26449 # Outer boundary of the solar wind and solar system, about 123 AU.
PARKER_CLOSEST_RADII = 8.2 # Parker's closest approach was 3.8 million miles on 12-24-24 at 6:53 AM EST (0.41 AU, 8.2 solar radii)
def create_sun_visualization(fig, animate=False, frames=None):
"""
Creates a visualization of the Sun's layers including photosphere, inner corona, and outer corona.
Parameters:
fig (plotly.graph_objects.Figure): The figure to add the Sun visualization to
animate (bool): Whether this is for an animated plot
frames (list, optional): List of frames for animation
Returns:
plotly.graph_objects.Figure: The updated figure
Constants used:
SOLAR_RADIUS_AU: Sun's radius in AU (0.00465047)
INNER_CORONA_RADII: Inner corona extends to 2-3 solar radii (~0.014 AU)
OUTER_CORONA_RADII: Outer corona extends to ~50 solar radii (~0.2 AU)
"""
# Create base traces for static visualization
def create_layer_traces():
traces = []
# 0.2. Sun's Gravitational Influence
x, y, z = create_corona_sphere(GRAVITATIONAL_INFLUENCE_AU)
# Define the text string once
gravitational_influence_info = (
"The Solar System\'s extent is actually defined in multiple ways. The Heliopause (120-123 AU):<br>"
"Where the solar wind meets interstellar space. Gravitational influence: Extends much further, including,<br>"
"Sedna\'s orbit (936 AU), The Hills Cloud/Inner Oort Cloud (2,000-20,000 AU), The Outer Oort Cloud (20,000-100,000 AU).<br>"
"The Sun's gravitational influence extends to about 2 light-years (~126,000 AU). While the Heliopause marks<br>"
"where the Sun\'s particle influence ends, its gravitational influence extends much further. Sedna and other<br>"
"distant objects remain gravitationally bound to the Sun despite being well beyond the Heliopause. This is<br>"
"why astronomers generally consider the Oort Cloud (and objects like Sedna) to be part of our Solar System,<br>"
"even though we've never directly observed the Oort Cloud. The distinction comes down to different types of influence:<br>"
"Particle/plasma influence (solar wind) → ends at Heliopause; gravitational influence → extends much further,<br>"
"including Sedna and the theoretical Oort Cloud. So the Solar System is generally considered to extend at least<br>"
"as far as these gravitationally bound objects, even beyond the Heliopause. Sedna is one of our first glimpses<br>"
"into this very distant region that may connect to the Oort Cloud population."
)
# Create a text list matching the number of points
text_array_gravitational_influence = [gravitational_influence_info for _ in range(len(x))]
customdata_array_gravitational_influence = ["Sun's Gravitational Influence" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=1.0,
# color='green',
color= 'rgb(102, 187, 106)',
opacity=0.2
),
name='Sun\'s Gravitational Influence',
text=text_array_gravitational_influence,
customdata=customdata_array_gravitational_influence, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='green',
size=0.5,
symbol='square-open',
opacity=0.2
),
name='Sun\'s Gravitational Influence',
text=['The Sun\'s gravitational influence to ~126,000 AU or 2 ly.'],
hovertemplate='%{text}<extra></extra>',
showlegend=False
)
)
# 0.3. Outer Oort Cloud
x, y, z = create_corona_sphere(OUTER_OORT_CLOUD_AU)
# Define the text string once
outer_oort_info = (
"The Oort Cloud is a theoretical, vast, spherical shell of icy objects that surrounds the<br>"
"Solar System at distances ranging from approximately 2,000 AU to 100,000 AU from the Sun.<br>"
"Predominantly composed of cometary nuclei—small, icy bodies made of water ice, ammonia, and methane.<br>"
"Believed to be the source of long-period comets that enter the inner Solar System with orbital<br>"
"periods exceeding 200 years.<br><br>"
"Oort Cloud's Outer Edge: At 100,000 AU, it's about 1.58 light-years from the Sun, placing it just<br>"
"beyond the nearest star systems and marking the boundary between the Solar System and interstellar space.<br>"
"The primary source of long-period comets. Objects here are more loosely bound and more susceptible to<br>"
"external gravitational perturbations."
)
# Create a text list matching the number of points
text_array_outer_oort = [outer_oort_info for _ in range(len(x))]
customdata_array_outer_oort = ["Outer Oort Cloud" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=1.0,
color='white', # Yellow approximates the visible color
opacity=0.2
),
name='Outer Oort Cloud',
text=text_array_outer_oort,
customdata=customdata_array_outer_oort, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='white',
size=1.0,
symbol='circle-open',
opacity=0.2
),
name='Outer Oort Cloud',
text=['Outer Oort Cloud from estimated 20,000 to 100,000 AU.'],
hovertemplate='%{text}<extra></extra>',
showlegend=False
)
)
# 0.4. Inner Oort Cloud
x, y, z = create_corona_sphere(INNER_OORT_CLOUD_AU)
# Define the text string once
inner_oort_info = (
"The Oort Cloud is a theoretical, vast, spherical shell of icy objects that surrounds the<br>"
"Solar System at distances ranging from approximately 2,000 AU to 100,000 AU from the Sun.<br>"
"Predominantly composed of cometary nuclei—small, icy bodies made of water ice, ammonia, and methane.<br>"
"Believed to be the source of long-period comets that enter the inner Solar System with orbital<br>"
"periods exceeding 200 years.<br><br>"
"Inner Oort Cloud (Hills Cloud): Extends from about 2,000 AU to 20,000 AU. More tightly bound to the<br>"
"Sun. More tightly bound to the Solar System compared to the outer Oort Cloud. It serves as an<br>"
"intermediate zone between the Kuiper Belt and the outer Oort Cloud."
)
# Create a text list matching the number of points
text_array_inner_oort = [inner_oort_info for _ in range(len(x))]
customdata_array_inner_oort = ["Inner Oort Cloud" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=1.0,
color='white', # Yellow approximates the visible color
opacity=0.3
),
name='Inner Oort Cloud',
text=text_array_inner_oort,
customdata=customdata_array_inner_oort, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='white',
size=1.0,
symbol='circle-open',
opacity=0.3
),
name='Inner Oort Cloud',
text=['Inner Oort Cloud from estimated 2,000 to 20,000 AU.'],
hovertemplate='%{text}<extra></extra>',
showlegend=False
)
)
# 0.45. Inner Limit of Inner Oort Cloud
x, y, z = create_corona_sphere(INNER_LIMIT_OORT_CLOUD_AU)
# Define the text string once
inner_limit_oort_info = (
"The Oort Cloud is a theoretical, vast, spherical shell of icy objects that surrounds the<br>"
"Solar System at distances ranging from approximately 2,000 AU to 100,000 AU from the Sun.<br>"
"Predominantly composed of cometary nuclei—small, icy bodies made of water ice, ammonia, and methane.<br>"
"Believed to be the source of long-period comets that enter the inner Solar System with orbital<br>"
"periods exceeding 200 years.<br><br>"
"Inner Oort Cloud (Hills Cloud): Extends from about 2,000 AU to 20,000 AU. More tightly bound to the<br>"
"Sun. More tightly bound to the Solar System compared to the outer Oort Cloud. It serves as an<br>"
"intermediate zone between the Kuiper Belt and the outer Oort Cloud."
)
# Create a text list matching the number of points
text_array_inner_limit_oort = [inner_limit_oort_info for _ in range(len(x))]
customdata_array_inner_limit_oort = ["Inner Limit of Oort Cloud" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=1.0,
color='white',
opacity=0.3
),
name='Inner Limit of Oort Cloud',
text=text_array_inner_limit_oort,
customdata=customdata_array_inner_limit_oort, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='white',
size=1.0,
symbol='circle-open',
opacity=0.3
),
name='Inner Limit of Oort Cloud',
text=['Inner Oort Cloud from estimated 2,000 to 20,000 AU.'],
hovertemplate='%{text}<extra></extra>',
showlegend=False
)
)
# 0.5. Solar Wind and Heliopause Sphere
x, y, z = create_corona_sphere(HELIOPAUSE_RADII * SOLAR_RADIUS_AU)
# Define the text string once
solar_wind_info = (
"Solar Wind (Heliosheath extends from ~120 to 150 AU at the Heliopause)<br>"
"Temperature: ~1,000,000K on average<br>"
"Black body radiation at 2.897 nm falls within the X-ray region of the<br>"
"electromagnetic spectrum, which is invisible to the human eye.<br>"
"Stream of charged particles (plasma) ejected from the upper atmosphere of the Sun.<br>"
"At the Heliopause the speed of the solar wind stops due to interaction with the<br>"
"interstellar medium. Voyager 1 encountered the Heliopause at ~123 AU. This is<br>"
"considered the end of the Sun's influence and the start of interstellar space.<br>"
"This region is turbulent and variable, and speeds slow to ~100 km/s at the Heliopause."
)
# Create a text list matching the number of points
text_array_solar_wind = [solar_wind_info for _ in range(len(x))]
customdata_array_solar_wind = ["Solar Wind Heliopause" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=0.5,
color='rgb(135, 206, 250)', # Nearest visible approximatation
opacity=0.2
),
name='Solar Wind Heliopause',
text=text_array_solar_wind, # Replicated text
customdata=customdata_array_solar_wind, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add Heliopause shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='rgb(135, 206, 250)',
size=0.5,
symbol='circle',
opacity=0.2
),
name='Solar Wind Heliopause',
text=['Solar Wind Heliopause (extends to 123 AU)'],
# hoverinfo='text',
hovertemplate='%{text}<extra></extra>',
showlegend=False
)
)
# 0.7. Solar Wind and Termination Shock Sphere
x, y, z = create_corona_sphere(TERMINATION_SHOCK_AU)
# Define the text string once
termination_shock_info = (
"Solar Wind Termination Shock (extends to from ~75 to 100 AU)<br>"
"Temperature: from ~100,000 to 400,000K before the shock to ~1M K after it.<br>"
"Black body radiation at 11.59 nm, extreme ultraviolet,<br>"
"which is invisible or at the edge of visibility.<br>"
"The Termination Shock is the region where the solar wind slows down from<br>"
"supersonic to subsonic speeds due to interaction with the interstellar<br>"
"medium. The kinetic energy transfers into heat, increasing abruptly.<br>"
"Voyager 1 encountered the Termination Shock at 94 AU, while Voyager 2 at 84 AU.<br>"
"After the Termination Shock the speeds slow down to ~100 to 200 km/s."
)
# Create a text list matching the number of points
text_array_termination_shock = [termination_shock_info for _ in range(len(x))]
customdata_array_termination_shock = ["Solar Wind Termination Shock" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=0.5,
color='rgb(240, 244, 255)', # Nearest visible approximatation
opacity=0.2
),
name='Solar Wind Termination Shock',
text=text_array_termination_shock, # Replicated text
customdata=customdata_array_termination_shock, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add Termination Shock shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='rgb(240, 244, 255)',
size=0.5,
symbol='circle',
opacity=0.2
),
name='Solar Wind Termination Shock',
text=['Solar Wind Termination Shock (extends to 94 AU)'],
# hoverinfo='text',
hovertemplate='%{text}<extra></extra>',
showlegend=False
)
)
# 1. Outer Corona Sphere (most expansive, very diffuse)
x, y, z = create_corona_sphere(OUTER_CORONA_RADII * SOLAR_RADIUS_AU)
# Define the text string once
outer_corona_info = (
"Solar Outer Corona (extends to 50 solar radii or more, ~0.2 AU)<br>"
"It is the most tenuous and expansive layer of the solar atmosphere<br>"
"Temperature: ~2-3M K, or an average of 2.5M K<br>"
"It radiates at an average wavelength of 1.159 nm, which falls within the<br>"
"extreme ultraviolet to X-ray regions of the electromagnetic spectrum.<br>"
"The solar Corona generates the solar wind, a stream of electrons, protons, and Helium<br>"
"travelling at supersonic speeds between 300 and 800 km/s, and temperatures to 2M K."
)
# Create a text list matching the number of points
text_array_outer_corona = [outer_corona_info for _ in range(len(x))]
customdata_array_outer_corona = ["Outer Corona" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=0.75,
color='rgb(25, 25, 112)', # approximate visualization
opacity=0.3
),
name='Outer Corona',
text=text_array_outer_corona, # Replicated text
customdata=customdata_array_outer_corona, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add outer corona shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='rgb(25, 25, 112)',
size=0.75,
symbol='circle',
opacity=0.3
),
name='Outer Corona',
text=['Solar Outer Corona (extends to 50 solar radii or more, or 0.2 AU)'],
# hoverinfo='text',
hovertemplate='%{text}<extra></extra>',
showlegend=False
)
)
# 2. Inner Corona Sphere
x, y, z = create_corona_sphere(INNER_CORONA_RADII * SOLAR_RADIUS_AU)
# Define the text string once
inner_corona_info = (
"Solar Inner Corona (extends to 2-3 solar radii, ~0.014 AU)<br>"
"Region of intense heating and complex magnetic activity"
"Temperature: 1-2M K, or an average of about 1.5M K<br>"
"It radiates at an average wavelenght of 1.93 nm, which falls within<br>"
"the extreme ultraviolet to soft X-ray regions of the electromagnetic spectrum."
)
# Create a text list matching the number of points
text_array_inner_corona = [inner_corona_info for _ in range(len(x))]
customdata_array_inner_corona = ["Inner Corona" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=1,
color='rgb(0, 0, 255)', # Warmer tint
opacity=0.09
),
name='Inner Corona',
text=text_array_inner_corona, # Replicated text
customdata=customdata_array_inner_corona, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add inner corona shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='rgb(0, 0, 255)',
size=1,
symbol='circle',
opacity=0.09
),
name='Inner Corona Shell',
text=['Solar Inner Corona (extends to 2-3 solar radii)'],
hoverinfo='text',
showlegend=False
)
)
# 2.5. Chromosphere
x, y, z = create_corona_sphere(CHROMOSPHERE_RADII * SOLAR_RADIUS_AU)
# Define the text string once
chromosphere_info = (
"Chromosphere<br>"
"Channels energy into the corona through a radiative process<br>"
"Site of Spicules, solar flares, and prominences"
"Radius: from Photosphere to 1.5 Solar radii or ~0.00465 - 0.0070 AU<br>"
"Temperature: ~6,000 to 20,000 K, for a average of 10,000 K<br>"
"Radiates at an average peak wavelength of ~290 nm, ultraviolet range, invisible.<br>"
)
# Create a text list matching the number of points
text_array_chromosphere = [chromosphere_info for _ in range(len(x))]
customdata_array_chromosphere = ["Chromosphere" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=1.25,
color='rgb(30, 144, 255)', # approximate visible
opacity=0.10
),
name='Chromosphere',
text=text_array_chromosphere, # Replicated text
customdata=customdata_array_chromosphere, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add chromosphere shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='rgb(30, 144, 255)',
size=1.25,
symbol='circle',
opacity=0.10
),
name='Chromosphere Shell',
text=['Solar Chromosphere (surface temperature ~6,000 to 20,000 K)'],
hoverinfo='text',
showlegend=False
)
)
# 3. Convective Zone and Photoshere Sphere
x, y, z = create_corona_sphere(SOLAR_RADIUS_AU)
# Define the text string once
photosphere_info = (
"Solar Convective Zone and Photosphere (visible surface)<br>"
"Radius: from 0.7 to 1 Solar radius, or ~0.00465 AU<br>"
"Temperature: from 2M K at the Radiative Zone to ~5,500K at the Photosphere<br>"
"Convection transports energy to the visible \"surface\" of the Sun.<br>"
"The convection process starts at the Radiative Zone<br>"
"It is noted by granulation patterns and magnetic field generation<br>"
"At the Photosphere, the energy is radiated as visible light<br>"
"Radiation emits at a peak wavelength at 527.32 nm, which is in the green spectrum.<br>"
"The Sun's emitted light is a combination of all visible wavelengths, resulting in a<br>"
"yellowish-white color. The Photosphere is noted by sunspots, faculae, and solar granules."
)
# Create a text list matching the number of points
text_array_photosphere = [photosphere_info for _ in range(len(x))]
customdata_array_photosphere = ["Photosphere" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=7.0,
color='rgb(255, 244, 214)', # Yellow approximates the visible color
opacity=1.0
),
name='Convective Zone and Photosphere',
text=text_array_photosphere, # Replicated text
customdata=customdata_array_photosphere, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add photosphere shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='rgb(255, 244, 214)',
size=7.0,
symbol='circle',
opacity=1.0
),
name='Photosphere',
text=['Solar Photosphere (surface temperature ~6,000K)'],
hoverinfo='text',
showlegend=False
)
)
# 4. Radiative Zone
x, y, z = create_corona_sphere(RADIATIVE_ZONE_AU)
# Define the text string once
radiative_zone_info = (
"Solar Radiative Zone (extends from about 0.2 to 0.7 solar radii, ~0.00325 AU)<br>"
"Temperature: ranges from about 7M K near the core to about 2M K near the convective zone<br>"
"A region where energy moves from the core to the convective zone, taking 170,000 years<br>"
"Energy is transported by radiative diffusion, through photon absorption and re-emission"
)
# Create a text list matching the number of points
text_array_radiative_zone = [radiative_zone_info for _ in range(len(x))]
customdata_array_radiative_zone = ["Radiative Zone" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=7,
color='rgb(30, 144, 255)', # arbitrary color for contrast
opacity=1.0
),
name='Radiative Zone',
text=text_array_radiative_zone, # Replicated text
customdata=customdata_array_radiative_zone, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add radiative zone shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='rgb(30, 144, 255)',
size=7,
symbol='circle',
opacity=1.0
),
name='Radiative Zone Shell',
text=['Solar Radiative Zone (extends to 0.2 to 0.7 solar radii)'],
hoverinfo='text',
showlegend=False
)
)
# 5. Core
x, y, z = create_corona_sphere(CORE_AU)
# Define the text string once
core_info = (
"Solar Core<br>"
"Radius: ~0.00093 AU or about 0.2 Solar radii<br>"
"Temperature: ~15M K<br>"
"Nuclear fusion and energy generation<br>"
"Energy is transported by radiative diffusion<br>"
"Site of proton-proton chain reactions<br>"
"The Sun is currently mostly (98 to 99 percent) fusing hydrogen into helium<br>"
"1 to 2 percent of the energy is produced by the Carbon-Nitrogen-Oxygen Cycle"
)
# Create a text list matching the number of points
text_array_core = [core_info for _ in range(len(x))]
customdata_array_core = ["Core" for _ in range(len(x))]
traces.append(
go.Scatter3d(
x=x, y=y, z=z,
mode='markers',
marker=dict(
size=10,
color='rgb(70, 130, 180)',
opacity=1.0
),
name='Core',
text=text_array_core, # Replicated text
customdata=customdata_array_core, # Replicated customdata
hovertemplate='%{text}<extra></extra>',
showlegend=True
)
)
# Add core shell
traces.append(
go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers',
marker=dict(
color='rgb(70, 130, 180)',
size=10,
symbol='circle',
opacity=1.0
),
name='Core Shell',
text=['Solar Core (temperature ~15M K)'],
hoverinfo='text',
showlegend=False
)
)
return traces
# Add base traces to figure
traces = create_layer_traces()
for trace in traces:
fig.add_trace(trace)
# If this is for animation, add the traces to each frame
if animate and frames is not None:
for frame in frames:
frame_data = list(frame.data) # Convert tuple to list if necessary
frame_data.extend(traces)
frame.data = frame_data
return fig
def create_sun_hover_text():
"""
Creates hover text for the Sun visualization with information about each layer.
Future expansion could include dynamic temperature and size data.
Returns:
dict: Hover text for each layer of the Sun
"""
return {
'photosphere': (
'Solar Photosphere<br>'
'Temperature: ~6,000K<br>'
'Radius: 0.00465 AU'
),
'inner_corona': (
'Inner Corona<br>'
'Temperature: >2,000,000K<br>'
'Extends to: 2-3 solar radii (~0.014 AU)'
),
'outer_corona': (
'Outer Corona<br>'
'Temperature: ~1,000,000K<br>'
'Extends to: ~50 solar radii (~0.2 AU)'
)
}
# In the create_corona_sphere function, increase the number of points
def create_corona_sphere(radius, n_points=100): # Increased from 50 to 100 points
"""Create points for a sphere surface to represent corona layers."""
phi = np.linspace(0, 2*np.pi, n_points)
theta = np.linspace(-np.pi/2, np.pi/2, n_points)
phi, theta = np.meshgrid(phi, theta)
x = radius * np.cos(theta) * np.cos(phi)
y = radius * np.cos(theta) * np.sin(phi)
z = radius * np.sin(theta)
return x.flatten(), y.flatten(), z.flatten()
def get_default_camera():
"""Return the default orthographic camera settings for top-down view"""
return {
"projection": {
"type": "orthographic"
},
# Looking straight down the z-axis
"eye": {"x": 0, "y": 0, "z": 1}, # Position above the x-y plane
"center": {"x": 0, "y": 0, "z": 0}, # Looking at origin
"up": {"x": 0, "y": 1, "z": 0} # "Up" direction aligned with y-axis
}
def calculate_axis_range(objects_to_plot):
"""Calculate appropriate axis range based on outermost planet"""
# Find the maximum semi-major axis of selected planets
max_orbit = max(planetary_params[obj['name']]['a']
for obj in objects_to_plot
if obj['name'] in planetary_params)
# Add 20% padding
max_range = max_orbit * 1.2
# Print debug info
print(f"\nAxis range calculation:")
print(f"Maximum orbit (AU): {max_orbit}")
print(f"Range with padding: ±{max_range}")
return [-max_range, max_range]
def plot_actual_orbits(fig, planets_to_plot, dates_lists, center_id='Sun', show_lines=False):
for planet in planets_to_plot:
dates_list = dates_lists.get(planet, [])
if not dates_list:
print(f"No dates available for {planet}, skipping.")
continue
obj_info = next((obj for obj in objects if obj['name'] == planet), None)
if not obj_info:
continue
trajectory = fetch_trajectory(obj_info['id'], dates_list, center_id=center_id, id_type=obj_info.get('id_type'))
# Now trajectory is a list of positions
if trajectory:
x = [pos['x'] for pos in trajectory if pos is not None]
y = [pos['y'] for pos in trajectory if pos is not None]
z = [pos['z'] for pos in trajectory if pos is not None]
if show_lines:
mode = 'lines'
line = dict(color=color_map(planet), width=1)
marker = None
else:
mode = 'markers'
line = None
marker = dict(color=color_map(planet), size=1)
fig.add_trace(
go.Scatter3d(
x=x,
y=y,
z=z,
mode=mode,
line=line,
marker=marker,
name=f"{planet} Orbit",
hoverinfo='none',
hovertemplate=None,
showlegend=True
)
)
# Function to fetch the position of a celestial object for a specific date
def fetch_position(object_id, date_obj, center_id='Sun', id_type=None, override_location=None, mission_url=None, mission_info=None):
try:
# Convert date to Julian Date
times = Time([date_obj])
epochs = times.jd.tolist()
# Set location
if override_location is not None:
location = override_location
else:
location = '@' + str(center_id)
# Query the Horizons system with coordinates relative to location
obj = Horizons(id=object_id, id_type=id_type, location=location, epochs=epochs)
vectors = obj.vectors()
if len(vectors) == 0:
print(f"No data returned for object {object_id} on {date_obj}")
return None
# Extract desired fields with error handling
x = float(vectors['x'][0]) if 'x' in vectors.colnames else None
y = float(vectors['y'][0]) if 'y' in vectors.colnames else None
z = float(vectors['z'][0]) if 'z' in vectors.colnames else None
range_ = float(vectors['range'][0]) if 'range' in vectors.colnames else None # Distance in AU from the Sun
range_rate = float(vectors['range_rate'][0]) if 'range_rate' in vectors.colnames else None # AU/day
vx = float(vectors['vx'][0]) if 'vx' in vectors.colnames else None # AU/day
vy = float(vectors['vy'][0]) if 'vy' in vectors.colnames else None
vz = float(vectors['vz'][0]) if 'vz' in vectors.colnames else None
velocity = np.sqrt(vx**2 + vy**2 + vz**2) if vx is not None and vy is not None and vz is not None else 'N/A'
# Calculate distance in light-minutes and light-hours
distance_lm = range_ * LIGHT_MINUTES_PER_AU if range_ is not None else 'N/A'
distance_lh = (distance_lm / 60) if isinstance(distance_lm, float) else 'N/A'
# Retrieve orbital period from planetary_params if available
orbital_period = 'N/A'
if object_id in [obj['id'] for obj in objects]:
obj_name = next((obj['name'] for obj in objects if obj['id'] == object_id), None)
if obj_name and obj_name in planetary_params:
a = planetary_params[obj_name]['a'] # Semi-major axis in AU
orbital_period_years = np.sqrt(a ** 3) # Period in Earth years
orbital_period = f"{orbital_period_years:.2f}"
return {
'x': x,
'y': y,
'z': z,
'range': range_,
'range_rate': range_rate,
'vx': vx,
'vy': vy,
'vz': vz,
'velocity': velocity,
'distance_lm': distance_lm,
'distance_lh': distance_lh,
'mission_info': mission_info, # Include mission info if available
'orbital_period': orbital_period # Include orbital period
}
except Exception as e: