nf2 0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. nf2/__init__.py +0 -0
  2. nf2/convert/__init__.py +0 -0
  3. nf2/convert/convert_series.py +64 -0
  4. nf2/convert/muram_to_vtk.py +33 -0
  5. nf2/convert/nf2_to_fits.py +45 -0
  6. nf2/convert/nf2_to_hdf5.py +62 -0
  7. nf2/convert/nf2_to_npy.py +37 -0
  8. nf2/convert/nf2_to_vtk.py +62 -0
  9. nf2/convert/nf2_to_vtk_spherical.py +51 -0
  10. nf2/convert/potential_vtk.py +118 -0
  11. nf2/data/__init__.py +0 -0
  12. nf2/data/analytical_field.py +137 -0
  13. nf2/data/combine_sst_hmi.py +165 -0
  14. nf2/data/convert_coordinates.py +19 -0
  15. nf2/data/convert_muram_slices.py +43 -0
  16. nf2/data/dataset.py +231 -0
  17. nf2/data/download.py +56 -0
  18. nf2/data/download_euv.py +32 -0
  19. nf2/data/download_full_disk.py +54 -0
  20. nf2/data/download_range.py +44 -0
  21. nf2/data/download_sharp.py +26 -0
  22. nf2/data/download_synoptic.py +29 -0
  23. nf2/data/get_coordinate_bounds.py +31 -0
  24. nf2/data/loader.py +65 -0
  25. nf2/data/noaa_to_sharp.py +26 -0
  26. nf2/data/sst/__init__.py +0 -0
  27. nf2/data/sst/mask.py +32 -0
  28. nf2/data/sst/prep.py +160 -0
  29. nf2/data/util.py +105 -0
  30. nf2/evaluation/__init__.py +0 -0
  31. nf2/evaluation/analytical/__init__.py +0 -0
  32. nf2/evaluation/analytical/corrugation.py +72 -0
  33. nf2/evaluation/analytical/metrics.py +28 -0
  34. nf2/evaluation/compute_metrics.py +31 -0
  35. nf2/evaluation/current_series.py +40 -0
  36. nf2/evaluation/energy.py +11 -0
  37. nf2/evaluation/metric.py +144 -0
  38. nf2/evaluation/mfr/__init__.py +0 -0
  39. nf2/evaluation/muram/__init__.py +0 -0
  40. nf2/evaluation/muram/compare_boundary.py +64 -0
  41. nf2/evaluation/muram/compare_currents.py +216 -0
  42. nf2/evaluation/muram/corrugation.py +136 -0
  43. nf2/evaluation/muram/cube_height.py +47 -0
  44. nf2/evaluation/muram/height_ff.py +115 -0
  45. nf2/evaluation/muram/height_metrics.py +124 -0
  46. nf2/evaluation/muram/metrics.py +24 -0
  47. nf2/evaluation/muram/potential_metrics.py +26 -0
  48. nf2/evaluation/muram/slices.py +36 -0
  49. nf2/evaluation/output.py +423 -0
  50. nf2/evaluation/output_metrics.py +328 -0
  51. nf2/evaluation/parameters.py +58 -0
  52. nf2/evaluation/plot_carrington_map.py +48 -0
  53. nf2/evaluation/plot_euv_comparison.py +30 -0
  54. nf2/evaluation/plot_kso_comparison.py +42 -0
  55. nf2/evaluation/sharp/__init__.py +0 -0
  56. nf2/evaluation/sharp/convert_series.py +134 -0
  57. nf2/evaluation/sharp/ensemble.py +102 -0
  58. nf2/evaluation/sharp/flares.py +171 -0
  59. nf2/evaluation/sharp/plot_current_series.py +134 -0
  60. nf2/evaluation/sharp/plot_euv_lightcurve.py +68 -0
  61. nf2/evaluation/sharp/plot_integrated_energy.py +242 -0
  62. nf2/evaluation/sharp/plot_integrated_quantities.py +138 -0
  63. nf2/evaluation/sharp/plot_maps.py +120 -0
  64. nf2/evaluation/sharp/plot_quality.py +160 -0
  65. nf2/evaluation/sharp/plot_separatrix.py +107 -0
  66. nf2/evaluation/sharp/video.py +104 -0
  67. nf2/evaluation/solis/__init__.py +0 -0
  68. nf2/evaluation/solis/plot_twist_maps.py +52 -0
  69. nf2/evaluation/solis/vsm_corrugation.py +89 -0
  70. nf2/evaluation/spherical/__init__.py +0 -0
  71. nf2/evaluation/spherical/b_diff.py +86 -0
  72. nf2/evaluation/spherical/b_diff_synoptic.py +81 -0
  73. nf2/evaluation/spherical/compare.py +314 -0
  74. nf2/evaluation/spherical/current_change.py +120 -0
  75. nf2/evaluation/spherical/currents.py +286 -0
  76. nf2/evaluation/spherical/disk.py +159 -0
  77. nf2/evaluation/spherical/energy_diff.py +66 -0
  78. nf2/evaluation/spherical/energy_gradient.py +74 -0
  79. nf2/evaluation/spherical/free_energy.py +206 -0
  80. nf2/evaluation/spherical/input_plot.py +20 -0
  81. nf2/evaluation/spherical/open_fields.py +123 -0
  82. nf2/evaluation/spherical/traces.py +104 -0
  83. nf2/evaluation/sst/__init__.py +0 -0
  84. nf2/evaluation/sst/compare_muram_mfr.py +180 -0
  85. nf2/evaluation/sst/compare_muram_sma.py +178 -0
  86. nf2/evaluation/sst/compare_sharp_disambiguation.py +10 -0
  87. nf2/evaluation/sst/compare_sst.py +237 -0
  88. nf2/evaluation/sst/compare_sst_eruption.py +208 -0
  89. nf2/evaluation/sst/compare_sst_full.py +251 -0
  90. nf2/evaluation/sst/plot_data.py +99 -0
  91. nf2/evaluation/unpack.py +190 -0
  92. nf2/evaluation/vtk.py +51 -0
  93. nf2/extrapolate.py +127 -0
  94. nf2/extrapolate_series.py +173 -0
  95. nf2/loader/__init__.py +0 -0
  96. nf2/loader/analytical.py +151 -0
  97. nf2/loader/base.py +337 -0
  98. nf2/loader/fits.py +310 -0
  99. nf2/loader/general.py +22 -0
  100. nf2/loader/muram.py +214 -0
  101. nf2/loader/spherical.py +396 -0
  102. nf2/loader/util.py +133 -0
  103. nf2/loader/vsm.py +17 -0
  104. nf2/potential/__init__.py +0 -0
  105. nf2/potential/potential_field.py +132 -0
  106. nf2/train/__init__.py +0 -0
  107. nf2/train/callback.py +695 -0
  108. nf2/train/config_from_template.py +28 -0
  109. nf2/train/extrapolate_analytic.py +91 -0
  110. nf2/train/loss.py +403 -0
  111. nf2/train/mapping.py +40 -0
  112. nf2/train/model.py +440 -0
  113. nf2/train/module.py +338 -0
  114. nf2/train/transform.py +44 -0
  115. nf2/train/util.py +14 -0
  116. nf2-0.3.dist-info/LICENSE +674 -0
  117. nf2-0.3.dist-info/METADATA +23 -0
  118. nf2-0.3.dist-info/RECORD +121 -0
  119. nf2-0.3.dist-info/WHEEL +5 -0
  120. nf2-0.3.dist-info/entry_points.txt +10 -0
  121. nf2-0.3.dist-info/top_level.txt +1 -0
nf2/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,64 @@
1
+ import argparse
2
+ import glob
3
+ import os.path
4
+
5
+ from tqdm import tqdm
6
+
7
+
8
+ def main():
9
+ parser = argparse.ArgumentParser(description='Convert NF2 file to VTK.')
10
+ parser.add_argument('--nf2_dir', type=str, help='path to the source NF2 files', nargs='+', required=True)
11
+ parser.add_argument('--out_dir', type=str, help='path to the target VTK directory', required=False, default=None)
12
+ parser.add_argument('--Mm_per_pixel', type=float, help='spatial resolution (0.36 for original HMI)', required=False,
13
+ default=None)
14
+ parser.add_argument('--height_range', type=float, nargs=2, help='height range in Mm', required=False, default=None)
15
+ parser.add_argument('--metrics', type=str, nargs='*', help='metrics to be computed', required=False, default=['j'])
16
+ parser.add_argument('--type', type=str, help='type of the conversion (vtk, hdf5, npy, fits)', required=False,
17
+ default='vtk')
18
+ parser.add_argument('--overwrite', action='store_true', help='overwrite existing files', required=False,
19
+ default=False)
20
+
21
+ args = parser.parse_args()
22
+
23
+ nf2_dir = args.nf2_dir if isinstance(args.nf2_dir, list) else [args.nf2_dir]
24
+ nf2_paths = [sorted(glob.glob(f)) for f in nf2_dir]
25
+ nf2_paths = [f for files in nf2_paths for f in files] # flatten list
26
+
27
+ Mm_per_pixel = args.Mm_per_pixel
28
+ out_dir = args.out_dir if args.out_dir is not None else os.path.dirname(nf2_dir[0])
29
+ os.makedirs(out_dir, exist_ok=True)
30
+ height_range = args.height_range
31
+ metrics = args.metrics
32
+
33
+ conversion_type = args.type
34
+ if conversion_type == 'vtk':
35
+ d_type = '.vtk'
36
+ from nf2.convert import nf2_to_vtk
37
+ convert_f = nf2_to_vtk.convert
38
+ elif conversion_type == 'hdf5':
39
+ d_type = '.hdf5'
40
+ from nf2.convert import nf2_to_hdf5
41
+ convert_f = nf2_to_hdf5.convert
42
+ elif conversion_type == 'npy':
43
+ d_type = '.npy'
44
+ from nf2.convert import nf2_to_npy
45
+ convert_f = nf2_to_npy.convert
46
+ elif conversion_type == 'fits':
47
+ d_type = '.fits'
48
+ from nf2.convert import nf2_to_fits
49
+ convert_f = nf2_to_fits.convert
50
+ else:
51
+ raise ValueError(f'Unknown conversion type: {conversion_type}')
52
+
53
+ for nf2_path in tqdm(nf2_paths, desc='Converting', unit='file'):
54
+ out_file = os.path.join(out_dir, os.path.basename(nf2_path).replace('.nf2', d_type))
55
+ if os.path.exists(out_file) and not args.overwrite:
56
+ print(f'File exists: {out_file}')
57
+ continue
58
+ convert_f(nf2_path=nf2_path, out_path=out_file,
59
+ Mm_per_pixel=Mm_per_pixel, height_range=height_range, metrics=metrics,
60
+ progress=False)
61
+
62
+
63
+ if __name__ == '__main__':
64
+ main()
@@ -0,0 +1,33 @@
1
+ import argparse
2
+
3
+ import numpy as np
4
+ from astropy import constants as const
5
+ from astropy import units as u
6
+
7
+ from nf2.evaluation.metric import curl
8
+ from nf2.evaluation.vtk import save_vtk
9
+ from nf2.loader.muram import MURaMSnapshot
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description='Convert NF2 file to VTK.')
14
+ parser.add_argument('--source_path', type=str, help='path to the MURaM simulation.')
15
+ parser.add_argument('--iteration', type=int, help='iteration of the snapshot.')
16
+ parser.add_argument('--vtk_path', type=str, help='path to the target VTK file', required=False, default=None)
17
+ parser.add_argument('--Mm_per_pixel', type=float, help='spatial resolution (needs to be a multiple of 0.192)',
18
+ required=False, default=0.192 * 4)
19
+ parser.add_argument('--height', type=float, help='height of the snapshot', required=False, default=None)
20
+ args = parser.parse_args()
21
+
22
+ snapshot = MURaMSnapshot(args.source_path, args.iteration)
23
+ muram_cube = snapshot.load_cube(args.Mm_per_pixel * u.Mm / u.pix, target_tau=1.0, height=args.height * u.Mm)
24
+ b = muram_cube['B']
25
+ j = curl(b) * u.G / (args.Mm_per_pixel * u.Mm) * const.c / (4 * np.pi) # Mm_per_pixel
26
+ j = j.to(u.G / u.s)
27
+
28
+ save_vtk(args.vtk_path, vectors={'b': b, 'j': j.value},
29
+ scalars={'tau': muram_cube['tau']}, Mm_per_pix=args.Mm_per_pixel)
30
+
31
+
32
+ if __name__ == '__main__':
33
+ main()
@@ -0,0 +1,45 @@
1
+ import argparse
2
+ import os
3
+
4
+ from astropy.io import fits
5
+
6
+ from nf2.evaluation.output import CartesianOutput
7
+
8
+
9
+ def convert(nf2_path, out_path=None, Mm_per_pixel=None, height_range=None, **kwargs):
10
+ out_path = out_path if out_path is not None \
11
+ else os.path.join(os.path.dirname(nf2_path), nf2_path.split(os.sep)[-2] + '.hdf5')
12
+
13
+ nf2_out = CartesianOutput(nf2_path)
14
+ output = nf2_out.load_cube(Mm_per_pixel=Mm_per_pixel, height_range=height_range, **kwargs)
15
+
16
+ b = output['b']
17
+ j = output['j']
18
+
19
+ header = {'Mm_per_pix': output['Mm_per_pixel'],
20
+ 'data': nf2_out.data_config,
21
+ 'wcs': nf2_out.wcs,
22
+ 'DATE_OBS': nf2_out.time}
23
+
24
+ b_hdu = fits.PrimaryHDU(b, header=header)
25
+ j_hdu = fits.FitsHDU(j)
26
+ hdul = fits.HDUList([b_hdu, j_hdu])
27
+ hdul.writeto(out_path)
28
+
29
+
30
+ def main():
31
+ parser = argparse.ArgumentParser(description='Convert NF2 file to fits.')
32
+ parser.add_argument('nf2_path', type=str, help='path to the source NF2 file')
33
+ parser.add_argument('--out_path', type=str, help='path to the target numpy file', required=False, default=None)
34
+ parser.add_argument('--strides', type=int, help='downsampling of the volume', required=False, default=1)
35
+
36
+ args = parser.parse_args()
37
+ nf2_path = args.nf2_path
38
+ strides = args.strides
39
+ out_path = args.out_path
40
+
41
+ convert(nf2_path, out_path, strides)
42
+
43
+
44
+ if __name__ == '__main__':
45
+ main()
@@ -0,0 +1,62 @@
1
+ import argparse
2
+ import os.path
3
+ from threading import Thread
4
+
5
+ import h5py
6
+
7
+ from nf2.evaluation.output import CartesianOutput
8
+
9
+
10
+ class _SaveFileTask(Thread):
11
+
12
+ def __init__(self, out_path, output, nf2_out):
13
+ super().__init__()
14
+ self.out_path = out_path
15
+ self.output = output
16
+ self.nf2_out = nf2_out
17
+
18
+ def run(self):
19
+ f = h5py.File(self.out_path, 'w')
20
+ f.create_dataset('B', data=self.output['b'], dtype='f4', compression='gzip')
21
+ f.attrs['INFO'] = f'Magnetic vector field (B_x, B_y, B_z): {self.output["b"].shape} (x, y, z, 3)'
22
+ f.attrs['Mm_per_pixel'] = self.output['Mm_per_pixel']
23
+ f.attrs['type'] = self.nf2_out.data_config['type']
24
+ f.attrs['wcs'] = self.nf2_out.wcs[0].to_header_string()
25
+ f.attrs['time'] = self.nf2_out.time.isoformat('T', timespec='seconds')
26
+ f.close()
27
+ print('File saved:', self.out_path)
28
+
29
+
30
+ def convert(nf2_path, out_path=None, Mm_per_pixel=None, height_range=None, **kwargs):
31
+ out_path = out_path if out_path is not None \
32
+ else os.path.join(os.path.dirname(nf2_path), nf2_path.split(os.sep)[-2] + '.hdf5')
33
+
34
+ nf2_out = CartesianOutput(nf2_path)
35
+ output = nf2_out.load_cube(Mm_per_pixel=Mm_per_pixel, height_range=height_range, **kwargs)
36
+
37
+ # save file in background
38
+ task = _SaveFileTask(out_path, output, nf2_out)
39
+ task.start()
40
+ return task
41
+
42
+
43
+ def main():
44
+ parser = argparse.ArgumentParser(description='Convert NF2 file to VTK.')
45
+ parser.add_argument('--nf2_path', type=str, help='path to the source NF2 file')
46
+ parser.add_argument('--out_path', type=str, help='path to the target HDF5 file', required=False, default=None)
47
+ parser.add_argument('--Mm_per_pixel', type=float, help='spatial resolution (0.36 for original HMI)', required=False,
48
+ default=None)
49
+ parser.add_argument('--height_range', type=float, nargs=2, help='height range in Mm', required=False, default=None)
50
+
51
+ args = parser.parse_args()
52
+ nf2_path = args.nf2_path
53
+
54
+ Mm_per_pixel = args.Mm_per_pixel
55
+ out_path = args.out_path
56
+ height_range = args.height_range
57
+
58
+ convert(nf2_path, out_path, Mm_per_pixel, height_range)
59
+
60
+
61
+ if __name__ == '__main__':
62
+ main()
@@ -0,0 +1,37 @@
1
+ import argparse
2
+ import os.path
3
+
4
+ import numpy as np
5
+
6
+ from nf2.evaluation.output import CartesianOutput
7
+
8
+
9
+ def convert(nf2_path, out_path=None, Mm_per_pixel=None, height_range=None, **kwargs):
10
+ out_path = out_path if out_path is not None \
11
+ else os.path.join(os.path.dirname(nf2_path), nf2_path.split(os.sep)[-2] + '.npy')
12
+
13
+ nf2_out = CartesianOutput(nf2_path)
14
+ output = nf2_out.load_cube(Mm_per_pixel=Mm_per_pixel, height_range=height_range, **kwargs)
15
+ np.save(out_path, output['b'])
16
+
17
+
18
+ def main():
19
+ parser = argparse.ArgumentParser(description='Convert NF2 file to VTK.')
20
+ parser.add_argument('--nf2_path', type=str, help='path to the source NF2 file')
21
+ parser.add_argument('--out_path', type=str, help='path to the target NPY file', required=False, default=None)
22
+ parser.add_argument('--Mm_per_pixel', type=float, help='spatial resolution (0.36 for original HMI)', required=False,
23
+ default=None)
24
+ parser.add_argument('--height_range', type=float, nargs=2, help='height range in Mm', required=False, default=None)
25
+
26
+ args = parser.parse_args()
27
+ nf2_path = args.nf2_path
28
+
29
+ Mm_per_pixel = args.Mm_per_pixel
30
+ out_path = args.out_path
31
+ height_range = args.height_range
32
+
33
+ convert(nf2_path, out_path, Mm_per_pixel, height_range)
34
+
35
+
36
+ if __name__ == '__main__':
37
+ main()
@@ -0,0 +1,62 @@
1
+ import argparse
2
+ import os.path
3
+ from threading import Thread
4
+
5
+ from nf2.evaluation.output import CartesianOutput
6
+ from nf2.evaluation.vtk import save_vtk
7
+
8
+
9
+ class _SaveFileTask(Thread):
10
+
11
+ def __init__(self, out_path, output, metrics):
12
+ super().__init__()
13
+ self.out_path = out_path
14
+ self.output = output
15
+ self.metrics = metrics if metrics is not None else []
16
+
17
+ def run(self):
18
+ Mm_per_pixel = self.output['Mm_per_pixel']
19
+
20
+ # split output into vectors and scalars
21
+ vectors = {k: v for k, v in self.output['metrics'].items() if len(v.shape) == 4 and v.shape[-1] == 3}
22
+ scalars = {k: v for k, v in self.output['metrics'].items() if len(v.shape) == 3}
23
+
24
+ vectors['b'] = self.output['b']
25
+
26
+ save_vtk(self.out_path, vectors=vectors, scalars=scalars, Mm_per_pix=Mm_per_pixel)
27
+
28
+
29
+ def convert(nf2_path, out_path=None, Mm_per_pixel=None, height_range=None, metrics=None, **kwargs):
30
+ out_path = out_path if out_path is not None \
31
+ else os.path.join(os.path.dirname(nf2_path), nf2_path.split(os.sep)[-2] + '.vtk')
32
+
33
+ nf2_out = CartesianOutput(nf2_path)
34
+ output = nf2_out.load_cube(Mm_per_pixel=Mm_per_pixel, height_range=height_range, metrics=metrics, **kwargs)
35
+
36
+ # save file in background
37
+ task = _SaveFileTask(out_path, output, metrics)
38
+ task.start()
39
+
40
+
41
+ def main():
42
+ parser = argparse.ArgumentParser(description='Convert NF2 file to VTK.')
43
+ parser.add_argument('--nf2_path', type=str, help='path to the source NF2 file')
44
+ parser.add_argument('--out_path', type=str, help='path to the target VTK file', required=False, default=None)
45
+ parser.add_argument('--Mm_per_pixel', type=float, help='spatial resolution (0.36 for original HMI)', required=False,
46
+ default=None)
47
+ parser.add_argument('--height_range', type=float, nargs=2, help='height range in Mm', required=False, default=None)
48
+ parser.add_argument('--metrics', type=str, nargs='*', help='metrics to be computed', required=False, default=['j'])
49
+
50
+ args = parser.parse_args()
51
+ nf2_path = args.nf2_path
52
+
53
+ Mm_per_pixel = args.Mm_per_pixel
54
+ out_path = args.out_path
55
+ height_range = args.height_range
56
+ metrics = args.metrics
57
+
58
+ convert(nf2_path, out_path, Mm_per_pixel, height_range, metrics=metrics, progress=True)
59
+
60
+
61
+ if __name__ == '__main__':
62
+ main()
@@ -0,0 +1,51 @@
1
+ import argparse
2
+ import os
3
+
4
+ import numpy as np
5
+ from astropy import units as u
6
+
7
+ from nf2.evaluation.output import SphericalOutput
8
+ from nf2.evaluation.output_metrics import current_density, alpha
9
+ from nf2.evaluation.vtk import save_vtk
10
+
11
+ if __name__ == '__main__':
12
+ parser = argparse.ArgumentParser(description='Convert NF2 file to VTK.')
13
+ parser.add_argument('--nf2_path', type=str, help='path to the source NF2 file')
14
+ parser.add_argument('--out_path', type=str, help='path to the target VTK files', required=False, default=None)
15
+ parser.add_argument('--overwrite', action='store_true', help='overwrite existing files')
16
+
17
+ parser.add_argument('--radius_range', nargs='+', type=float, default=(0.999, 1.3), required=False)
18
+ parser.add_argument('--latitude_range', nargs='+', type=float, default=(0, 180), required=False)
19
+ parser.add_argument('--longitude_range', nargs='+', type=float, default=(0, 360), required=False)
20
+ parser.add_argument('--radians', action='store_true', help='latitude and longitude in radians', required=False,
21
+ default=False)
22
+ parser.add_argument('--pixels_per_solRad', type=int, default=64, required=False)
23
+
24
+ args = parser.parse_args()
25
+ nf2_path = args.nf2_path
26
+ out_path = nf2_path.replace('.nf2', '.vtk') if args.out_path is None else args.out_path
27
+
28
+ radius_range = tuple(args.radius_range) * u.solRad
29
+ if args.radians:
30
+ latitude_range = tuple(args.latitude_range) * u.rad
31
+ longitude_range = tuple(args.longitude_range) * u.rad
32
+ else:
33
+ latitude_range = tuple(args.latitude_range) * u.deg
34
+ longitude_range = tuple(args.longitude_range) * u.deg
35
+ pixels_per_solRad = args.pixels_per_solRad * u.pix / u.solRad
36
+
37
+ assert len(radius_range) == 2, 'radius_range must be a tuple of length 2'
38
+ assert len(latitude_range) == 2, 'latitude_range must be a tuple of length 2'
39
+ assert len(longitude_range) == 2, 'longitude_range must be a tuple of length 2'
40
+
41
+ output = SphericalOutput(nf2_path)
42
+ result = output.load(radius_range, latitude_range, longitude_range, pixels_per_solRad, progress=True,
43
+ metrics={'j': current_density, 'alpha': alpha})
44
+
45
+ vectors = {'B': result['b'], 'B_rtp': result['b_rtp']}
46
+ radius = result['spherical_coords'][..., 0]
47
+ metrics = result['metrics']
48
+ scalars = {'radius': radius, 'current_density': np.sum(metrics['j'] ** 2, -1) ** 0.5, 'alpha': metrics['alpha']}
49
+ coords = result['coords']
50
+
51
+ save_vtk(out_path, coords, vectors, scalars)
@@ -0,0 +1,118 @@
1
+ import argparse
2
+ import os
3
+
4
+ import numpy as np
5
+ import pfsspy
6
+ import torch
7
+ from astropy import units as u
8
+ from astropy.coordinates import SkyCoord
9
+ from matplotlib import pyplot as plt
10
+ from mpl_toolkits.axes_grid1 import make_axes_locatable
11
+ from sunpy.map import Map
12
+
13
+ from nf2.data.util import cartesian_to_spherical, spherical_to_cartesian, vector_spherical_to_cartesian
14
+ from nf2.evaluation.vtk import save_vtk
15
+
16
+ parser = argparse.ArgumentParser(description='Convert NF2 file to VTK.')
17
+ parser.add_argument('--Br', type=str, help='path to the source radial magnetic field file (full disk map)')
18
+ parser.add_argument('--synoptic', type=str, help='path to the source radial magnetic field file (synoptic map)')
19
+ parser.add_argument('--out_path', type=str, help='path to the target VTK files')
20
+
21
+ parser.add_argument('--radius_range', nargs='+', type=float, default=(0.999, 1.5), required=False)
22
+ parser.add_argument('--latitude_range', nargs='+', type=float, default=(0 * np.pi, 1 * np.pi), required=False)
23
+ parser.add_argument('--longitude_range', nargs='+', type=float, default=(0 * np.pi, 2 * np.pi), required=False)
24
+ parser.add_argument('--pixels_per_solRad', type=int, default=64, required=False)
25
+
26
+ args = parser.parse_args()
27
+ out_path = args.out_path
28
+
29
+ os.makedirs(out_path, exist_ok=True)
30
+
31
+ radius_range = tuple(args.radius_range)
32
+ latitude_range = tuple(args.latitude_range)
33
+ longitude_range = tuple(args.longitude_range)
34
+ pixels_per_solRad = args.pixels_per_solRad
35
+
36
+ assert len(radius_range) == 2, 'radius_range must be a tuple of length 2'
37
+ assert len(latitude_range) == 2, 'latitude_range must be a tuple of length 2'
38
+ assert len(longitude_range) == 2, 'longitude_range must be a tuple of length 2'
39
+
40
+ device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
41
+
42
+ synoptic_map_br = Map(args.synoptic)
43
+ synoptic_map_br = synoptic_map_br.resample([360 * 2, 180 * 2] * u.pix)
44
+
45
+ mag_r_map = Map(args.Br)
46
+ mag_r_map = mag_r_map.reproject_to(synoptic_map_br.wcs)
47
+
48
+ nan_mask = ~np.isnan(mag_r_map.data)
49
+ synoptic_map_br.data[nan_mask] = mag_r_map.data[nan_mask]
50
+
51
+ nrho = 100
52
+ rss = 2.5
53
+ pfss_in = pfsspy.Input(synoptic_map_br, nrho, rss)
54
+ pfss_out = pfsspy.pfss(pfss_in)
55
+
56
+ fig, axs = plt.subplots(10, 3, figsize=(10, 10))
57
+ b_potential = pfss_out.bg
58
+ for i in range(10):
59
+ v_min_max = np.abs(b_potential[:, :, i]).max()
60
+ v_min_max = 500 if v_min_max > 500 else v_min_max
61
+ axs[i, 0].imshow(b_potential[:, :, i, 0].T, vmin=-v_min_max, vmax=v_min_max, cmap='gray', origin='lower')
62
+ axs[i, 1].imshow(b_potential[:, :, i, 1].T, vmin=-v_min_max, vmax=v_min_max, cmap='gray', origin='lower')
63
+ im = axs[i, 2].imshow(b_potential[:, :, i, 2].T, vmin=-v_min_max, vmax=v_min_max, cmap='gray', origin='lower')
64
+ divider = make_axes_locatable(axs[i, 2])
65
+ cax = divider.append_axes("right", size="5%", pad=0.05)
66
+ plt.colorbar(im, cax=cax)
67
+
68
+ [ax.axis('off') for ax in axs.ravel()]
69
+ plt.tight_layout()
70
+ plt.savefig(os.path.join(out_path, 'potential.jpg'), dpi=300)
71
+ plt.close()
72
+
73
+ vtk_path = os.path.join(out_path, 'potential.vtk')
74
+
75
+ spherical_bounds = np.stack(
76
+ np.meshgrid(np.linspace(radius_range[0], radius_range[1], 50),
77
+ np.linspace(latitude_range[0], latitude_range[1], 50),
78
+ np.linspace(longitude_range[0], longitude_range[1], 50), indexing='ij'), -1)
79
+
80
+ cartesian_bounds = spherical_to_cartesian(spherical_bounds)
81
+
82
+ x_min, x_max = cartesian_bounds[..., 0].min(), cartesian_bounds[..., 0].max()
83
+ y_min, y_max = cartesian_bounds[..., 1].min(), cartesian_bounds[..., 1].max()
84
+ z_min, z_max = cartesian_bounds[..., 2].min(), cartesian_bounds[..., 2].max()
85
+
86
+ x_min, x_max = -1.3, 1.3
87
+ y_min, y_max = -1.3, 1.3
88
+ z_min, z_max = -1.3, 1.3
89
+
90
+ coords = np.stack(
91
+ np.meshgrid(np.linspace(x_min, x_max, int((x_max - x_min) * pixels_per_solRad)),
92
+ np.linspace(y_min, y_max, int((y_max - y_min) * pixels_per_solRad)),
93
+ np.linspace(z_max, z_min, int((z_max - z_min) * pixels_per_solRad)), indexing='ij'), -1)
94
+ # flipped z axis
95
+ radius = np.sqrt(np.sum(coords ** 2, -1))
96
+
97
+ spherical_coords = cartesian_to_spherical(coords)
98
+ condition = (spherical_coords[..., 0] >= radius_range[0]) & (spherical_coords[..., 0] < radius_range[1]) \
99
+ & (spherical_coords[..., 1] > latitude_range[0]) & (spherical_coords[..., 1] < latitude_range[1]) \
100
+ # & (spherical_coords[..., 2] > longitude_range[0]) & (spherical_coords[..., 2] < longitude_range[1])
101
+ spherical_coords[..., 1] -= np.pi / 2
102
+ sub_coords = spherical_coords[condition]
103
+
104
+ cube_shape = coords.shape[:-1]
105
+ sky_sub_coords = SkyCoord(lon=sub_coords[..., 2] * u.rad,
106
+ lat=(sub_coords[..., 1]) * u.rad,
107
+ radius=sub_coords[..., 0] * u.solRad, frame=mag_r_map.coordinate_frame)
108
+ sub_b = pfss_out.get_bvec(sky_sub_coords, out_type='cartesian')
109
+ # sub_b[..., 2] *= -1 # flip z axis
110
+
111
+ b = np.zeros(cube_shape + (3,))
112
+ b[condition] = sub_b
113
+ b = np.nan_to_num(b, nan=0)
114
+
115
+ coords = np.stack(np.mgrid[0:b.shape[0], 0:b.shape[1], 0:b.shape[2]], -1).astype(np.int64)
116
+ coords = (coords/ (b.shape[0] - 1) - 0.5) * 2 * 1.3
117
+
118
+ save_vtk(vtk_path, coords=coords, vectors={'B': b})
nf2/data/__init__.py ADDED
File without changes
@@ -0,0 +1,137 @@
1
+ import numpy as np
2
+ from scipy.integrate import solve_bvp
3
+
4
+
5
+ def _differential_equation(mu, u, n, a2):
6
+ """
7
+ The differential equation to solve for P
8
+
9
+ :param mu: cos(theta)
10
+ :param u: P function and derivative
11
+ :param n: variable according to Low & Lou (1989)
12
+ :param a2: eigenvalue
13
+
14
+ """
15
+ P, dP = u
16
+ dP_dmu = dP
17
+ d2P_dmu2 = -(n * (n + 1) * P + a2 * (1 + n) / n * P ** (1 + 2 / n)) / (1 - mu ** 2 + 1e-8)
18
+ return (dP_dmu, d2P_dmu2)
19
+
20
+
21
+ def get_analytic_b_field(n=1, m=1, l=0.3, psi=np.pi / 4, resolution=64, bounds=[-1, 1, -1, 1, 0, 2], tau_surfaces=None):
22
+ """
23
+ Calculate the analytic NLFF field from Low & Lou (1989).
24
+
25
+ :param n: variable see Low & Lou (1989), only works for n=1
26
+ :param m: used for generating a proper initial condition.
27
+ :param a2: eigenvalue
28
+ :param l: depth below the photosphere
29
+ :param psi: angle of the magnetic field relative to the dipol axis
30
+ :param resolution: spatial resolution of the magnetic field in pixels
31
+ :param bounds: dimensions of the volume (x_start, x_end, y_start, y_end, z_start, z_end)
32
+ :return: magnetic field B (x, y, z, v)
33
+ """
34
+ sol_P, a2 = solve_P(n, m)
35
+
36
+ resolution = [resolution] * 3 if not isinstance(resolution, list) else resolution
37
+ if tau_surfaces:
38
+ coords = np.stack(np.meshgrid(np.linspace(bounds[0], bounds[1], resolution[0], dtype=np.float32),
39
+ np.linspace(bounds[2], bounds[3], resolution[1], dtype=np.float32),
40
+ np.ones(len(tau_surfaces), dtype=np.float32),
41
+ indexing='ij'), -1)
42
+ for i, c in enumerate(tau_surfaces):
43
+ coords[:, :, i, 2] = c / resolution[2] * (bounds[5] - bounds[4]) + bounds[4]
44
+ if c == 0: # no corrugation for photospheric layer
45
+ continue
46
+ sx = sy = c / resolution[2] + 0.5
47
+ x, y = coords[:, :, i, 0], coords[:, :, i, 1]
48
+ gaussian = np.exp(-(x**2. / (2. * sx**2.) + y **2. / (2. * sy**2.)))
49
+ gaussian /= gaussian.max() # normalize
50
+ coords[:, :, i, 2] *= gaussian
51
+ else:
52
+ coords = np.stack(np.meshgrid(np.linspace(bounds[0], bounds[1], resolution[0], dtype=np.float32),
53
+ np.linspace(bounds[2], bounds[3], resolution[1], dtype=np.float32),
54
+ np.linspace(bounds[4], bounds[5], resolution[2], dtype=np.float32),
55
+ indexing='ij'), -1)
56
+
57
+ x, y, z = coords[..., 0], coords[..., 1], coords[..., 2]
58
+ X = x * np.cos(psi) - (z + l) * np.sin(psi)
59
+ Y = y
60
+ Z = x * np.sin(psi) + (z + l) * np.cos(psi)
61
+
62
+ # to spherical coordinates
63
+ xy = X ** 2 + Y ** 2
64
+ r = np.sqrt(xy + Z ** 2)
65
+ theta = np.arctan2(np.sqrt(xy), Z)
66
+ phi = np.arctan2(Y, X)
67
+
68
+ mu = np.cos(theta)
69
+
70
+ P, dP_dmu = sol_P(mu)
71
+ A = P / r ** n
72
+ dA_dtheta = -np.sin(theta) / (r ** n) * dP_dmu
73
+ dA_dr = P * (-n * r ** (-n - 1))
74
+ Q = np.sqrt(a2) * A * np.abs(A) ** (1 / n)
75
+
76
+ Br = (r ** 2 * np.sin(theta)) ** -1 * dA_dtheta
77
+ Btheta = - (r * np.sin(theta)) ** -1 * dA_dr
78
+ Bphi = (r * np.sin(theta)) ** -1 * Q
79
+
80
+ BX = Br * np.sin(theta) * np.cos(phi) + Btheta * np.cos(theta) * np.cos(phi) - Bphi * np.sin(phi)
81
+ BY = Br * np.sin(theta) * np.sin(phi) + Btheta * np.cos(theta) * np.sin(phi) + Bphi * np.cos(phi)
82
+ BZ = Br * np.cos(theta) - Btheta * np.sin(theta)
83
+
84
+ Bx = BX * np.cos(psi) + BZ * np.sin(psi)
85
+ By = BY
86
+ Bz = - BX * np.sin(psi) + BZ * np.cos(psi)
87
+
88
+ b_field = np.real(np.stack([Bx, By, Bz], -1))
89
+ return b_field
90
+
91
+
92
+ def solve_P(n, m):
93
+ """
94
+ Solve the differential equation from Low & Lou (1989).
95
+
96
+ :param n: variable (only n=1)
97
+ :param v0: start condition for dP/dmu
98
+ :param P0: boundary condition for P(-1) and P(1)
99
+ :return: interpolated functions for P and dP/dmu
100
+ """
101
+
102
+ def f(x, y, p):
103
+ a2 = p[0]
104
+ d2P_dmu2 = -(n * (n + 1) * y[0] + a2 * (1 + n) / n * y[0] ** (1 + 2 / n)) / (1 - x ** 2 + 1e-6)
105
+ return [y[1], d2P_dmu2]
106
+
107
+ def f_boundary(Pa, Pb, p):
108
+ return np.array([Pa[0] - 0, Pb[0] - 0, Pa[1] - 10])
109
+
110
+ mu = np.linspace(-1, 1, num=256)
111
+
112
+ if m % 2 == 0:
113
+ init = np.cos(mu * (m + 1) * np.pi / 2)
114
+ else:
115
+ init = np.sin(mu * (m + 1) * np.pi / 2)
116
+
117
+ dinit = 10 * np.ones_like(init) #
118
+ initial = np.stack([init, dinit])
119
+
120
+ @np.vectorize
121
+ def shooting(a2_init):
122
+ eval = solve_bvp(f, f_boundary, x=mu, y=initial, p=[a2_init], verbose=0, tol=1e-6)
123
+ if eval.success == False:
124
+ return None
125
+ return eval
126
+
127
+ # use shooting to find eigenvalues
128
+ evals = shooting(np.linspace(0, 10, 100, dtype=np.float32))
129
+ evals = [e for e in evals if e is not None]
130
+
131
+ eigenvalues = np.array([e.p for e in evals])
132
+ eigenvalues = sorted(set(np.round(eigenvalues, 4).reshape((-1,))))
133
+
134
+ # get final solution
135
+ eval = shooting([eigenvalues[-1]])[0]
136
+
137
+ return eval.sol, eval.p[0]