diff --git a/CHANGELOG.md b/CHANGELOG.md index 32ae3ae..b7317b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add selected sources export as `astropy.Table` list with property `selected_objects` (#100) - Add function `get_view_as_fits` to export the view as a `astropy.io.fits.HDUList` (#86) - Add function `save_view_as_image` to save the view as an image file (#108) +- Add option to `add_table` to display the error of the sources (#110) ### Deprecated diff --git a/examples/04_Importing_Tables.ipynb b/examples/04_Importing_Tables.ipynb index cd5df9a..c30fa7b 100644 --- a/examples/04_Importing_Tables.ipynb +++ b/examples/04_Importing_Tables.ipynb @@ -99,45 +99,61 @@ "source": [ "aladin.add_table(t)" ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Display the table with the error approximations\n", + "First, let's re-use the table from the Simbad query and specify the error columns and units, and the ellipse enclosed probability.\n", + "### Note\n", + "Ipyaladin only support oriented ellipse and radial errors for the moment.\n", + "Radial error can be specified with the `r` column in the table." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "error_dict = {\n", + " \"smaj\": {\"col\": \"coo_err_maj\", \"unit\": u.mas},\n", + " \"smin\": {\"col\": \"coo_err_min\", \"unit\": u.mas},\n", + " \"pa\": {\"col\": \"coo_err_angle\", \"unit\": u.deg},\n", + " # Let's the default value for the ellipse enclosed probability\n", + " \"ellipse_enclosed_probability\": 0.39347,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And then add the table to the Aladin widget" + ] }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": false, - "sideBar": false, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": false, - "toc_window_display": false + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "aladin2 = Aladin(target=\"M1\", fov=0.2)\n", + "aladin2" + ] }, - "vscode": { - "interpreter": { - "hash": "85bb43f988bdbdc027a50b6d744a62eda8a76617af1f4f9b115d38242716dbac" - } + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "aladin2.add_table(table, error_dict=error_dict)" + ] } - }, + ], + "metadata": {}, "nbformat": 4, "nbformat_minor": 4 } diff --git a/js/models/message_handler.js b/js/models/message_handler.js index b3def3a..df6279b 100644 --- a/js/models/message_handler.js +++ b/js/models/message_handler.js @@ -136,13 +136,28 @@ export default class MessageHandler { handleAddTable(msg, buffers) { const options = convertOptionNamesToCamelCase(msg["options"] || {}); + const errorDict = options.errorDict; const buffer = buffers[0].buffer; const decoder = new TextDecoder("utf-8"); const blob = new Blob([decoder.decode(buffer)]); const url = URL.createObjectURL(blob); + options.onClick = "showTable"; + if (errorDict) + options.shape = (s) => { + if (errorDict["pa"] && s.data[errorDict["pa"]["col"]]) + return A.ellipse( + s.ra, + s.dec, + s.data[errorDict["smaj"]["col"]], + s.data[errorDict["smin"]["col"]], + s.data[errorDict["pa"]["col"]], + ); + if (errorDict["r"] && s.data[errorDict["r"]["col"]]) + return A.circle(s.ra, s.dec, s.data[errorDict["r"]["col"]]); + }; A.catalogFromURL( url, - Object.assign(options, { onClick: "showTable" }), + options, (catalog) => { this.aladin.addCatalog(catalog); }, diff --git a/src/ipyaladin/widget.py b/src/ipyaladin/widget.py index 2d06681..35aba22 100644 --- a/src/ipyaladin/widget.py +++ b/src/ipyaladin/widget.py @@ -5,9 +5,11 @@ It allows to display astronomical images and catalogs in an interactive way. """ +import math from collections.abc import Callable import io import pathlib +from copy import deepcopy from json import JSONDecodeError from pathlib import Path from typing import ClassVar, Dict, Final, List, Optional, Tuple, Union @@ -19,6 +21,7 @@ from astropy.table import Table from astropy.io import fits as astropy_fits from astropy.io.fits import HDUList +import astropy.units as u from astropy.wcs import WCS import numpy as np import traitlets @@ -633,6 +636,72 @@ def add_moc_from_dict( moc_options = {} self.add_moc(moc_dict, **moc_options) + def _convert_table_units( + self, table: Union[QTable, Table], error_dict: Dict, unit_to: u.Unit = u.deg + ) -> Union[QTable, Table]: + """Convert the units of a table according to the error_dict. + + Parameters + ---------- + table : astropy.table.table.QTable or astropy.table.table.Table + The table to convert. + error_dict : dict + The dictionary containing the error specifications. + unit_to : astropy.units.Unit + The unit to convert to. Default is degrees. + + Returns + ------- + astropy.table.table.QTable or astropy.table.table.Table + The table with the units converted. + + """ + table = table.copy() + for error_spec in error_dict.values(): + if not isinstance(error_spec, dict): + continue + col_name = error_spec["col"] + unit_from = error_spec["unit"] + table[col_name].unit = unit_from + table[col_name] = table[col_name].astype(float) + for row in table: + if row[col_name] != "--" and not np.isnan(row[col_name]): + row[col_name] = ( + Angle(row[col_name], unit=unit_from).to(unit_to).value + ) + table[col_name].unit = unit_to + + return table + + def _update_ellipse_enclosed_probability( + self, table: Union[QTable, Table], error_dict: Dict + ) -> Union[QTable, Table]: + """Update the table according to the ellipse_enclosed_probability. + + Parameters + ---------- + table : astropy.table.table.QTable or astropy.table.table.Table + The table to update. + error_dict : dict + The dictionary containing the error specifications. + + Returns + ------- + astropy.table.table.QTable or astropy.table.table.Table + The updated table. + + """ + table = table.copy() + r = math.sqrt(-2 * math.log(1 - error_dict["ellipse_enclosed_probability"])) + impacted_keys = {"smin", "smaj", "r"} + for key in impacted_keys: + if not error_dict.get(key): + continue + # Multiply table column by r + table[error_dict[key]["col"]] = table[error_dict[key]["col"]] * r + + return table + def add_table(self, table: Union[QTable, Table], **table_options: any) -> None: """Load a table into the widget. @@ -646,6 +715,19 @@ def add_table(self, table: Union[QTable, Table], **table_options: any) -> None: `_ """ + if table_options.get("error_dict"): + table_options["error_dict"] = deepcopy(table_options["error_dict"]) + table = self._convert_table_units(table, table_options["error_dict"]) + # if dict contains ellipse_enclosed_probability, update the table + if table_options["error_dict"].get("ellipse_enclosed_probability"): + table = self._update_ellipse_enclosed_probability( + table, table_options["error_dict"] + ) + # Remove unit sub-key for all the keys + for key in table_options["error_dict"]: + if key == "ellipse_enclosed_probability": + continue + table_options["error_dict"][key].pop("unit") table_bytes = io.BytesIO() table.write(table_bytes, format="votable") self.send(