hyper-py-photometry 0.1.0__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.
hyper_py/survey.py ADDED
@@ -0,0 +1,70 @@
1
+ from astropy.io import fits
2
+ import numpy as np
3
+ from astropy import units as u
4
+
5
+ # Static beam info per survey_code (with conversion micron -> GHz)
6
+
7
+ BEAM_INFO = {
8
+ 1: {'band': (70*u.micron).to(u.GHz, equivalencies=u.spectral()), 'beam': 5.2, 'area': None},
9
+ 2: {'band': (100*u.micron).to(u.GHz, equivalencies=u.spectral()), 'beam': 6.8, 'area': None},
10
+ 3: {'band': (160*u.micron).to(u.GHz, equivalencies=u.spectral()), 'beam': 12.0, 'area': None},
11
+ 4: {'band': (250*u.micron).to(u.GHz, equivalencies=u.spectral()), 'beam': 18.0, 'area': 423},
12
+ 5: {'band': (350*u.micron).to(u.GHz, equivalencies=u.spectral()), 'beam': 24.0, 'area': 751},
13
+ 6: {'band': (500*u.micron).to(u.GHz, equivalencies=u.spectral()), 'beam': 34.5, 'area': 1587},
14
+ 7: {'band': (450*u.micron).to(u.GHz, equivalencies=u.spectral()), 'beam': 8.0, 'area': None},
15
+ 8: {'band': (850*u.micron).to(u.GHz, equivalencies=u.spectral()), 'beam': 14.5, 'area': None},
16
+ 15: 'DYNAMIC',
17
+ }
18
+
19
+
20
+ arcsec2_to_sr = 2.35045e-11
21
+
22
+ def get_beam_info(survey_code, fits_file=None):
23
+ beam_arcsec = []
24
+ beam_area_arcsec2 = []
25
+ beam_area_sr = []
26
+ band_ref = []
27
+
28
+ entry = BEAM_INFO.get(survey_code)
29
+
30
+ if entry == 'DYNAMIC':
31
+ if not fits_file:
32
+ raise ValueError("FITS file required for ALMA Band 6 beam estimation.")
33
+ with fits.open(fits_file) as hdul:
34
+ hdr = hdul[0].header
35
+ bmin = hdr.get('BMIN', 0) * 3600 # degrees → arcsec
36
+ bmaj = hdr.get('BMAJ', 0) * 3600
37
+ beam = np.sqrt(bmin * bmaj)
38
+
39
+ beam_area_arcsec2 = 1.1331 * bmin * bmaj
40
+ beam_area_sr = beam_area_arcsec2 * arcsec2_to_sr
41
+
42
+
43
+ # area = 1.133 * bmin * bmaj *(3600 * np.pi / 180) ** 2 / SR_TO_ARCSEC2
44
+ elif isinstance(entry, dict):
45
+ with fits.open(fits_file) as hdul: hdr = hdul[0].header
46
+ beam = np.mean(entry['beam']) if isinstance(entry['beam'], list) else entry['beam']
47
+ beam_area_arcsec2 = entry['area']
48
+ if beam_area_arcsec2 is None:
49
+ beam_area_arcsec2 = 1.1331 * beam ** 2
50
+ beam_area_sr = beam_area_arcsec2 * arcsec2_to_sr
51
+ else:
52
+ raise ValueError(f"Survey code {survey_code} not recognized.")
53
+
54
+ beam_arcsec = beam
55
+ beam_area_arcsec2 = beam_area_arcsec2
56
+ beam_area_sr = beam_area_sr
57
+
58
+
59
+ # --- Search for frequency axis (CTYPEi == 'FREQ') --- #
60
+ band_ref = None
61
+
62
+ for i in range(1, 6): # Loop over axis 1 to 5 in case of multi-dimensional FITS
63
+ ctype = hdr.get(f'CTYPE{i}', '')
64
+ if 'FREQ' in ctype.upper():
65
+ crval = hdr.get(f'CRVAL{i}', 0.0)
66
+ band_ref = crval * 1e-9 # Convert from Hz to GHz
67
+ break
68
+
69
+
70
+ return beam_arcsec, beam_area_arcsec2, beam_area_sr
@@ -0,0 +1,150 @@
1
+ import os
2
+
3
+ import matplotlib.pyplot as plt
4
+ import numpy as np
5
+
6
+
7
+ def plot_fit_summary(cutout, cutout_masked_full, model, residual, output_dir, label_name="fit", dpi=300,
8
+ box_size=None, poly_order=None, nmse=None):
9
+ """
10
+ Save 2D and 3D plots of cutout, model, and residual.
11
+
12
+ Parameters
13
+ ----------
14
+ cutout : 2D array
15
+ Original data image (cutout).
16
+ model : 2D array
17
+ Fitted Gaussian + background model.
18
+ residual : 2D array
19
+ Difference between data and model.
20
+ output_dir : str
21
+ Directory to save PNG files.
22
+ label : str
23
+ Filename prefix for saving.
24
+ dpi : int
25
+ Resolution for PNG files.
26
+ """
27
+ os.makedirs(output_dir, exist_ok=True)
28
+
29
+ # Shared color scale based on cutout range, ignoring NaNs
30
+ vmin_shared = np.nanmin(cutout)
31
+ vmax_shared = np.nanmax(cutout)
32
+
33
+ # Individual residual rescaled range, ignoring NaNs
34
+ vmin_resid = np.nanmin(residual)
35
+ vmax_resid = np.nanmax(residual)
36
+
37
+ data_list = [cutout, cutout_masked_full, model, residual, residual]
38
+ file_tags = ["cutout", "cutout masked full", "model", "residual", "residual_rescaled"]
39
+
40
+ if box_size is not None:
41
+ titles = [
42
+ f"Map Cutout Back. subt. • Box = {box_size} px ",
43
+ f"Original Map Cutout masked • Box = {box_size} px ",
44
+ f"Model (Gauss. + Backgr.) • Box = {box_size} px ",
45
+ f"Residual (Data − Model) • Box = {box_size} px ",
46
+ f"Residual (Rescaled) • Box = {box_size} px "
47
+ ]
48
+ else:
49
+ titles = [
50
+ "Map Cutout Back. subt • Box = NaN ",
51
+ "Original Map Cutout masked • Box = NaN ",
52
+ "Model (Gauss. + Backgr.) • Box = NaN ",
53
+ "Residual (Data − Model) • Box = NaN ",
54
+ "Residual (Rescaled) • Box = NaN "
55
+ ]
56
+
57
+ # Color limits: first three plots share cutout scale, last uses residual scale
58
+ vmins = [vmin_shared, vmin_shared, vmin_shared, vmin_resid]
59
+ vmaxs = [vmax_shared, vmax_shared, vmax_shared, vmax_resid]
60
+
61
+
62
+ # ------------------------ 2D plots --------------------------- #
63
+ for arr, title, tag, vmin, vmax in zip(data_list, titles, file_tags, vmins, vmaxs):
64
+ fig, ax = plt.subplots(figsize=(5, 4))
65
+
66
+ im = ax.imshow(arr, origin='lower', cmap='viridis', aspect='auto', vmin=vmin, vmax=vmax)
67
+ cb = fig.colorbar(im, ax=ax)
68
+ cb.set_label("Flux (Jy)", fontweight="bold", fontsize=9)
69
+
70
+ # Bold colorbar tick labels
71
+ cb.ax.tick_params(labelsize=8)
72
+ for tick in cb.ax.get_yticklabels():
73
+ tick.set_fontweight("bold")
74
+
75
+ # Bold axis labels
76
+ ax.set_xlabel("X (pix)", fontweight="bold", fontsize=9)
77
+ ax.set_ylabel("Y (pix)", fontweight="bold", fontsize=9)
78
+
79
+ # Bold tick labels
80
+ ax.tick_params(axis='both', labelsize=8)
81
+ for tick in ax.get_xticklabels():
82
+ tick.set_fontweight("bold")
83
+ for tick in ax.get_yticklabels():
84
+ tick.set_fontweight("bold")
85
+
86
+ # Format title with fit info, if provided
87
+ if box_size is not None and poly_order is not None and nmse is not None:
88
+ subtitle = f"• Polynomial Order = {poly_order} • NMSE = {nmse:.3f}"
89
+ full_title = f"{title}\n{subtitle}"
90
+ else:
91
+ full_title = title
92
+
93
+ ax.set_title(full_title, fontsize=10, fontweight="bold", linespacing=1.4)
94
+
95
+ # Save with tight bounding box
96
+ outname = os.path.join(output_dir, f"{label_name}_{tag}_2D.png")
97
+ fig.savefig(outname, dpi=dpi, bbox_inches='tight')
98
+ plt.close(fig)
99
+
100
+
101
+ # ------------------------ 3D surface plots --------------------------- #
102
+ ny, nx = cutout.shape
103
+ x = np.arange(nx)
104
+ y = np.arange(ny)
105
+ X, Y = np.meshgrid(x, y)
106
+
107
+ for arr, title, tag, vmin, vmax in zip(data_list, titles, file_tags, vmins, vmaxs):
108
+ fig = plt.figure(figsize=(6, 5))
109
+ ax = fig.add_subplot(111, projection='3d')
110
+
111
+ # Plot 3D surface
112
+ surf = ax.plot_surface(X, Y, arr, cmap='viridis',
113
+ vmin=vmin, vmax=vmax,
114
+ linewidth=0, antialiased=True)
115
+
116
+ # Z limits scaled to color range
117
+ ax.set_zlim(vmin, vmax)
118
+
119
+ # Set title with fit info
120
+ if poly_order is not None and nmse is not None:
121
+ subtitle = f"• Polynomial Order = {poly_order} • NMSE = {nmse:.3f}"
122
+ full_title = f"{title}\n{subtitle}"
123
+ else:
124
+ full_title = title
125
+
126
+ ax.set_title(full_title, fontsize=9, fontweight="bold", pad=-40)
127
+
128
+ # Axis labels
129
+ ax.set_xlabel("X (pix)", fontweight="bold", fontsize=9, labelpad=2)
130
+ ax.set_ylabel("Y (pix)", fontweight="bold", fontsize=9, labelpad=2)
131
+ ax.set_zlabel("Flux (Jy)", fontweight="bold", fontsize=9, labelpad=2)
132
+
133
+ # Bold tick labels
134
+ for label in ax.get_xticklabels():
135
+ label.set_fontweight("bold")
136
+ label.set_fontsize(8)
137
+ for label in ax.get_yticklabels():
138
+ label.set_fontweight("bold")
139
+ label.set_fontsize(8)
140
+ for label in ax.get_zticklabels():
141
+ label.set_fontweight("bold")
142
+ label.set_fontsize(8)
143
+
144
+ # Adjust margins to make room
145
+ fig.subplots_adjust(left=0.05, right=0.95, top=0.85, bottom=0.05)
146
+
147
+ # Save using tight layout with padding
148
+ outname = os.path.join(output_dir, f"{label_name}_{tag}_3D.png")
149
+ fig.savefig(outname, dpi=dpi, bbox_inches='tight', pad_inches=0.25)
150
+ plt.close(fig)