visualens 0.1.0__tar.gz

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 (40) hide show
  1. visualens-0.1.0/PKG-INFO +24 -0
  2. visualens-0.1.0/README.md +3 -0
  3. visualens-0.1.0/pyproject.toml +30 -0
  4. visualens-0.1.0/setup.cfg +4 -0
  5. visualens-0.1.0/visualens/__init__.py +5 -0
  6. visualens-0.1.0/visualens/catalog.py +512 -0
  7. visualens-0.1.0/visualens/fits_image.py +604 -0
  8. visualens-0.1.0/visualens/gui.py +313 -0
  9. visualens-0.1.0/visualens/lenstool_model/im2source.py +145 -0
  10. visualens-0.1.0/visualens/lenstool_model/lenstool_model.py +800 -0
  11. visualens-0.1.0/visualens/lenstool_model/simulate_image/simulate_image.py +256 -0
  12. visualens-0.1.0/visualens/lenstool_model/simulate_image/utils.py +301 -0
  13. visualens-0.1.0/visualens/lenstool_model/utils/file_makers.py +204 -0
  14. visualens-0.1.0/visualens/lenstool_model/utils/lenstool_to_lenstronomy.py +102 -0
  15. visualens-0.1.0/visualens/lenstool_model/utils/operations.py +32 -0
  16. visualens-0.1.0/visualens/lenstool_model/utils/param_extractors.py +622 -0
  17. visualens-0.1.0/visualens/lenstool_model/utils/redshift_extractors.py +81 -0
  18. visualens-0.1.0/visualens/lenstool_model/utils/utils_general.py +617 -0
  19. visualens-0.1.0/visualens/main.py +19 -0
  20. visualens-0.1.0/visualens/sliders_TEST.py +213 -0
  21. visualens-0.1.0/visualens/source_extraction/source_extract.py +23 -0
  22. visualens-0.1.0/visualens/utils/__init__.py +0 -0
  23. visualens-0.1.0/visualens/utils/utils_Qt/drag_widgets.py +339 -0
  24. visualens-0.1.0/visualens/utils/utils_Qt/selectable_classes.py +268 -0
  25. visualens-0.1.0/visualens/utils/utils_Qt/utils_general.py +104 -0
  26. visualens-0.1.0/visualens/utils/utils_astro/cat_manip.py +293 -0
  27. visualens-0.1.0/visualens/utils/utils_astro/get_cosmology.py +13 -0
  28. visualens-0.1.0/visualens/utils/utils_astro/utils_general.py +205 -0
  29. visualens-0.1.0/visualens/utils/utils_astro/utils_lensing.py +16 -0
  30. visualens-0.1.0/visualens/utils/utils_fits/utils_fits_image.py +111 -0
  31. visualens-0.1.0/visualens/utils/utils_general/sort_points.py +293 -0
  32. visualens-0.1.0/visualens/utils/utils_general/utils_general.py +222 -0
  33. visualens-0.1.0/visualens/utils/utils_plots/plot_utils_general.py +230 -0
  34. visualens-0.1.0/visualens/utils/utils_plots/plot_utils_specific.py +48 -0
  35. visualens-0.1.0/visualens/utils/utils_plots/plt_framework.py +78 -0
  36. visualens-0.1.0/visualens.egg-info/PKG-INFO +24 -0
  37. visualens-0.1.0/visualens.egg-info/SOURCES.txt +38 -0
  38. visualens-0.1.0/visualens.egg-info/dependency_links.txt +1 -0
  39. visualens-0.1.0/visualens.egg-info/requires.txt +11 -0
  40. visualens-0.1.0/visualens.egg-info/top_level.txt +1 -0
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: visualens
3
+ Version: 0.1.0
4
+ Summary: A visual interface to explore and manipulate astronomical images, catalogs, and lens models created using Lenstool.
5
+ Author-email: Tom Resseguier <tresseg1@jhu.edu>
6
+ Project-URL: Homepage, https://github.com/tomresseguier/Lenstool_GUI/
7
+ Project-URL: Issues, https://github.com/tomresseguier/Lenstool_GUI/issues
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: numpy>=1.21
11
+ Requires-Dist: astropy>=5.0
12
+ Requires-Dist: matplotlib>=3.5
13
+ Requires-Dist: PyQt5>=5.15
14
+ Requires-Dist: pyqtgraph>=0.12
15
+ Requires-Dist: scipy>=1.7
16
+ Requires-Dist: pandas>=1.4
17
+ Requires-Dist: tqdm>=4.60
18
+ Requires-Dist: corner>=2.2
19
+ Requires-Dist: stsynphot>=0.3
20
+ Requires-Dist: lenstronomy>=1.1
21
+
22
+ # Lenstool GUI
23
+
24
+ A visual interface to explore astronomical images, catalogs, and lens models created using Lenstool.
@@ -0,0 +1,3 @@
1
+ # Lenstool GUI
2
+
3
+ A visual interface to explore astronomical images, catalogs, and lens models created using Lenstool.
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "visualens"
3
+ version = "0.1.0"
4
+ authors = [
5
+ { name="Tom Resseguier", email="tresseg1@jhu.edu" },
6
+ ]
7
+ description = "A visual interface to explore and manipulate astronomical images, catalogs, and lens models created using Lenstool."
8
+ readme = "README.md"
9
+ requires-python = ">=3.8"
10
+ dependencies = [
11
+ "numpy>=1.21",
12
+ "astropy>=5.0",
13
+ "matplotlib>=3.5",
14
+ "PyQt5>=5.15",
15
+ "pyqtgraph>=0.12",
16
+ "scipy>=1.7",
17
+ "pandas>=1.4",
18
+ "tqdm>=4.60",
19
+ "corner>=2.2",
20
+ "stsynphot>=0.3",
21
+ "lenstronomy>=1.1"
22
+ ]
23
+
24
+ [project.urls]
25
+ "Homepage" = "https://github.com/tomresseguier/Lenstool_GUI/"
26
+ "Issues" = "https://github.com/tomresseguier/Lenstool_GUI/issues"
27
+
28
+ [build-system]
29
+ requires = ["setuptools>=61.0"]
30
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+
2
+
3
+
4
+
5
+
@@ -0,0 +1,512 @@
1
+ import os
2
+ import numpy as np
3
+ from matplotlib import pyplot as plt
4
+ from matplotlib.patches import Ellipse, Rectangle
5
+ from tqdm import tqdm
6
+
7
+ import PyQt5
8
+ import pyqtgraph as pg
9
+ from astropy.table import Table
10
+ import pandas as pd
11
+
12
+ ###############################################################################
13
+ from .source_extraction.match_cat import run_match
14
+ from .utils.utils_astro.cat_manip import match_cat2
15
+ from .utils.utils_plots.plot_utils_general import *
16
+ from .utils.utils_Qt.selectable_classes import SelectableEllipse, SelectableScatter, SelectSources, ellipse_maker_ROI
17
+ from .utils.utils_Qt.utils_general import make_handles, InRectangle, make_full_color
18
+ from .utils.utils_general.utils_general import make_colnames_dict
19
+
20
+
21
+
22
+
23
+
24
+
25
+ def open_cat(cat_path) :
26
+ if '.fits' in cat_path :
27
+ cat = Table.read(cat_path, format='fits')
28
+ else :
29
+ with open(cat_path, 'r') as raw_cat :
30
+ first_line, second_line = raw_cat.readlines()[0:2]
31
+ print(first_line)
32
+ start_line = 1 if second_line.startswith('--') else 0
33
+ if len(first_line.split()) > len(first_line.split(',')) :
34
+ cat_df = pd.read_csv(cat_path, sep='\s+', skip_blank_lines=True, comment='#')[start_line:].apply(pd.to_numeric, errors='coerce')
35
+ else :
36
+ cat_df = pd.read_csv(cat_path, skip_blank_lines=True, comment='#')[start_line:].apply(pd.to_numeric, errors='coerce')
37
+ cat = Table.from_pandas(cat_df)
38
+ return cat
39
+
40
+
41
+ def make_uniform_names_cat(cat, self) :
42
+ uniform_names_cat = cat.copy()
43
+ colnames_dict = make_colnames_dict(cat, use_default_names=self.use_default_names)
44
+
45
+ print('Column names to be used:')
46
+ print(colnames_dict)
47
+
48
+ for colname in colnames_dict.keys() :
49
+ if colnames_dict[colname] is not None :
50
+
51
+ if colname in uniform_names_cat.colnames :
52
+ uniform_names_cat[colname] = uniform_names_cat[colnames_dict[colname]]
53
+ elif colname in ['ra', 'dec'] :
54
+ uniform_names_cat.rename_column(colnames_dict[colname], colname)
55
+ else :
56
+ uniform_names_cat.add_column( uniform_names_cat[colnames_dict[colname]], name=colname )
57
+
58
+
59
+ if colnames_dict['a'] != 'A_IMAGE' and colnames_dict['b'] != 'B_IMAGE' :
60
+ if self.units is None :
61
+ self.units = input("ellipticity parameters " + str(colnames_dict['a']) \
62
+ + " and " + str(colnames_dict['b']) + " in pixels? [y][arcsec][deg]")
63
+ if self.units == 'deg' :
64
+ uniform_names_cat.replace_column( 'a', uniform_names_cat['a']/(self.fits_image.pix_deg_scale) )
65
+ uniform_names_cat.replace_column( 'b', uniform_names_cat['b']/(self.fits_image.pix_deg_scale) )
66
+ if self.units == 'arcsec' :
67
+ uniform_names_cat.replace_column( 'a', uniform_names_cat['a']/(self.fits_image.pix_deg_scale*3600) )
68
+ uniform_names_cat.replace_column( 'b', uniform_names_cat['b']/(self.fits_image.pix_deg_scale*3600) )
69
+
70
+ #if colnames_dict['x']==None :
71
+ x, y = self.fits_image.world_to_image(uniform_names_cat['ra'], uniform_names_cat['dec'], unit='deg')
72
+ uniform_names_cat['x'] = x
73
+ uniform_names_cat['y'] = y
74
+
75
+ yesno = 'y'
76
+ if colnames_dict['a'] is not None and not self.use_default_names :
77
+ yesno = input("'a', 'b' and 'theta' columns found in catalog. Use them as ellipticity parameters (if not, sources will be shown as circles)? [y] or [n]")
78
+ if colnames_dict['a'] is None or yesno != 'y' :
79
+ size = np.min([self.fits_image.image_data.shape[0], self.fits_image.image_data.shape[1]]) / 1000
80
+ uniform_names_cat['a'] = np.full(len(uniform_names_cat), size)
81
+ uniform_names_cat['b'] = np.full(len(uniform_names_cat), size)
82
+ uniform_names_cat['theta'] = np.full(len(uniform_names_cat), 0.)
83
+ return uniform_names_cat
84
+
85
+
86
+ def initialize_catalog(cat, self) :
87
+ ##### Combine or just open the catalog #####
88
+ ref_path = None
89
+ if isinstance(cat, str) :
90
+ ref_path = cat
91
+ cat = open_cat(cat)
92
+ elif isinstance(cat, list) :
93
+ ref_path = os.path.dirname(cat[0])
94
+ run_match(cat[0], cat[1])
95
+ for i in range(len(cat)-3) :
96
+ run_match('matched_A_B.fits', cat[i+2])
97
+ matched_cat = run_match('matched_A_B.fits', cat[-1])
98
+ cat = Table(matched_cat[1].data)
99
+ os.remove('matched_A_B.fits')
100
+
101
+ ##### Standardize the column names #####
102
+ uniform_names_cat = make_uniform_names_cat(cat, self)
103
+ return uniform_names_cat, ref_path
104
+
105
+
106
+
107
+
108
+
109
+
110
+
111
+
112
+ class catalog :
113
+ def __init__(self, cat, fits_image, color=[0., 1., 1., 0., 0.5], mag_colnames=['magAB_F814W', 'magAB_F435W'], use_default_names=True, units=None) :
114
+ self.fits_image = fits_image
115
+
116
+ self.mag_colnames = mag_colnames
117
+ self.use_default_names = use_default_names
118
+ self.units = units
119
+
120
+ self.cat, self.ref_path = initialize_catalog(cat, self)
121
+ self.qtItems = [] #np.empty(len(self.cat), dtype=PyQt5.QtWidgets.QGraphicsEllipseItem)
122
+ #self.qtItems = [PyQt5.QtWidgets.QGraphicsEllipseItem() for _ in range(len(self.cat))]
123
+ self.qtItems_column = [] #np.empty(len(self.cat), dtype=pg.TextItem)
124
+ #self.qtItems = np.empty(len(self.cat), dtype=utils.utils_classes.selectable_ellipse.SelectableEllipse)
125
+ self.color = color if color is not None else [0., 1., 1., 0., 0.5]
126
+ self.selection_mask = np.full(len(self.cat), False)
127
+ self.selection_regions = []
128
+ self.Scatter_widget = None
129
+ self.x_axis_cleaned = np.full(len(self.cat), None)
130
+ self.y_axis_cleaned = np.full(len(self.cat), None)
131
+
132
+ #self.is_plotted = False
133
+ #self.is_plotted_column = False
134
+
135
+
136
+ def make_mask_naninf(self, xy_axes=None) :
137
+ #mag_F444W = flux_muJy_to_magAB(self.cat['f444w_tot_0'])
138
+ #mag_F090W = flux_muJy_to_magAB(self.cat['f090w_tot_0'])
139
+ #x_axis = mag_F444W
140
+ #y_axis = mag_F090W - mag_F444W
141
+
142
+ if xy_axes is None :
143
+ mag_F814W = self.cat[self.mag_colnames[0]]
144
+ mag_F435W = self.cat[self.mag_colnames[1]]
145
+ x_axis = mag_F814W
146
+ y_axis = mag_F435W - mag_F814W
147
+ else :
148
+ x_axis = self.cat[xy_axes[0]]
149
+ y_axis = self.cat[xy_axes[1]]
150
+
151
+ nan_mask = np.logical_not(np.isnan(x_axis)) & np.logical_not(np.isnan(y_axis))
152
+ inf_mask = (x_axis!=np.inf) & (y_axis!=np.inf)
153
+ #extremes_mask = (x_axis>0) & (x_axis<50)
154
+ self.mask_naninf = nan_mask & inf_mask
155
+ self.x_axis_cleaned = x_axis[self.mask_naninf]
156
+ self.y_axis_cleaned = y_axis[self.mask_naninf]
157
+
158
+ self.cat = self.cat[self.mask_naninf]
159
+ self.plot()
160
+ #self.qtItems = np.empty(len(self.cat), dtype=PyQt5.QtWidgets.QGraphicsEllipseItem)
161
+ self.selection_mask = np.full(len(self.cat), False)
162
+
163
+ def plot(self, scale=1., color=None, text_column=None, linewidth=3, marker=None) :
164
+ self.clear()
165
+ print('Plotting...')
166
+ x = self.cat['x']
167
+ y = self.cat['y']
168
+ semi_major = self.cat['a'] * scale
169
+ semi_minor = self.cat['b'] * scale
170
+ angle = self.cat['theta']
171
+ for i in tqdm(range(len(semi_major))) :
172
+ ellipse = self.plot_one_object(x[i], y[i], semi_major[i], semi_minor[i], angle[i], i, \
173
+ color=color, linewidth=linewidth, marker=marker, size=scale*15.)
174
+ self.qtItems.append(ellipse)
175
+
176
+ if text_column is not None:
177
+ self.plot_column(text_column, color=color)
178
+
179
+ def plot_column(self, text_column, color=None, n_digit=3):
180
+ self.clear_column()
181
+ print('Plotting...')
182
+ if text_column not in self.cat.colnames:
183
+ print(f"Column '{text_column}' not found in catalog")
184
+ return
185
+
186
+ color = self.color if color is None else color
187
+ color = list(np.array(self.color[:3])*255)
188
+
189
+ for i in tqdm(range(len(self.cat))) :
190
+ to_plot = self.cat[text_column][i]
191
+ text = to_plot if type(to_plot)==str else f"{to_plot:.{n_digit}g}"
192
+ text_item = pg.TextItem(text, color=color)
193
+
194
+ # Get ellipse position and size for offset calculation
195
+ x = self.cat['x'][i]
196
+ y = self.fits_image.image_data.shape[0] - self.cat['y'][i] # Flip y to match PyQtGraph convention
197
+ semi_major = self.cat['a'][i]
198
+ semi_minor = self.cat['b'][i]
199
+
200
+ # Position text slightly offset from the ellipse
201
+ offset = max(semi_major, semi_minor)
202
+ text_item.setPos(x + offset/2, y - offset/2)
203
+
204
+ font = PyQt5.QtGui.QFont()
205
+ font.setPointSize(15)
206
+ text_item.setFont(font)
207
+
208
+ self.fits_image.qt_image.addItem(text_item)
209
+ self.qtItems_column.append(text_item)
210
+
211
+ def clear(self) :
212
+ #qtItems_list = self.fits_image.qt_image.getView().allChildItems()
213
+ print('Clearing galaxies...')
214
+ for qtItem in self.qtItems :
215
+ self.fits_image.qt_image.removeItem(qtItem)
216
+ del qtItem
217
+ self.qtItems.clear()
218
+
219
+ self.clear_column()
220
+
221
+ def clear_column(self) :
222
+ print('Clearing column labels...')
223
+ for text_item in self.qtItems_column:
224
+ self.fits_image.qt_image.removeItem(text_item)
225
+ del text_item
226
+ self.qtItems_column.clear()
227
+
228
+ def clear_selection(self) :
229
+ self.selection_mask[np.full(len(self.cat), True)] = False
230
+ self.selection_regions.clear()
231
+ self.clear()
232
+ self.plot()
233
+
234
+ def plot_one_object(self, x, y, semi_major, semi_minor, angle, idx, color=None, linewidth=3, marker=None, size=15) :
235
+ if color is None :
236
+ color = self.color
237
+ #make the flip to accomosate pyqtgraph's plotting conventions
238
+ y = self.fits_image.image_data.shape[0] - y
239
+ angle = -angle
240
+ #####################################################################
241
+ if marker==None or marker=='ellipse' :
242
+ if self.Scatter_widget==None :
243
+ scatter_pos = None
244
+ else :
245
+ scatter_pos = (self.x_axis_cleaned[idx], self.y_axis_cleaned[idx])
246
+ to_plot = SelectableEllipse(x-semi_major/2, y-semi_minor/2, semi_major, semi_minor, idx, self.selection_mask,
247
+ color, linewidth=linewidth, scatter_pos=scatter_pos,
248
+ Scatter_widget=self.Scatter_widget)
249
+ to_plot.setTransformOriginPoint( PyQt5.QtCore.QPointF(x, y) )
250
+ to_plot.setRotation(angle)
251
+ else :
252
+ to_plot = pg.ScatterPlotItem(size=size, symbol=marker)
253
+ color = make_full_color(color)
254
+ if color[3]==0 : #filled_markers==False
255
+ to_plot.setPen( pg.mkPen(color[:3], width=2) ) #no outline
256
+ to_plot.setBrush( pg.mkBrush([0,0,0,0]) )
257
+ else :
258
+ to_plot.setPen( pg.mkPen([0,0,0,0], width=0.1) ) #outline makes cross thicker
259
+ to_plot.setBrush( pg.mkBrush(color[:-2]) )
260
+
261
+ to_plot.setData([x], [y])
262
+
263
+ self.fits_image.qt_image.addItem(to_plot)
264
+ return to_plot
265
+
266
+ def make_selection_panel(self, xy_axes=None) :
267
+ self.make_mask_naninf(xy_axes=xy_axes)
268
+
269
+ self.Scatter_widget = SelectableScatter(self)
270
+ #self.Scatter_widget.setTitle('Red sequence')
271
+
272
+ self.Scatter_widget.setAspectLocked(lock=True, ratio=1)
273
+ self.Scatter_widget.autoRange()
274
+ #self.Scatter_widget.setSizePolicy(pg.QtWidgets.QSizePolicy.Fixed, pg.QtWidgets.QSizePolicy.Expanding)
275
+ self.fits_image.qt_layout.addWidget(self.Scatter_widget)
276
+
277
+ if xy_axes is not None :
278
+ self.Scatter_widget.setLabel('bottom', xy_axes[0])
279
+ self.Scatter_widget.setLabel('left', xy_axes[1])
280
+
281
+ #self.selection_mask = np.full(len(self.mag_F444W_cleaned), False)
282
+ self.plot()
283
+
284
+ def remove_selection_panel(self) :
285
+ self.Scatter_widget.hide()
286
+ del self.Scatter_widget
287
+ self.Scatter_widget = None
288
+
289
+ def make_image_ROI(self) :
290
+ """
291
+ Creates an ellipse ROI to add elliptic sources to the catalog by drawing them by hand.
292
+ """
293
+ center_y = self.fits_image.image_data.shape[0]/2
294
+ center_x = self.fits_image.image_data.shape[1]/2
295
+ self.image_ROI = ellipse_maker_ROI([center_x-200, center_y-100], [400, 200], self.fits_image.qt_image, self.fits_image.window, self.cat)
296
+ make_handles(self.image_ROI)
297
+ self.fits_image.qt_image.addItem(self.image_ROI)
298
+
299
+ def make_selection_ROI(self) :
300
+ """
301
+ Creates a rectangle ROI to select all sources inside of it.
302
+ """
303
+ self.fits_image.image_widget.cat = self
304
+ self.select_sources = SelectSources(self)
305
+
306
+ def save_selection_mask(self, path=None) :
307
+ self.selection_mask_path = self.make_path(path, self.ref_path, 'selection_mask.npy')
308
+ np.save(self.selection_mask_path, self.selection_mask)
309
+ print("Selection mask saved at " + self.selection_mask_path)
310
+
311
+ def load_selection_mask(self, path=None) :
312
+ self.selection_mask_path = self.make_path(path, self.ref_path, 'selection_mask.npy')
313
+ self.selection_mask = np.load(self.selection_mask_path)
314
+
315
+ def save_selection_regions(self, path=None) :
316
+ self.selection_regions_path = self.make_path(path, self.fits_image.image_path, 'selection_regions.npy')
317
+ np.save(self.selection_regions_path, self.selection_regions)
318
+
319
+ def load_selection_regions(self, path=None, name='selection_regions.npy') :
320
+ self.selection_regions_path = self.make_path(path, self.fits_image.image_path, name)
321
+ self.selection_regions = np.load(self.selection_regions_path).tolist()
322
+
323
+ size_y = self.fits_image.qt_image.image.shape[0]
324
+ for rect_params in self.selection_regions :
325
+ indiv_mask = InRectangle(self.cat['x'], size_y - self.cat['y'], rect_params)
326
+ self.selection_mask[indiv_mask] = True
327
+
328
+ fig, ax = plt.subplots()
329
+ ax.axis('equal')
330
+ size = max(self.fits_image.qt_image.image.shape[0], self.fits_image.qt_image.image.shape[1])
331
+ #ax.invert_yaxis()
332
+ ax.set_ylim([size+2000, -2000])
333
+ ax.set_xlim([-4000, size+4000])
334
+
335
+ for i, rect_params in enumerate(self.selection_regions) :
336
+ x0, y0, a, b, angle = rect_params
337
+ #x1, y1 = x0, y0
338
+ #x2, y2 = x0 + a*np.cos(angle), y0 + a*np.sin(angle)
339
+ #x3, y3 = x0 + a*np.cos(angle) - b*np.sin(angle), y0 + a*np.sin(angle) + b*np.cos(angle)
340
+ #x4, y4 = x0 - b*np.sin(angle), y0 + b*np.cos(angle)
341
+ #ax.plot([x1, x2, x3, x4, x1], size_y-np.array([y1, y2, y3, y4, y1]), c='b')
342
+ #ax.axis('equal')
343
+ ax.add_patch( Rectangle((x0, y0), a, b, angle=angle*180/np.pi, alpha=0.4 ))
344
+ ax.text(x0, y0, str(i))
345
+ fig.show()
346
+ plt.pause(0.05)
347
+
348
+ def make_path(self, path, ref_path, name) :
349
+ if path is None :#and self.ref_path is not None :
350
+ #to_return = os.path.join(os.path.dirname(ref_path), name)
351
+ to_return = os.path.join(os.path.dirname(ref_path), os.path.basename(ref_path).split('.')[0] + '_' + name)
352
+ elif os.path.isdir(path) :
353
+ to_return = os.path.join(path, name)
354
+ elif os.path.isdir(os.path.dirname(path)) :
355
+ to_return = path
356
+ return to_return
357
+
358
+
359
+ def plot_one_galaxy_mpl(self, x, y, a, b, theta, color=[1,1,1], text=None, ax=None, linewidth=1., text_color='white', text_alpha=0.5) :
360
+ edgecolor = list(color).copy()
361
+ edgecolor.append(1)
362
+ facecolor = edgecolor.copy()
363
+ facecolor[-1] = 0
364
+ ellipse = Ellipse( (x, y), a, b, angle=theta, facecolor=facecolor, edgecolor=edgecolor, lw=linewidth )
365
+ if ax is None :
366
+ self.fits_image.mpl_ax.add_artist(ellipse)
367
+ if text is not None :
368
+ #self.fits_image.mpl_ax.text(x-1.5*b*np.abs(np.sin(theta)), y-1.5*b*np.abs(np.cos(theta)), text, color=edgecolor[:3], \
369
+ # ha='right', va='top')
370
+ offset = 0.85
371
+ theta_modulo = theta%180 * np.pi/180
372
+ if theta_modulo<np.pi/2 :
373
+ x_text, y_text = x+offset*b*np.abs(np.sin(theta_modulo)), y-offset*b*np.abs(np.cos(theta_modulo))
374
+ horizontalalignment, verticalalignment = 'left', 'top'
375
+ else :
376
+ x_text, y_text = x-offset*b*np.abs(np.sin(theta_modulo)), y-offset*b*np.abs(np.cos(theta_modulo))
377
+ horizontalalignment, verticalalignment = 'right', 'top'
378
+ self.fits_image.mpl_ax.text( x_text, y_text, text, c=text_color, alpha=1, fontsize=15, \
379
+ ha=horizontalalignment, va=verticalalignment, \
380
+ bbox=dict(facecolor=edgecolor[:3], alpha=text_alpha, edgecolor='none') )
381
+ else :
382
+ ax.add_artist(ellipse)
383
+ if text is not None :
384
+ offset = 0.85
385
+ theta_modulo = theta%180 * np.pi/180
386
+ if theta_modulo<np.pi/2 :
387
+ x_text, y_text = x+offset*b*np.abs(np.sin(theta_modulo)), y-offset*b*np.abs(np.cos(theta_modulo))
388
+ horizontalalignment, verticalalignment = 'left', 'top'
389
+ else :
390
+ x_text, y_text = x-offset*b*np.abs(np.sin(theta_modulo)), y-offset*b*np.abs(np.cos(theta_modulo))
391
+ horizontalalignment, verticalalignment = 'right', 'top'
392
+ #ax.text(x_text, y_text, text, color=edgecolor[:3], \
393
+ # ha=horizontalalignment, va=verticalalignment)
394
+ ax.text( x_text, y_text, text, c=text_color, alpha=1, fontsize=15, \
395
+ ha=horizontalalignment, va=verticalalignment, \
396
+ bbox=dict(facecolor=edgecolor[:3], alpha=text_alpha, edgecolor='none') )
397
+
398
+ def export_to_mult_file(self, file_path=None) :
399
+ if file_path is None :
400
+ file_path = os.path.join(os.path.dirname(self.fits_image.image_path), 'mult.lenstool')
401
+
402
+ sub_cat = self.cat[self.selection_mask] if True in self.selection_mask else self.cat
403
+
404
+ header = "#REFERENCE 0\n## id RA Dec a b theta z mag\n"
405
+
406
+ if 'THETA_WORLD' in sub_cat.colnames :
407
+ theta_colname = 'THETA_WORLD'
408
+ elif 'theta' in sub_cat.colnames :
409
+ theta_colname = 'theta'
410
+ print("Using 'theta' column in exported catalog, make sure theta is relative to world coordinates and not image coordinates!!")
411
+ else :
412
+ theta_colname = None
413
+
414
+ with open(file_path, 'w') as f :
415
+ f.write(header)
416
+ for index, row in enumerate(sub_cat) :
417
+ if theta_colname is not None :
418
+ if 'zb' in sub_cat.colnames and 'f814w_mag' in sub_cat.colnames :
419
+ line = (f"{row['id']:<3} {row['ra']:10.6f} {row['dec']:10.6f} "
420
+ f"{row['a']:8.6f} {row['b']:8.6f} {row[theta_colname]:8.6f} "
421
+ f"{row['zb']:8.6f} {row['f814w_mag']:8.6f}\n")
422
+ else :
423
+ line = (f"{row['id']:<3} {row['ra']:10.6f} {row['dec']:10.6f} "
424
+ f"{row['a']:8.6f} {row['b']:8.6f} {row[theta_colname]:8.6f} "
425
+ "0.0 0.0\n")
426
+ else :
427
+ line = (f"{row['id']:<3} {row['ra']:10.6f} {row['dec']:10.6f} "
428
+ "0.0 0.0 0.0 0.0 0.0\n")
429
+ f.write(line)
430
+ print('Selected sources exported at ' + file_path)
431
+
432
+
433
+ def export_to_potfile(self, file_path=None, units='pixel') :
434
+ cat = self.cat[self.selection_mask] if True in self.selection_mask else self.cat
435
+
436
+ if units=='pixel' :
437
+ cat['a'] *= self.fits_image.pix_deg_scale*3600
438
+ cat['b'] *= self.fits_image.pix_deg_scale*3600
439
+ print("Converting pixel units to arcsec")
440
+ elif units=='deg' :
441
+ cat['a'] *= 3600
442
+ cat['b'] *= 3600
443
+ print("Converting deg units to arcsec")
444
+ else :
445
+ if units!='arcsec' :
446
+ print("Units not recognized, exporting as is")
447
+
448
+ mag_col = self.mag_colnames[0] if self.mag_colnames[0] in cat.colnames else 'mag'
449
+ print(f"Using '{mag_col}' as mag column")
450
+ sort_array = np.argsort(cat[mag_col])
451
+ sorted_cat = cat[sort_array]
452
+
453
+ lines = []
454
+ lines.append('#REFERENCE 0\n')
455
+ lines.append('## id RA Dec a b theta mag lum\n')
456
+
457
+ for i, galaxy in enumerate(sorted_cat) :
458
+ lines.append( '%d %f %f %f %f %f %f 0.\n' % (i+1, galaxy['ra'], galaxy['dec'], galaxy['a'], galaxy['b'], galaxy['theta']-self.fits_image.orientation, galaxy[mag_col]) )
459
+
460
+ if file_path is None :
461
+ if self.ref_path is not None :
462
+ file_path = os.path.join( os.path.dirname(self.ref_path), 'exported_potfile.lenstool')
463
+ else :
464
+ file_path = os.path.join( os.path.dirname(self.fits_image.image_path), 'exported_potfile.lenstool')
465
+ print('Exporting selected sources to ' + file_path)
466
+ with open(file_path, 'w') as file:
467
+ file.writelines(lines)
468
+
469
+ #def transfer_col(self, col_to_transfer) :
470
+ # if self.fits_image.imported_cat is not None :
471
+ # if col_to_transfer in self.fits_image.imported_cat.cat.colnames :
472
+ # temp_cat = match_cat2([self.cat, self.fits_image.imported_cat.cat], keep_all_col=True, fill_in_value=-1)
473
+ # if col_to_transfer in self.cat.colnames :
474
+ # col_to_transfer = col_to_transfer + '_CAT2'
475
+ # self.cat[col_to_transfer] = temp_cat[col_to_transfer]
476
+ # print('###############\nColumn ' + col_to_transfer + ' added.\n###############')
477
+ # else :
478
+ # print(col_to_transfer + ' not found in imported_cat')
479
+ # else :
480
+ # print('No imported_cat')
481
+
482
+ def transfer_col(self, col_to_transfer, which_cat="imported_cat", index=None):
483
+ if index is not None :
484
+ source_cat = self.fits_image.imported_cat_list[index]
485
+ else :
486
+ source_cat = getattr(self.fits_image, which_cat, None)
487
+
488
+ if source_cat is not None:
489
+ if col_to_transfer in source_cat.cat.colnames:
490
+ temp_cat, match_idx = match_cat2([self.cat, source_cat.cat], keep_all_col=True, fill_in_value=-1.0, return_match_idx=True)
491
+ if col_to_transfer in self.cat.colnames:
492
+ col_to_transfer = col_to_transfer + '_CAT2'
493
+ self.cat[col_to_transfer] = temp_cat[col_to_transfer]
494
+ print(f'###############\nColumn {col_to_transfer} added.\n###############')
495
+ else:
496
+ print(f'{col_to_transfer} not found in {which_cat}')
497
+ else:
498
+ print(f'No {which_cat}')
499
+ return match_idx
500
+
501
+ #def export_thumbnails(self, mask=None, group_images=True) :
502
+ # if mask is None :
503
+ # mask = self.selection_mask
504
+ # print('Function is being built.')
505
+ # ### TO DO ###
506
+
507
+
508
+
509
+
510
+
511
+
512
+