forked from Christian-Palmroos/sub_pfss_analysis_notebook
-
Notifications
You must be signed in to change notification settings - Fork 1
/
pfss_notebook_lib.py
1531 lines (1165 loc) · 51.3 KB
/
pfss_notebook_lib.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
'''
A library intended for importing into pfsspy_notebook.ibynp. It contains the necessary functions
for seeking the footpoints of IMF field lines connecting back to the photosphere and plotting them.
@Author: Christian Palmroos
Last updated: 2022-09-28
'''
# imports:
import os
import numpy as np
import pandas as pd
import astropy.constants as const
import astropy.units as units
# import astropy.coordinates
import astropy.units as u
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import matplotlib.patches as mpatch
import cmasher as cmr
import pfsspy
import pickle
import warnings
import sunpy.map
from matplotlib.collections import LineCollection
# from astropy.time import Time
from astropy.coordinates import SkyCoord
from pfsspy import tracing
# from pfsspy.sample_data import get_gong_map
# from matplotlib import cm
from matplotlib.offsetbox import AnchoredText
from sunpy.net import Fido, attrs as a
from sunpy.coordinates import frames
# some matplotlib settings:
plt.rcParams['axes.linewidth'] = 1.5
plt.rcParams['font.size'] = 20
plt.rcParams['agg.path.chunksize'] = 20000
# living on the edge:
warnings.simplefilter('ignore')
# ----------------------------------------------------------------------------------------
def save_pickle(obj, file_name):
"""
Saves an object as a python pickle file.
Parameters:
-----------
obj : object
file_name : str
"""
with open(file_name, 'wb') as file:
pickle.dump(obj, file)
def load_pickle(file_name):
"""
Loads a python pickle and returns it.
Parameters:
-----------
file_name : str
Returns:
----------
obj : object
"""
with open(file_name, 'rb') as file:
obj = pickle.load(file)
if isinstance(obj,list):
obj = np.asarray(obj, dtype=object)
return obj
def get_color(key: str = None) -> str:
"""
Returns the right color for an object according to SOLAR-MACH tool.
params
-------
key: str (default = None)
The name of the spacecraft or planet.
returns
-------
color: str (default = 'k')
The color identifier that matplotlib understands
"""
if key is not None:
key = key.lower()
else:
key = 'default'
color_dict = {'mercury': 'darkturquoise',
'venus': 'darkorchid',
'earth': 'green',
'mars': 'maroon',
'jupiter': 'navy',
'stereo a': 'red',
'stereo-a': 'red',
'stereo b': 'b',
'stereo-b': 'b',
'soho': 'darkgreen',
'solo': 'dodgerblue',
'solar orbiter': 'dodgerblue',
'psp': 'purple',
'parker solar probe': 'purple',
'bepicolombo': 'orange',
'maven': 'brown',
'mars express': 'darkorange',
'messenger': 'olivedrab',
'juno': 'orangered',
'cassini': 'mediumvioletred',
'rosetta': 'blueviolet',
'pioneer 10': 'teal',
'pioneer 11': 'darkblue',
'ulysses': 'dimgray',
'voyager 1': 'darkred',
'voyager 2': 'midnightblue',
'default': 'k'}
try:
return color_dict[key]
except KeyError:
return color_dict['default']
def get_sc_data(csvfile: str):
"""
Reads the contents of solar-mach produced csv file, and returns lists
of necessary data to run pfss field line tracing analysis.
params
------
csvfile: str
The name of the csv file one wants to read
returns
-------
names: list[str]
List of names
sw: list[int/float]
List of solar winds in units of km/s
dist: list[int/float]
List of heliocentric distances in units of AU
lons: list[float]
List of Carrington longitudes of the corresponding objects
lats: list[float]
List of Carrington latitudes of the corresponding objects
"""
if type(csvfile) is not str:
raise TypeError("File name is not a string.")
csvdata = pd.read_csv(csvfile)
names = list(csvdata['Spacecraft/Body'])
lons = list(csvdata['Carrington Longitude (°)'])
# for some reason the output of Streamlit Solar-MACH is different from notebook-produced csv
try:
lats = list(csvdata['Latitude (°)'])
except:
lats = list(csvdata['Carrington Latitude (°)'])
dist = au_to_km(list(csvdata['Heliocentric Distance (AU)']))
sw = list(csvdata['Vsw'])
return names, sw, dist, lons, lats
def field_line_accuracy(flines):
"""
Calculates at least now the central point, average distance from the central point and
the standard deviation of the photospheric footpoints for a set of field lines
"""
if isinstance(flines[0], list):
flines = flatten(flines)
footpoints = []
for fline in flines:
r, lon, lat = get_coord_values(fline)
# Index 0 of coordinates corresponds to photospheric coordinates, index -1 to pfss
footpoint = [lon[0], lat[0]]
footpoints.append(footpoint)
# Declare longitudes and latitudes of footpoints
footp_lons = [pair[0] for pair in footpoints]
footp_lats = [pair[1] for pair in footpoints]
# If separation in longitudes is over half a circle, then points are probably
# scattered near the 0-deg line -> transfer them far enough from that line
# while calculations are ran
if max(footp_lons) > min(footp_lons)+180.0:
# Transfer of longitudes
footp_lons = shift_longitudes(footp_lons)
# Calculate the central point
c_point = [np.mean(footp_lons), np.mean(footp_lats)]
# Standard deviation of longitudes and latitudes
lon_std = np.std(footp_lons)
lat_std = np.std(footp_lats)
# Calculate mean distance from the central point
dist_sum = 0
for i in range(len(footp_lons)):
lon1, lat1 = footp_lons[i], footp_lats[i]
angular_separation = orthodrome(lon1, lat1, c_point[0], c_point[1])
# Distance is in solar radii
distance_rs = angular_separation
dist_sum = dist_sum + distance_rs
# Transfer lons and the central point back the same amount that the longitudes were moved
footp_lons = shift_longitudes(footp_lons, shift=-180.0)
c_point[0] = shift_longitudes([c_point[0]], shift=-180.0)[0]
else:
# Calculate the central point
c_point = [np.mean(footp_lons), np.mean(footp_lats)]
# Standard deviation of longitudes and latitudes
lon_std = np.std(footp_lons)
lat_std = np.std(footp_lats)
# Calculate mean distance from the central point
dist_sum = 0
for i in range(len(footp_lons)):
lon1, lat1 = footp_lons[i], footp_lats[i]
angular_separation = orthodrome(lon1, lat1, c_point[0], c_point[1])
# Distance is in solar radii
distance_rs = angular_separation
dist_sum = dist_sum + distance_rs
avg_dist = dist_sum/len(footp_lons)
return footpoints, c_point, avg_dist, [lon_std, lat_std]
def shift_longitudes(lons, shift=180.0):
"""
Shifts the longitudes by <shift> amount
"""
if shift>0:
lons = [lon+shift for lon in lons]
lons = [lon-360.0 if lon>360 else lon for lon in lons]
else:
# If shift is negative, points are likely being moved back to their
# original place
lons = [lon+shift for lon in lons]
lons = [lon+360.0 if lon<0 else lon for lon in lons]
return lons
def map_on_surface(fps, c_point, avg_d, shift=None, zoom=None, show_avg_d=False):
"""
Plots a really simple 2d representation of fieldline objects' footpoints.
"""
import matplotlib.patches as mpatch
centre = np.array(c_point)
fpslons = [item[0] for item in fps]
fpslats = [item[1] for item in fps]
if shift is not None:
fpslons = shift_longitudes(fpslons, shift=shift)
centre[0] = c_point[0]+shift
fig_tuple = (16, 7)
if zoom is not None:
fig_tuple = (12, 12)
fig = plt.figure(figsize=fig_tuple)
ax = plt.subplot()
ax.scatter(fpslons[0], fpslats[0], c='navy', s=60, label="original footpoint")
ax.scatter(fpslons[1:], fpslats[1:], c='C0', label="dummy footpoints")
ax.scatter(centre[0], centre[1], c='r', label="avg(lons,lats)")
if show_avg_d:
avg_d_deg = np.rad2deg(avg_d)
ax.add_patch(mpatch.Circle((centre[0], centre[1]), avg_d_deg, color='r', lw=0.8, ls='--', fill=False))
plt.ylim(-90, 90)
plt.xlim(0, 360)
if zoom is not None:
plt.ylim(centre[1]-zoom/2, centre[1]+zoom/2)
plt.xlim(centre[0]-zoom/2, centre[0]+zoom/2)
plt.legend()
plt.grid("True")
plt.show()
def orthodrome(lon1, lat1, lon2, lat2, rad=False) -> float:
"""
Calculates the othodrome (total angular separtion) between two coordinates defined by their lon/lat positions
params
-------
lon1: int/float
Longitude of the first coordinate point
lat1: int/float
Latitude of the first coordinate point
lon2: int/float
See lon1
lat2: int/float
See lat1
rad: bool (default = False)
Is the input given as radians? If not, treat them as degrees
returns
-------
ortho: float
The total angular separation between (lon1,lat1) and (lon2,lat2)
"""
if(rad == False):
lon1 = np.deg2rad(lon1)
lon2 = np.deg2rad(lon2)
lat1 = np.deg2rad(lat1)
lat2 = np.deg2rad(lat2)
ortho = np.arccos(np.sin(lat1)*np.sin(lat2) + np.cos(lat1)*np.cos(lat2)*np.cos(lon2-lon1))
return ortho
def ortho_to_points(lon1, lat1, orthodrome, rad=False):
"""
Calculates a lon/lat pair from a reference point given orthodrome away.
"""
if rad == False:
lon1 = np.deg2rad(lon1)
lat1 = np.deg2rad(lat1)
lon2, lat2 = np.cos(lon1+orthodrome), np.sin(lat1+orthodrome)
return lon2, lat2
def _isstreamlit():
"""
Function to check whether python code is run within streamlit
Returns
-------
use_streamlit : boolean
True if code is run within streamlit, else False
"""
# https://discuss.streamlit.io/t/how-to-check-if-code-is-run-inside-streamlit-and-not-e-g-ipython/23439
try:
from streamlit.scriptrunner import get_script_run_ctx
if not get_script_run_ctx():
use_streamlit = False
else:
use_streamlit = True
except ModuleNotFoundError:
use_streamlit = False
return use_streamlit
def null_decorator(a):
# decorator that does nothing
return a
# if run inside streamlit, use streamlit's caching decorator on get_pfss_hmimap()
if _isstreamlit():
import streamlit as st
st_cache_decorator = st.cache(persist=True, allow_output_mutation=True)
else:
st_cache_decorator = null_decorator
@st_cache_decorator
def get_pfss_hmimap(filepath, email, carrington_rot, date, rss=2.5, nrho=35):
"""
Downloading hmi map or calculating the PFSS solution
params
-------
filepath: str
Path to the hmimap, if exists.
email: str
The email address of a registered user
carrington_rot: int
The Carrington rotation corresponding to the hmi map
date: str
The date of the map. Format = 'YYYY/MM/DD'
rss: float (default = 2.5)
The height of the potential field source surface for the solution.
nrho: int (default = 35)
The resolution of the PFSS-solved field line objects
returns
-------
output: hmi_synoptic_map object
The PFSS-solution
"""
time = a.Time(date, date)
pfname = f"PFSS_output_{str(time.start.datetime.date())}_CR{str(carrington_rot)}_SS{str(rss)}_nrho{str(nrho)}.p"
# Check if PFSS file already exists locally:
print(f"Searching for PFSS file from {filepath}")
try:
with open(f"{filepath}/{pfname}", 'rb') as f:
u = pickle._Unpickler(f)
u.encoding = 'latin1'
output = u.load()
print("Found pickled PFSS file!")
# If not, then download MHI mag, calc. PFSS, and save as picle file for next time
except FileNotFoundError:
print("PFSS file not found.\nDownloading...")
series = a.jsoc.Series('hmi.synoptic_mr_polfil_720s')
crot = a.jsoc.PrimeKey('CAR_ROT', carrington_rot)
result = Fido.search(time, series, crot, a.jsoc.Notify(email))
files = Fido.fetch(result)
hmi_map = sunpy.map.Map(files[0])
pfsspy.utils.fix_hmi_meta(hmi_map)
print('Data shape: ', hmi_map.data.shape)
hmi_map = hmi_map.resample([360, 180]*units.pix)
print('New shape: ', hmi_map.data.shape)
pfss_input = pfsspy.Input(hmi_map, nrho, rss)
output = pfsspy.pfss(pfss_input)
with open(pfname, 'wb') as f:
pickle.dump(output, f)
return output
def circle_around(x, y, n, r=0.1):
"""
Produces new points around a (x,y) point in a circle.
At the moment does not work perfectly in the immediate vicinity of either pole.
params
-------
x,y: int/float
Coordinates of the original point
n: int
The amount of new points around the origin
r: int/float (default = 0.1)
The radius of the circle at which new points are placed
returns
-------
pointlist: list[float]
List of new points (tuples) around the original point in a circle, placed at equal intervals
"""
origin = (x, y)
x_coords = np.array([])
y_coords = np.array([])
for i in range(0, n):
theta = (2*i*np.pi)/n
newx = origin[0] + r*np.cos(theta)
newy = origin[1] + r*np.sin(theta)
if newx >= 2*np.pi:
newx = newx - 2*np.pi
if newy > np.pi/2:
overflow = newy - np.pi/2
newy = newy - 2*overflow
if newy < -np.pi/2:
overflow = newy + np.pi/2
newy = newy + 2*overflow
x_coords = np.append(x_coords, newx)
y_coords = np.append(y_coords, newy)
pointlist = np.array([x_coords, y_coords])
return pointlist
def vary_flines(lon, lat, hmimap, n_varies, rss):
"""
Finds a set of sub-pfss fieldlines connected to or very near a single footpoint on the pfss.
lon: longitude of the footpoint [rad]
lat: latitude of the footpoint [rad]
n_varies: tuple that holds the amount of circles and the number of dummy flines per circle
if type(n_varies)=int, consider that as the amount of circles, and set the
amount of dummy flines per circle to 16
params
-------
lon: int/float
The longitude of the footpoint in radians
lat: int/float
The latitude of the footpoint in radians
hmimap: hmi_synoptic_map object
The pfss-solution used to calculate the field lines
n_varies: list[int,int] or int
A list that holds the amount of circles and the number of dummy flines per circle
if type(n_varies)=int, consider that as the amount of circles, and set the
amount of dummy flines per circle to 16
rss: float
Heliocentric height of the source surface
returns
-------
coordlist: list[float,float,float]
List of coordinate triplets of the original field lines (lon,lat,height)
flinelist: list[FieldLine-object]
List of Fieldline objects of the original field lines
varycoords: list[float,float,float]
List of coordinate triplets of the varied field lines
varyflines: list[FieldLine-object]
List of Fieldline objects of the varied field lines
"""
# Field lines per n_circles (circle)
if isinstance(n_varies,list):
n_circles = n_varies[0]
n_flines = n_varies[1]
else:
n_circles = n_varies
n_flines = 16
# First produce new points around the given lonlat_pair
lons, lats= np.array([lon]), np.array([lat])
increments = np.array([0.03, 0.05, 0.07, 0.09, 0.11, 0.13, 0.15, 0.17, 0.19, 0.21, 0.23, 0.25, 0.27, 0.29])
for circle in range(n_circles):
newlons, newlats = circle_around(lon,lat,n_flines,r=increments[circle])
lons, lats = np.append(lons,newlons), np.append(lats,newlats)
pointlist = np.array([lons, lats])
# Trace fieldlines from all of these points
varycoords, varyflines = get_field_line_coords(pointlist[0],pointlist[1],hmimap, rss)
# Because the original fieldlines and the varied ones are all in the same arrays,
# Extract the varied ones to their own arrays
coordlist, flinelist = [], []
# Total amount of flines = 1 + (circles) * (fieldlines_per_circle)
total_per_fp = n_flines*n_circles+1
erased_indices = []
for i in range(len(varycoords)):
# n_flines*n_circles = the amount of extra field lines between each "original" field line
if i%(total_per_fp)==0:
erased_indices.append(i)
# pop(i) removes the ith element from the list and returns it
# -> we append it to the list of original footpoint fieldlines
coordlist.append(varycoords[i]) #.pop(i)
flinelist.append(varyflines[i])
# Really ugly quick fix to erase values from varycoords and varyflines
for increment, index in enumerate(erased_indices):
varycoords.pop(index-increment)
varyflines.pop(index-increment)
return coordlist, flinelist, varycoords, varyflines
def get_coord_values(field_line):
"""
Gets the coordinate values from FieldLine object and makes sure that they are in the right order.
params
-------
field_line: FieldLine object
returns
-------
fl_r: list[float]
The list of heliocentric distances of each segment of the field line
fl_lon: list[float]
The list of Carrington longitudes of each field line segment
fl_lat: list[float]
The list of Carrington latitudes of each field line segment
"""
# first check that the field_line object is oriented correctly (start on photosphere and end at pfss)
fl_coordinates = field_line.coords
fl_coordinates = check_field_line_alignment(fl_coordinates)
fl_r = fl_coordinates.radius.value / const.R_sun.value
fl_lon = fl_coordinates.lon.value
fl_lat = fl_coordinates.lat.value
return fl_r, fl_lon, fl_lat
def get_field_line_coords(longitude, latitude, hmimap, seedheight):
"""
Returns triplets of open magnetic field line coordinates, and the field line object itself
params
-------
longitude: int/float
Carrington longitude of the seeding point for the FieldLine tracing
latitude: int/float
Carrington latitude of the seeding point for the FieldLine tracing
hmimap: hmi_synoptic_map object
seedheight: float
Heliocentric height of the seeding point
returns
-------
coordlist: list[list[float,float,float]]
The list of lists of all coordinate triplets that correspond to the FieldLine objects traced
flinelist: list[FieldLine]
List of all FieldLine objects traced
"""
# The amount of coordinate triplets we are going to trace
try:
coord_triplets = len(latitude)
except TypeError:
coord_triplets = 1
latitude = [latitude]
longitude = [longitude]
# The loop in which we trace the field lines and collect them to the coordlist
coordlist = []
flinelist = []
for i in range(coord_triplets):
# Inits for finding another seed point if we hit null or closed line
turn = 'lon'
sign_switch = 1
# Steps to the next corner, steps taken
corner_tracker = [1,0]
init_lon, init_lat = longitude[i], latitude[i]
# Keep tracing the field line until a valid one is found
while(True):
# Trace a field line downward from the point lon,lat on the pfss
fline = trace_field_line(longitude[i], latitude[i], hmimap, seedheight=seedheight)
radius0 = fline.coords.radius[0].value
radius9 = fline.coords.radius[-1].value
bool_key = (radius0==radius9)
# If fline is not a valid field line, then spiral out from init_point and try again
# Also check if this is a null line (all coordinates identical)
# Also check if polarity is 0, meaning that the field line is NOT open
if( (len(fline.coords) < 10) or bool_key or fline.polarity==0): #fline.polarity==0
longitude[i], latitude[i], sign_switch, corner_tracker, turn = spiral_out(longitude[i], latitude[i], sign_switch, corner_tracker, turn)
# If there was nothing wrong, break the loop and proceed with the traced field line
else:
break
# Check that we are not too far from the original coordinate
if(corner_tracker[0] >= 10):
print(f"no open field line found in the vicinity of {init_lon},{init_lat}")
break
# Get the field line coordinate values in the correct order
# Start on the photopshere, end at the pfss
fl_r, fl_lon, fl_lat = get_coord_values(fline)
# Fill in the lists
triplet = [fl_r, fl_lon, fl_lat]
coordlist.append(triplet)
flinelist.append(fline)
return coordlist, flinelist
def spiral_out(lon, lat, sign_switch, corner_tracker, turn):
"""
Moves the seeding point in an outward spiral.
Parameters
---------
lon, lat: float
the carrington coordinates on a surface of a sphere (sun or pfss)
sign_switch: int
-1 or 1, dictates the direction in which lon or lat is
incremented
corner_tracker: tuple
first entry is steps_unti_corner, int that tells how many steps to the next corner of a spiral
the second entry is steps taken on a given spiral turn
turn: str
indicates which is to be incremented, lon or lat
returns
-----------
lon, lat: float
new coordinate pair
"""
# In radians, 1 rad \approx 57.3 deg
step = 0.01
# Keeps track of how many steps until it's time to turn
steps_until_corner, steps_moved = corner_tracker[0], corner_tracker[1]
if turn=='lon':
lon = lon + step*sign_switch
lat = lat
steps_moved += 1
# We have arrived in a corner, time to move in lat direction
if steps_until_corner == steps_moved:
steps_moved = 0
turn = 'lat'
return lon, lat, sign_switch, [steps_until_corner, steps_moved], turn
if turn=='lat':
lon = lon
lat = lat + step*sign_switch
steps_moved += 1
# Hit a corner; start moving in the lon direction
if steps_until_corner == steps_moved:
steps_moved = 0
steps_until_corner += 1
turn = 'lon'
sign_switch = sign_switch*(-1)
return lon, lat, sign_switch, [steps_until_corner, steps_moved], turn
def au_to_km(list_of_distances):
"""
Takes a list of distances in AU and converts them to kilometers
"""
# Convert to numpy array for performance
list_of_distances = np.asarray(list_of_distances)
# Add units of astronomical units
list_of_distances = list_of_distances * u.au
# Convert to units of kilometers
list_of_distances = list_of_distances.to(u.km)
return list_of_distances.value
def multicolorline(x, y, cvals, ax, vmin=-90, vmax=90):
"""
Constructs a line object, with each segemnt of the line color coded
Original example from: https://matplotlib.org/stable/gallery/lines_bars_and_markers/multicolored_line.html
params
-------
x, y: float
cvals: str
ax: Figure.Axes object
vmin, vmax: int (default = -90, 90)
returns
-------
line: LineCollection object
"""
# Create a set of line segments so that we can color them individually
# This creates the points as a N x 1 x 2 array so that we can stack points
# together easily to get the segments. The segments array for line collection
# needs to be (numlines) x (points per line) x 2 (for x and y)
points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
# Create a continuous norm to map from data points to colors
norm = plt.Normalize(vmin, vmax)
cmrmap = cmr.redshift
# sample the colormaps that you want to use. Use 90 from each so there is one
# color for each degree
colors_pos = cmrmap(np.linspace(0.0, 0.30, 45))
colors_neg = cmrmap(np.linspace(0.70, 1.0, 45))
# combine them and build a new colormap
colors = np.vstack((colors_pos, colors_neg))
mymap = mcolors.LinearSegmentedColormap.from_list('my_colormap', colors)
# establish the linecollection object
lc = LineCollection(segments, cmap=mymap, norm=norm)
# set the values used for colormapping
lc.set_array(cvals)
# set the width of line
lc.set_linewidth(3)
# this we want to return
line = ax.add_collection(lc)
return line
def plot3d(field_lines, names, color_code='polarity'):
"""
Creates an interactive 3D plot that the user is free to rotate and zoom in a Jupyter notebook.
params
-------
field_lines : FieldLine object, or a list of them
names : str
names of the objects corresponding to the tracked field lines
color_code : str
either 'polarity' or 'object', dictates the color coding of the field lines
"""
if not isinstance(field_lines, list):
field_lines = [field_lines]
if isinstance(field_lines[0], list):
modulator = len(field_lines[1])//len(names) #modulates the order of field lines and the colors picked from c_list
field_lines = flatten_flines(field_lines, modulator)
if color_code=='object':
num_objects = len(names)
if len(field_lines) % num_objects != 0:
raise Exception("Names do not match field lines")
c_list = [get_color(x) for x in names]
elif color_code=='polarity':
colors = {0: 'black', -1: 'tab:blue', 1: 'tab:red'}
else:
raise Exception("Choose either 'polarity' or 'object' as the color coding.")
fig, axarr = plt.subplots(subplot_kw={"projection": "3d"})
axarr.set_box_aspect((1, 1, 1))
axarr.set_xlabel(r"x / $R_{\odot}$", labelpad=20)
axarr.set_ylabel(r"y / $R_{\odot}$", labelpad=20)
axarr.set_zlabel(r"z / $R_{\odot}$", labelpad=20)
# Draw the Sun
u, v = np.mgrid[0:2*np.pi:20j, 0:np.pi:10j]
x = np.cos(u)*np.sin(v)
y = np.sin(u)*np.sin(v)
z = np.cos(v)
axarr.plot_wireframe(x*1.0, y*1.0, z*1.0, color="darkorange")
axarr.set_xlim(-2, 2)
axarr.set_ylim(-2, 2)
axarr.set_zlim(-2, 2)
for i, field_line in enumerate(field_lines):
coords = field_line.coords
coords.representation = 'cartesian'
if color_code=='polarity':
color = colors.get(field_line.polarity)
if color_code=='object':
color = c_list[i//(modulator+1)]
axarr.plot(coords.cartesian.x / const.R_sun,
coords.cartesian.y / const.R_sun,
coords.cartesian.z / const.R_sun,
color=color, linewidth=1)
try:
axarr.set_aspect('equal', adjustable='box')
except NotImplementedError:
axarr.set_aspect('auto', adjustable='box')
def draw_fieldlines(field_lines, rss=2.5, frame='yz', color_code='polarity', names=[], save=False):
# check if there's a list inside a list, if there is -> flatten
# NOTICE: flatten() messes up the order of the field lines, putting the original flines
# first and then the varied field lines
if isinstance(field_lines[0], list):
modulator = len(field_lines[1])//len(names) # modulates the order of field lines and the colors picked from c_list
field_lines = flatten_flines(field_lines, modulator) # flatten_flines() conserves the correct ordering of field line objects
fig, ax = plt.subplots(figsize=[10, 10])
ax.set_aspect('equal')
ax.set_xlabel(frame[0]+r" / $R_{\odot}$")
ax.set_ylabel(frame[1]+r" / $R_{\odot}$")
# set color coding to field lines
if color_code=='object':
num_objects = len(names)
if len(field_lines) % num_objects != 0:
raise Exception("Names do not match field lines")
c_list = [get_color(x) for x in names]
elif color_code=='polarity':
colors = {0: 'black', -1: 'tab:blue', 1: 'tab:red'}
else:
raise Exception("Choose either 'polarity' or 'object' as the color coding.")
if(isinstance(field_lines, list)):
for i, field_line in enumerate(field_lines):
coords = field_line.coords
coords.representation = 'cartesian'
if color_code == 'polarity':
color = colors.get(field_line.polarity)
if color_code == 'object':
color = c_list[i//(modulator+1)]
if(frame=='yz'):
ax.plot(coords.cartesian.y / const.R_sun, coords.cartesian.z / const.R_sun, color=color)
projection = 'POV: Carrington longitude 0'
elif(frame=='xy'):
ax.plot(coords.cartesian.x / const.R_sun, coords.cartesian.y / const.R_sun, color=color)
projection = 'POV: North'
elif(frame=='xz'):
ax.plot(coords.cartesian.x / const.R_sun, coords.cartesian.z / const.R_sun, color=color)
projection = 'POV: Carrington longitude 270'
else:
raise Exception("Invalid frame")
else:
field_line = field_lines
coords = field_line.coords
coords.representation = 'cartesian'
color = {0: 'black', -1: 'tab:blue', 1: 'tab:red'}.get(field_line.polarity)
if(frame=='yz'):
ax.plot(coords.cartesian.y / const.R_sun, coords.cartesian.z / const.R_sun, color=color)
projection = 'POV: Carrington longitude 0'
elif(frame=='xy'):
ax.plot(coords.cartesian.x / const.R_sun, coords.cartesian.y / const.R_sun, color=color)
projection = 'POV: North'
elif(frame=='xz'):
ax.plot(coords.cartesian.x / const.R_sun, coords.cartesian.z / const.R_sun, color=color)
projection = 'POV: Carrington longitude 270'
else:
raise Exception("Invalid frame")
ax.add_patch(mpatch.Circle((0, 0), 1, color='darkorange', lw=2.0, fill=False))
ax.add_patch(mpatch.Circle((0, 0), rss, color='k', linestyle='--', fill=False))
plt.title(projection)
if save:
plt.savefig('overhead.png')
plt.show()
def flatten_flines(field_lines, modulator):
'''
Flattens a list of field_lines in such a way that the order of
corresponding field lines and their varied counterparts is preserved.
'''
flines0, flines1 = field_lines[0], field_lines[1]
flines_all = []
# append the original field line, and then all the varied field lines corresponding to
# that original field line in order
for i in range(len(flines0)):
flines_all.append(flines0[i])
for j in range(modulator):
index = i*modulator+j #index takes into account that there are a modulator amount of dummy flines
flines_all.append(flines1[index])
return flines_all