Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ecfc7fc
First draft of form to display a target selection for a specific faci…
rachel3834 Feb 10, 2024
70d5d9b
First working version of target selection table plus docs
rachel3834 Feb 12, 2024
070a1b6
Removing files committed accidentally
rachel3834 Feb 12, 2024
9e1b042
Fixed code blocks in docs
rachel3834 Feb 12, 2024
011537e
Maximizing figure width
rachel3834 Feb 12, 2024
05d149b
Fixed broken link
rachel3834 Feb 12, 2024
ce5aa95
Fixed broken link
rachel3834 Feb 12, 2024
f4e677f
Fixed indentation
rachel3834 Feb 12, 2024
f8d76e5
Added indicator of which file to edit
rachel3834 Feb 12, 2024
7feef59
Merge branch 'dev' into feature/facility_target_selection
jchate6 Feb 12, 2024
22d37bc
fix linting errors
jchate6 Feb 12, 2024
c2fc5b6
Resolved conflicts with current dev
rachel3834 Sep 2, 2025
ff4b5fb
Resolving commits from review
rachel3834 Sep 2, 2025
e233cb8
Revised view and template to provide links to target detail page
rachel3834 Sep 2, 2025
8076c7c
Fixed linting errors
rachel3834 Sep 2, 2025
729454e
Merge branch 'dev' into feature/facility_target_selection
jchate6 Sep 3, 2025
7b6b779
Merge branch 'dev' into feature/facility_target_selection
jchate6 Sep 4, 2025
d2d35fc
Refactored target facility selection view
rachel3834 Sep 11, 2025
ad489a9
Fixed conflicts
rachel3834 Sep 11, 2025
c7dbb9a
Refactored FormView and templates to handle pagination
rachel3834 Sep 12, 2025
377514d
Removing superfluous partial
rachel3834 Sep 12, 2025
39bd323
Refactored to paginate the targets rather than the visibility results…
rachel3834 Sep 13, 2025
2a10aec
Removed section referring to adding facilities
rachel3834 Sep 13, 2025
f0626f3
Fixed linting issues
rachel3834 Sep 13, 2025
9a85621
Merge branch 'dev' of https://github.com/TOMToolkit/tom_base into fea…
rachel3834 Oct 8, 2025
da983bc
Updated to dev
rachel3834 Oct 8, 2025
9f32a32
Added general facilities to the target selection form and visibility …
rachel3834 Oct 8, 2025
2f8f96d
Linting updates
rachel3834 Oct 9, 2025
d06ba1e
Added documentation of new general facilities table
rachel3834 Oct 9, 2025
068da97
Merge branch 'dev' into feature/facility_target_selection
jchate6 Oct 10, 2025
4843a55
Merge branch 'dev' into feature/facility_target_selection
jchate6 Oct 13, 2025
be29224
Updated docs
rachel3834 Nov 19, 2025
6159b55
Refactored following review; moved resolution of observatory to get_s…
rachel3834 Nov 19, 2025
ca09072
Updated for linting
rachel3834 Nov 19, 2025
dfb19d2
Merge branch 'dev' of https://github.com/TOMToolkit/tom_base into fea…
rachel3834 Nov 19, 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
Binary file added docs/observing/add_facility_form_admin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/observing/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ allow your TOM to submit observation requests to observatories.
:doc:`Facility Modules <../api/tom_observations/facilities>` - Take a look at the supported facilities.

:doc:`Observation Views <../api/tom_observations/views>` - Familiarize yourself with the available Observation Views.

:doc:`Selecting Targets <selecting_targets_for_facility>` - Display a selection of targets for a specific observing facility.
220 changes: 220 additions & 0 deletions docs/observing/selecting_targets_for_facility.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
Selecting Targets for an Observing Facility
===========================================

During observing runs, particularly at manually- or remotely-operated telescope
facilities, it can often be very useful to display a selection of targets to be
observed on a particular night. This needs to take into account target visibility from
the telescope site, as well as any prioritization of targets that the team have made.

TOMs provide support for this through the Target Selection option under the Target menu
in the main navigation bar.

.. image:: target_selection_menu_option.png
:alt: Menu option for Target Selection view

Observers can select the telescope facility that they are observing from using the form
provided, indicating the date of the observing run. The selected targets will be draw
from a predefined Target Grouping, which users can chose from the pulldown menu.

The TOM will evaluate the visibility of the selected sidereal targets for the telescope on the
night in question, and the resulting table will include all objects with a minimum
airmass less than 2.0.

.. image:: target_selection_table_default.png
:alt: Default table output for target selection

Customizing the Selected Targets Table
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default, this table will include the essential parameters necessary to point a
telescope at the target, but it can be easily extended to add further information.

The columns of the table can be configured by editing the TOM's ``settings.py`` file.
Additional parameters can be defined for each target by adding dictionary definitions
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems confusing to me. I think we should separate the EXTRA_FIELDS conversation from the SELECTION_EXTRA_FIELDS description.

Instead, let's link them to docs/targets/target_fields.rst where they can learn how to add other fields, and then tell them how to use SELECTION_EXTRA_FIELDS

to the ``EXTRA_FIELDS`` list, as shown in the example below:

.. code-block:: python

# settings.py
EXTRA_FIELDS = [
{'name': 'Mag_now', 'type': 'number'},
{'name': 'Priority1', 'type': 'number'},
{'name': 'Priority2', 'type': 'string'}
]
SELECTION_EXTRA_FIELDS = [
'Mag_now',
'Priority1',
'Priority2',
]

In this example, we have added ``EXTRA_FIELDS`` named ``Mag_now``, ``Priority1``
and ``Priority2``, which the user can set either by editing each Target's parameters
directly, or programmatically. Having done so, we can add those ``EXTRA_FIELDS`` to
the target selection table by adding the parameter names to the list of ``SELECTION_EXTRA_FIELDS``.
This produces the table displayed below.

.. image:: target_selection_table_extra_fields.png
:alt: Target Selection table with additional columns added

.. code:: python
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this section sill relevant? At the very least it feels non-sequitur

Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably all be included in or a reference to docs/observing/observation_module.rst


# casleo.py

# DEFAULT:
try:
EXAMPLE_MANUAL_SETTINGS = settings.FACILITIES['EXAMPLE_MANUAL']
except KeyError:
EXAMPLE_MANUAL_SETTINGS = {
}

EXAMPLE_SITES = {
'Example Manual Facility': {
'sitecode': 'Example',
'latitude': 0.0,
'longitude': 0.0,
'elevation': 0.0
},
}
EXAMPLE_TERMINAL_OBSERVING_STATES = ['Completed']

# UPDATED TO:
try:
CASLEO_SETTINGS = settings.FACILITIES['CASLEO']
except KeyError:
CASLEO_SETTINGS = {
}

CASLEO_SITES = {
'El Leoncito': {
'sitecode': 'CASLEO',
'latitude': -31.7986,
'longitude': -69.2956,
'elevation': 2483.0
},
}
TERMINAL_OBSERVING_STATES = ['Completed']

Then we give the facility class a distinctive name:

.. code:: python

# casleo.py

# DEFAULT:
class ExampleManualFacility(BaseManualObservationFacility):
"""
"""

name = 'Example'
observation_types = [('OBSERVATION', 'Manual Observation')]

# UPDATED TO:
class CASLEOFacility(BaseManualObservationFacility):
"""
"""

name = 'El Leoncito'
observation_types = [('OBSERVATION', 'Manual Observation')]

We also need to update the reference to the list of possible end states of observing requests.
This list can be expanded for telescopes that are programmatically accessible, but it can be left
with the default list for manual facilities.

.. code:: python

# casleo.py

# DEFAULT:
def get_terminal_observing_states(self):
"""
Returns the states for which an observation is not expected
to change.
"""
return EXAMPLE_TERMINAL_OBSERVING_STATES


# UPDATED TO:
def get_terminal_observing_states(self):
"""
Returns the states for which an observation is not expected
to change.
"""
return TERMINAL_OBSERVING_STATES


Lastly, we need to make sure that the method to fetch the information on observing sites refers to the
list of dictionaries that we specified above.

.. code:: python

# casleo.py

# DEFAULT:
def get_observing_sites(self):
"""
Return a list of dictionaries that contain the information
necessary to be used in the planning (visibility) tool. The
list should contain dictionaries each that contain sitecode,
latitude, longitude and elevation.
"""
return EXAMPLE_SITES


# UPDATED TO:
def get_observing_sites(self):
"""
Return a list of dictionaries that contain the information
necessary to be used in the planning (visibility) tool. The
list should contain dictionaries each that contain sitecode,
latitude, longitude and elevation.
"""
return CASLEO_SITES


The new facility is now ready. To make sure that the TOM includes it,
we simply need to add it to our TOM's list of facilities in the ``settings.py`` file:


.. code-block:: python

# settings.py
TOM_FACILITY_CLASSES = [
'tom_observations.facilities.lco.LCOFacility',
'tom_observations.facilities.gemini.GEMFacility',
'tom_observations.facilities.soar.SOARFacility',
'facilities.casleo.CASLEOFacility',
]


Returning to the target selection form, the new observatory now appears as
an option in the Observatory pulldown menu.


.. image:: target_selection_table_new_facility.png
:alt: Target selection table with new telescope facility added


Adding Facilities to the Observing Facilities Table
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you would like to record a telescope facility for the purposes of observation planning, but don't want to
add code for a new facility module, you can add it to the TOM's table of observing facilities.

Currently, this can be done by the TOM's administrator, by navigating to the TOM's built-in admin interface.
This page can be reached by adding ``/admin/`` to the end of the TOM's root URL in your browser's navigation bar, e.g.:

.. code-block:: html
> https://demo.lco.global/admin/
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs a blank line and an indent.


Scrolling down the list of database tables, you will find ``Facilitys`` under the tables from the ``tom_observations`` app.
Copy link
Contributor

Choose a reason for hiding this comment

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

Oof, no, let's fix this.

go to tom_observations.models.py

inside Facility immediately after you define the model parameters, add

class Meta:
        verbose_name_plural = "facilities"

Clicking on this link will take you to a list of telescopes recorded in the TOM's database. Note that this list
is distinct (and does not include) telescopes already known to the TOM through installed facility modules.

You can record new telescopes to this table using the admin interface's ``Add Facility`` button; this will present you
with the following form:

.. image:: add_facility_form_admin.png
:alt: Form to add a new telescope facility

Fill in the form and click ``save``. Now if you return to your TOM's usual interface, and navigate to the ``Target Selection``
page, the facility you added will appear in the list of facilities for which visibilities can be calculated.
Binary file added docs/observing/target_selection_menu_option.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/observing/target_selection_table_default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tom_common/templates/tom_common/navbar_content.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<div class="dropdown-menu">
<a class="dropdown-item" href="{% url 'targets:list' %}">Targets</a>
<a class="dropdown-item" href="{% url 'targets:targetgrouping' %}">Target Grouping</a>
<a class="dropdown-item" href="{% url 'targets:target-selection' %}">Target Selection</a>
</div>
</li>
<li class="nav-item dropdown">
Expand Down
9 changes: 8 additions & 1 deletion tom_observations/templatetags/observation_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,14 @@ def observation_plan(target, facility=None, length=2, interval=60, airmass_limit
start_time = datetime.now()
end_time = start_time + timedelta(days=length)

visibility_data = get_sidereal_visibility(target, start_time, end_time, interval, airmass_limit, facility)
visibility_data = get_sidereal_visibility(
target,
start_time,
end_time,
interval,
airmass_limit,
facility_name=facility
)
i = 0
plot_data = []
for site, data in visibility_data.items():
Expand Down
100 changes: 76 additions & 24 deletions tom_observations/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@
import logging

from tom_observations import facility
from tom_observations.models import Facility as GeneralFacility

logger = logging.getLogger(__name__)


def get_sidereal_visibility(target, start_time, end_time, interval, airmass_limit, observation_facility=None):
def get_sidereal_visibility(
target,
start_time,
end_time,
interval,
airmass_limit,
facility_name=None
):
"""
Uses astroplan to calculate the airmass for a sidereal target
for each given interval between the start and end times.
Expand All @@ -34,9 +42,10 @@ def get_sidereal_visibility(target, start_time, end_time, interval, airmass_limi
:param airmass_limit: maximum acceptable airmass for the resulting calculations
:type airmass_limit: int

:param observation_facility: observing facility for which to calculate the airmass. None indicates all available
facilities.
:type observation_facility: BaseObservationFacility
:param facility_name: name string of a declared observing facility class OR general facility,
for which to calculate the airmass.
None indicates all available facilities.
:type facility_name: string

:returns: A dictionary containing the airmass data for each site. The dict keys consist of the site name prepended
with the observing facility. The values are the airmass data, structured as an array containing two arrays. The
Expand All @@ -57,35 +66,66 @@ def get_sidereal_visibility(target, start_time, end_time, interval, airmass_limi
if airmass_limit is None:
airmass_limit = 10

if observation_facility is None:
facilities = facility.get_service_classes()
else:
facilities = [observation_facility]
# Build list of observers, including all sites for a given facility
observers = {}

# First check whether a general facility was selected. This allows us to calculate for a single facility
# if that's what the user asked for
observation_facility = None
general_facility = None
if facility_name is not None:
qs = GeneralFacility.objects.filter(full_name=facility_name, location='ground')

# If the observatory refers to a general facility, build a list of observers from that DB entry.
# The zero-length class_facilities list indicates that these are not required
if qs.count() > 0:
general_facility = qs[0]
observation_facility = None
class_facilities = []
observers[f'{general_facility.short_name}'] = Observer(longitude=general_facility.longitude * units.deg,
latitude=general_facility.latitude * units.deg,
elevation=general_facility.elevation * units.m)
else:
general_facility = None
observation_facility = facility.get_service_class(facility_name)

# If the user did not select a general facility, but selected a facility module,
# this function should calculate for that facility alone.
# If the user selected neither a general facility nor a facility module, calculate for the default
# list of facility modules
if observation_facility is None and general_facility is None:
class_facilities = [clazz for name, clazz in facility.get_service_classes().items()]
elif observation_facility and general_facility is None:
class_facilities = [observation_facility]

# Add observers to the list for the facility modules, including all sites for a given facility
for observing_facility_class in class_facilities:
sites = observing_facility_class().get_observing_sites()
for site, site_details in sites.items():
observers[f'({observing_facility_class.name}) {site}'] = Observer(
longitude=site_details.get('longitude') * units.deg,
latitude=site_details.get('latitude') * units.deg,
elevation=site_details.get('elevation') * units.m
)

body = FixedTarget(name=target.name, coord=SkyCoord(target.ra, target.dec, unit='deg'))

visibility = {}
sun, time_range = get_astroplan_sun_and_time(start_time, end_time, interval)
for observing_facility in facilities:
observing_facility_class = facility.get_service_class(observing_facility)
sites = observing_facility_class().get_observing_sites()
for site, site_details in sites.items():
observer = Observer(longitude=site_details.get('longitude')*units.deg,
latitude=site_details.get('latitude')*units.deg,
elevation=site_details.get('elevation')*units.m)
for observer_name, observer in observers.items():
sun_alt = observer.altaz(time_range, sun).alt
obj_airmass = observer.altaz(time_range, body).secz

sun_alt = observer.altaz(time_range, sun).alt
obj_airmass = observer.altaz(time_range, body).secz
bad_indices = np.argwhere(
(obj_airmass >= airmass_limit) |
(obj_airmass <= 1) |
(sun_alt > -18*units.deg) # between astronomical twilights, i.e. sun is up
)

bad_indices = np.argwhere(
(obj_airmass >= airmass_limit) |
(obj_airmass <= 1) |
(sun_alt > -18*units.deg) # between astronomical twilights, i.e. sun is up
)
obj_airmass = [None if i in bad_indices else float(airmass) for i, airmass in enumerate(obj_airmass)]

obj_airmass = [None if i in bad_indices else float(airmass) for i, airmass in enumerate(obj_airmass)]
visibility[observer_name] = (time_range.datetime, obj_airmass)

visibility[f'({observing_facility}) {site}'] = (time_range.datetime, obj_airmass)
return visibility


Expand Down Expand Up @@ -132,3 +172,15 @@ def get_astroplan_sun_and_time(start_time, end_time, interval):
sun = get_sun(time_range)

return sun, time_range


def get_facilities():
"""
Function to return a complete list of all available observing facilities, including
both facility classes and general facilities.
"""

facilities = [(x, x) for x in facility.get_service_classes()]
facilities += [(x.full_name, x.full_name) for x in GeneralFacility.objects.all()]

return facilities
1 change: 1 addition & 0 deletions tom_setup/templates/tom_setup/settings.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ HARVESTERS = {
# {'name': 'discovery_date', 'type': 'datetime'}
# ]
EXTRA_FIELDS = []
SELECTION_EXTRA_FIELDS = []

# Authentication strategy can either be LOCKED (required login for all views)
# or READ_ONLY (read only access to views)
Expand Down
Loading
Loading