Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance From File catalog loading to support more columns and improve Clear Table functionality #3359

Merged
merged 25 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2f28144
Add functionality to load catalogs from a file
haticekaratay Dec 15, 2024
746d8db
Fix table and viewer clearing functionality
haticekaratay Dec 15, 2024
485846e
Remove redundant 'Clear' button and combine its functionality with 'C…
haticekaratay Dec 16, 2024
edf93eb
Codestyle
haticekaratay Dec 17, 2024
73aad20
Add change log
haticekaratay Dec 17, 2024
d19de55
Update tests
haticekaratay Dec 17, 2024
5ece13a
Fix failing test
haticekaratay Dec 18, 2024
3fb6df9
Fix the test, ensuring to first select a row before zooming in.
haticekaratay Dec 18, 2024
640b615
Update change log
haticekaratay Dec 19, 2024
d7d7439
Preserve the unit when serializing
haticekaratay Dec 19, 2024
7799594
Generate default Object IDs when label column is missing
haticekaratay Dec 20, 2024
a96babd
Update docs related to label column in catalogs
haticekaratay Dec 20, 2024
9dbbeca
Merge branch 'main' into load_catalog_from_file
haticekaratay Dec 23, 2024
b5b4570
Update tests after merge
haticekaratay Dec 23, 2024
670cf31
Update test
haticekaratay Dec 23, 2024
c2ba76b
Merge branch 'main' of https://github.com/spacetelescope/jdaviz into …
haticekaratay Dec 23, 2024
0b22f24
Move changelog to correct milestone
haticekaratay Dec 23, 2024
04bf999
Adjust test assertions
haticekaratay Dec 26, 2024
b1f2277
Add test for loading catalogs with additional columns
haticekaratay Dec 26, 2024
b8ae09e
Update change log
haticekaratay Jan 2, 2025
dcb5971
Update based on feedback
haticekaratay Jan 2, 2025
289f216
Merge branch 'main' into load_catalog_from_file
haticekaratay Jan 2, 2025
d692f86
Apply suggestions from code review
haticekaratay Jan 2, 2025
7c01d7a
Update CHANGES.rst
haticekaratay Jan 2, 2025
974bee4
Adjust test tolerance
haticekaratay Jan 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Imviz
- Catalog Search plugin now exposes a maximum sources limit for all catalogs and resolves an edge case
when loading a catalog from a file that only contains one source. [#3337]

- Catalog Search plugin now supports loading catalog data directly from files. [#3359]
pllim marked this conversation as resolved.
Show resolved Hide resolved

Mosviz
^^^^^^

Expand Down
66 changes: 41 additions & 25 deletions jdaviz/configs/imviz/plugins/catalogs/catalogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ class Catalogs(PluginTemplateMixin, ViewerSelectMixin, HasFileImportSelect, Tabl
'Object ID': np.nan,
'id': np.nan,
'x_coord': np.nan,
'y_coord': np.nan}
'y_coord': np.nan
}

headers = ['Right Ascension (degrees)', 'Declination (degrees)',
'Object ID', 'x_coord', 'y_coord']

@property
def user_api(self):
Expand All @@ -70,11 +74,8 @@ def __init__(self, *args, **kwargs):
self._marker_name = 'catalog_results'

# initializing the headers in the table that is displayed in the UI
headers = ['Right Ascension (degrees)', 'Declination (degrees)',
'Object ID', 'x_coord', 'y_coord']

self.table.headers_avail = headers
self.table.headers_visible = headers
self.table.headers_avail = self.headers
self.table.headers_visible = self.headers
self.table._default_values_by_colname = self._default_table_values
self.table._selected_rows_changed_callback = lambda msg: self.plot_selected_points()
self.table.item_key = 'id'
Expand All @@ -88,13 +89,15 @@ def __init__(self, *args, **kwargs):
def _file_parser(path):
haticekaratay marked this conversation as resolved.
Show resolved Hide resolved
try:
table = QTable.read(path)
except Exception:
return 'Could not parse file with astropy.table.QTable.read', {}
if not table.colnames: # Ensure the file has columns
return "File contains no columns", {}

if 'sky_centroid' not in table.colnames:
return 'Table does not contain required sky_centroid column', {}
if 'sky_centroid' not in table.colnames:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this have to be within try? This check should never fail if table is successfully read and we already have a blanket except for that. It would only result in a bool.

In fact, I think all the if after .read can be after the try-except block. Unless I missed something here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for catching that! Honestly, I don’t know what I was thinking when I moved those checks inside the try-except block. You’re absolutely right that they belong outside since they’re not handling exceptions from QTable.read. I’ve fixed it now, and I appreciate your sharp eye for detail!

return 'Table does not contain required sky_centroid column', {}

return '', {path: table}
return '', {path: table}
except Exception:
return 'Could not parse file with astropy.table.QTable.read', {}

@with_spinner()
def search(self, error_on_fail=False):
Expand All @@ -113,7 +116,7 @@ def search(self, error_on_fail=False):

"""
# calling clear in the case the user forgot after searching
self.clear()
self.clear_table()

# gets the current viewer
viewer = self.viewer.selected_obj
Expand Down Expand Up @@ -202,6 +205,11 @@ def search(self, error_on_fail=False):
# all exceptions when going through the UI should have prevented setting this path
# but this exceptions might be raised here if setting from_file from the UI
table = self.catalog.selected_obj
column_names = list(table.colnames)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
column_names = list(table.colnames)
column_names = table.colnames

I think it is always a list. No need to recast.

https://github.com/astropy/astropy/blob/53b673bb4fd98da7b8972837a9650469bc130b02/astropy/table/table.py#L2218

self.table.headers_avail = self.headers + [
col for col in column_names if col not in self.headers and col not in ["sky_centroid", "label"] # noqa:E501
haticekaratay marked this conversation as resolved.
Show resolved Hide resolved
]
self.table.headers_visible = self.headers
self.app._catalog_source_table = table
if len(table['sky_centroid']) > self.max_sources:
skycoord_table = table['sky_centroid'][:self.max_sources]
Expand Down Expand Up @@ -269,16 +277,19 @@ def search(self, error_on_fail=False):
if len(self.app._catalog_source_table) == 1 or self.max_sources == 1:
x_coordinates = [x_coordinates]
y_coordinates = [y_coordinates]
for idx, (row, x_coord, y_coord) in enumerate(zip(self.app._catalog_source_table, x_coordinates, y_coordinates)): # noqa:E501
row_info = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the loaded table has to have a 'sky centroid' column, but it is then broken up into ra and dec, which means when its written back out it cant be read back in because it doesn't have 'sky centroid'. would it be possible to add a check when loading if the table already contains a ra and dec column rather than unpacking sky centroid? as it is, a table written out by the app can not be read back in

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that problem pre-date this PR? Just wondering if round-tripping is in scope here (e.g., writing it out back as sky centroid as a single column in ECSV format).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export_table is ultimately called which will treat the the TableMixin columns as a QTable

self._qtable.write(filename, overwrite=overwrite)
and write the columns as is, so unless you have a column in our table that is labeled sky_centroid, you wouldn't be able to read it back in after exporting. This is also true if you try exporting Gaia or SDSS.

In the case of 'from file', we already have the sky_centroid column, so we could add the sky_centroid column with the same name to our table, export the table, and then the exported file is able to be reloaded into Imviz.

The other catalogs options could be added in a follow up effort for either writing the sky_centroid column to the loaded catalog table or handle it on export.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to keep it aligned with the other Search options, but I didn't think of the export functionality then. Thanks for the additional context, all; I will keep the existing sky_centroid column in the table.

'Right Ascension (degrees)': row['sky_centroid'].ra.deg,
'Declination (degrees)': row['sky_centroid'].dec.deg,
'Object ID': str(row.get('label', 'N/A')),
'id': idx,
'x_coord': x_coord,
'y_coord': y_coord,
}
for col in table.colnames:
if col not in ['label', 'sky_centroid']: # Skip already processed columns
haticekaratay marked this conversation as resolved.
Show resolved Hide resolved
row_info[col] = row[col]

for row, x_coord, y_coord in zip(self.app._catalog_source_table,
x_coordinates, y_coordinates):
# Check if the row contains the required keys
row_info = {'Right Ascension (degrees)': row['sky_centroid'].ra.deg,
'Declination (degrees)': row['sky_centroid'].dec.deg,
'Object ID': str(row.get('label', 'N/A')),
'id': len(self.table),
'x_coord': x_coord.item() if x_coord.size == 1 else x_coord,
'y_coord': y_coord.item() if y_coord.size == 1 else y_coord}
self.table.add_item(row_info)

filtered_skycoord_table = viewer.state.reference_data.coords.pixel_to_world(x_coordinates,
Expand Down Expand Up @@ -326,7 +337,8 @@ def plot_selected_points(self):
def vue_zoom_in(self, *args, **kwargs):
"""This function will zoom into the image based on the selected points"""
selected_rows = self.table.selected_rows

if not selected_rows: # Check if no rows are selected
return
x = [float(coord['x_coord']) for coord in selected_rows]
y = [float(coord['y_coord']) for coord in selected_rows]

Expand Down Expand Up @@ -361,9 +373,13 @@ def vue_do_search(self, *args, **kwargs):
# calls self.search() which handles all of the searching logic
self.search()

def clear(self, hide_only=True):
def clear_table(self, hide_only=True):
haticekaratay marked this conversation as resolved.
Show resolved Hide resolved
# gets the current viewer
viewer = self.viewer.selected_obj
# Clear the table before performing a new search
self.table.items = []
self.table.selected_rows = []
self.table.selected_indices = []

if not hide_only and self._marker_name in self.app.data_collection.labels:
# resetting values
Expand All @@ -385,5 +401,5 @@ def clear(self, hide_only=True):
if layer_is_table_data(lyr.layer) and lyr.layer.label == self._marker_name:
lyr.visible = False

def vue_do_clear(self, *args, **kwargs):
self.clear()
def vue_do_clear_table(self, *args, **kwargs):
self.clear_table()
18 changes: 10 additions & 8 deletions jdaviz/configs/imviz/plugins/catalogs/catalogs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@
See the <j-external-link link='https://astroquery.readthedocs.io/en/latest/gaia/gaia.html' linktext='astropy.gaia docs'></j-external-link> for details on the query defaults.
</j-docs-link>
</v-row>

<v-row v-if="catalog_selected && catalog_selected.endsWith('.ecsv')">
<v-select
v-model="selected_columns"
:items="column_names"
label="Select Columns"
multiple
hint="Select columns to display in the table."
/>
</v-row>

<v-row>
<v-text-field
Expand All @@ -53,14 +63,6 @@
</v-row>

<v-row class="row-no-outside-padding">
<v-col>
<plugin-action-button
:results_isolated_to_plugin="true"
@click="do_clear"
>
Clear
</plugin-action-button>
</v-col>
<v-col>
<plugin-action-button
:results_isolated_to_plugin="true"
Expand Down
34 changes: 23 additions & 11 deletions jdaviz/configs/imviz/tests/test_catalogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from astropy.nddata import NDData
from astropy.coordinates import SkyCoord
from astropy.table import Table, QTable
from numpy.testing import assert_allclose


@pytest.mark.remote_data
Expand Down Expand Up @@ -113,7 +114,7 @@ def test_plugin_image_with_result(self, imviz_helper, tmp_path):
prev_results = catalogs_plugin.number_of_results

# testing that every variable updates accordingly when markers are cleared
catalogs_plugin.vue_do_clear()
catalogs_plugin.vue_do_clear_table()

assert not catalogs_plugin.results_available

Expand Down Expand Up @@ -157,12 +158,16 @@ def test_plugin_image_with_result(self, imviz_helper, tmp_path):
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == -0.5
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 1488.5

# First select a row
catalogs_plugin.table.selected_rows = [
catalogs_plugin.table.items[0]]
# Then zoom in to the selected
catalogs_plugin.vue_zoom_in()

assert imviz_helper.viewers['imviz-0']._obj.state.x_min == 858.24969
assert imviz_helper.viewers['imviz-0']._obj.state.x_max == 958.38461
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == 278.86265
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 378.8691
assert imviz_helper.viewers['imviz-0']._obj.state.x_min == 1022.5631800000001
pllim marked this conversation as resolved.
Show resolved Hide resolved
assert imviz_helper.viewers['imviz-0']._obj.state.x_max == 1122.56318
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == 675.29611
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 775.29611


def test_from_file_parsing(imviz_helper, tmp_path):
Expand Down Expand Up @@ -237,23 +242,30 @@ def test_offline_ecsv_catalog(imviz_helper, image_2d_wcs, tmp_path):
assert catalogs_plugin.number_of_results == n_entries
assert len(imviz_helper.app.data_collection) == 2 # image + markers

catalogs_plugin.clear()
catalogs_plugin.clear_table()

assert not catalogs_plugin.results_available
assert len(imviz_helper.app.data_collection) == 2 # markers still there, just hidden

catalogs_plugin.clear(hide_only=False)
catalogs_plugin.clear_table(hide_only=False)
assert not catalogs_plugin.results_available
assert len(imviz_helper.app.data_collection) == 1 # markers gone for good

assert imviz_helper.viewers['imviz-0']._obj.state.x_min == -0.5
assert imviz_helper.viewers['imviz-0']._obj.state.x_max == 9.5
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == -0.5
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 9.5
# Re-populate the table with a new search
out_tbl = catalogs_plugin.search()
assert len(out_tbl) > 0
# Ensure at least one row is selected before zooming
catalogs_plugin.table.selected_rows = [catalogs_plugin.table.items[0]]
assert len(catalogs_plugin.table.selected_rows) > 0

# Now zoom in
catalogs_plugin.vue_zoom_in()

assert imviz_helper.viewers['imviz-0']._obj.state.x_min == -49.99966
assert imviz_helper.viewers['imviz-0']._obj.state.x_max == 50.00034
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == -48.99999
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 51.00001
assert_allclose(imviz_helper.viewers['imviz-0']._obj.state.x_min, -49.99966, rtol=1e-6)
assert_allclose(imviz_helper.viewers['imviz-0']._obj.state.x_max, 50.00034, rtol=1e-6)
assert_allclose(imviz_helper.viewers['imviz-0']._obj.state.y_min, -48.99999, rtol=1e-6)
assert_allclose(imviz_helper.viewers['imviz-0']._obj.state.y_max, 51.00001, rtol=1e-6)
14 changes: 12 additions & 2 deletions jdaviz/core/template_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4691,12 +4691,20 @@ def float_precision(column, item):
return ''
elif isinstance(item, tuple) and np.all([np.isnan(i) for i in item]):
return ''

elif isinstance(item, float):
return float_precision(column, item)
elif isinstance(item, (list, tuple)):
return [float_precision(column, i) if isinstance(i, float) else i for i in item]

elif isinstance(item, (np.float32, np.float64)):
return float(item)
elif isinstance(item, u.Quantity):
pllim marked this conversation as resolved.
Show resolved Hide resolved
return item.value.tolist() if item.size > 1 else item.value
elif isinstance(item, np.bool_):
return bool(item)
elif isinstance(item, np.ndarray):
return item.tolist()
elif isinstance(item, tuple):
return tuple(json_safe(v) for v in item)
return item

if isinstance(item, QTable):
Expand Down Expand Up @@ -4738,6 +4746,8 @@ def clear_table(self):
Clear all entries/markers from the current table.
"""
self.items = []
self.selected_rows = []
self.selected_indices = []
self._qtable = None
self._plugin.session.hub.broadcast(PluginTableModifiedMessage(sender=self))

Expand Down
Loading