diff --git a/bhtom2/bhtom_observations/facilities/rem.py b/bhtom2/bhtom_observations/facilities/rem.py
index aa49c2fb..f9c33f3e 100644
--- a/bhtom2/bhtom_observations/facilities/rem.py
+++ b/bhtom2/bhtom_observations/facilities/rem.py
@@ -1,46 +1,137 @@
from django import forms
+from bhtom_base.bhtom_targets.models import Target
from crispy_forms.layout import Column, Div, HTML, Layout, Row, MultiWidgetField, Fieldset
from bhtom_base.bhtom_observations.facility import BaseManualObservationFacility, BaseManualObservationForm
from bhtom_base.bhtom_observations.widgets import FilterField
from bhtom_base.bhtom_observations.cadence import CadenceForm
+from django.core.mail import send_mail
+from django.conf import settings
+from datetime import datetime, timedelta
+
SUCCESSFUL_OBSERVING_STATES = ['COMPLETED']
FAILED_OBSERVING_STATES = ['WINDOW_EXPIRED', 'CANCELED', 'FAILURE_LIMIT_REACHED', 'NOT_ATTEMPTED']
TERMINAL_OBSERVING_STATES = SUCCESSFUL_OBSERVING_STATES + FAILED_OBSERVING_STATES
valid_instruments = ['ROS2']
-valid_filters = [['griz+J','griz+J'],['griz+H','griz+H'],['griz+Ks','griz+Ks']] #griz are always used in REM + infrared filter
+valid_filters = [['griz+H','griz+H'],['griz+J','griz+J'],['griz+Ks','griz+Ks'],['griz+z','griz+z']] #griz are always used in REM + infrared filter
+#z_IRCam, J_IRCam, H_IRCam, K_IRCam,
+ # H2_IRCam, JH_IRCam, JK_IRCam, HK_IRCam, JHK_IRCam, KH2_IRCam
+# z -is the other half of the z band, H2 was an experiment, don't use.
+#infrared filters are behind the filter wheel, only one at a time can be used.
+rem_proposals = settings.FACILITIES.get('REM', {}).get('proposalIDs', [])
+proposal_choices = [(str(proposal_id), description) for proposal_id, description in rem_proposals]
class REMPhotometricSequenceForm(BaseManualObservationForm):
- name = forms.CharField()
- start = forms.CharField(widget=forms.TextInput(attrs={'type': 'date'}))
- end = forms.CharField(required=False, widget=forms.TextInput(attrs={'type': 'date'}))
- observation_id = forms.CharField(required=False)
- observation_params = forms.CharField(required=False, widget=forms.Textarea(attrs={'type': 'json'}))
+# name = forms.CharField()
+
+ proposal_id = forms.ChoiceField(label="Proposal ID", choices=proposal_choices)
+
+ start = forms.CharField(label="Start date [UT]",widget=forms.TextInput(attrs={'type': 'date'}))
+ end = forms.CharField(label="End date [UT]",required=True, widget=forms.TextInput(attrs={'type': 'date'}))
+
+# observation_id = forms.CharField(required=False)
+# observation_params = forms.CharField(required=False, widget=forms.Textarea(attrs={'type': 'json'}))
+
+ exposure_time = forms.IntegerField(label="Exposure time Opt [s]",initial=60,help_text="in sec per optical exposure") # in sec
+ exposure_count = forms.IntegerField(initial=1, help_text="number of optical exposures per visit") # number of exposures per visit
+
+ exposure_time_ir = forms.IntegerField(label="Exposure time IR [s]",initial=10,help_text="in sec per IR exposure") # in sec
+ exposure_count_ir = forms.IntegerField(label="Number of NDIT in IR",initial=5,help_text="number of dithers in IR per exposure")
+
+ cadence = forms.FloatField(initial=1,help_text="days until next visit") # in days to next visit
+ filter = forms.ChoiceField(required=True, label='Filters', choices=valid_filters)
+
+ mag_init=99.
+ exposure_times = {}
+
+ def __init__(self, *args, **kwargs):
+ # Set default values for 'start', 'end', and 'name' in initial_data
+ initial_data = kwargs.get('initial', {})
+ current_date = datetime.now().strftime('%Y-%m-%d')
+ next_day = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d')
+
+ initial_data.setdefault('start', current_date)
+ initial_data.setdefault('end', next_day)
+ kwargs['initial'] = initial_data
+
+ super().__init__(*args, **kwargs)
+
+ target = Target.objects.get(id=self.initial.get('target_id'))
+ # initial_data.setdefault('name', f'BHTOM_REM_{target.name}')
+ # kwargs['initial'] = initial_data
+
+ # Precompute exposure time for each filter option
+ self.mag_init = target.mag_last
- exposure_time = forms.IntegerField()
- exposure_count = forms.IntegerField()
- cadence_in_days = forms.IntegerField() #in days
+ self.exposure_times = {}
- filters = forms.ChoiceField(required=True, label='Filters', choices=valid_filters)
+ instrument = "ROS2"
+ for filter_option, _ in valid_filters:
+ self.exposure_times[filter_option] = int(self.exposure_time_calculator(
+ mag=self.mag_init, filter_name=filter_option, instrument=instrument
+ )) #it has to be int - REM's requirement
+
+ # Set initial exposure time based on the first filter choice
+ first_filter = self.fields['filter'].initial or valid_filters[0][0]
+ initial_data.setdefault('exposure_time', self.exposure_times.get(first_filter))
+ kwargs['initial'] = initial_data
+ super().__init__(*args, **kwargs)
+
+ def clean(self):
+ cleaned_data = super().clean()
+ selected_filter = cleaned_data.get('filter')
+ if selected_filter:
+ # Set the computed exposure_time directly in the form field : TODO does not work
+ self.fields['exposure_time'].initial = self.exposure_times.get(selected_filter)
+ return cleaned_data
def layout(self):
+ # Display a table of filters and exposure times
+ filter_rows = "".join(f"
{filter_option} | {self.exposure_times.get(filter_option)} |
" for filter_option, _ in valid_filters)
+ mag = self.mag_init
return Div(
- Div('name', 'observation_id'),
+# Div('name'),
+ Div('proposal_id'),
Div(
Div('start', css_class='col'),
Div('end', css_class='col'),
css_class='form-row'
),
- Div('filters'),
+ Div('filter'),
+ HTML(f"Suggested exposure times for mag={mag}
Filter | Exposure Time |
{filter_rows}
"),
Div('exposure_time'),
Div('exposure_count'),
- Div('cadence_in_days'),
- Div('observation_params')
+ Div('exposure_time_ir'),
+ Div('exposure_count_ir'),
+ Div('cadence'),
)
+ # http://www.rem.inaf.it/?p=etc
+ # 60s for V=15mag gives S/N=100 in optical
+ #
+ def exposure_time_calculator(self, mag, filter_name, instrument):
+ if instrument not in valid_instruments:
+ return -1
+ if filter_name in [item for sublist in valid_filters for item in sublist]:
+ pass
+ else:
+ return -1
+
+ # Define a base exposure time for each filter
+ filter_base_exposure_times = {
+ 'griz+J': 60, # Example base exposure time for griz+J filter
+ 'griz+H': 60, # Example base exposure time for griz+H filter
+ 'griz+Ks': 60, # Example base exposure time for griz+Ks filter
+ 'griz+JHK': 60,
+ }
+
+ # Get the base exposure time for the selected filter
+ base_exposure_time = filter_base_exposure_times.get(filter_name, 60) # Default to 60s
+ adjusted_exposure_time = base_exposure_time * (10**((mag-15)/2.5))
+ return adjusted_exposure_time
class REM(BaseManualObservationFacility):
@@ -60,10 +151,6 @@ class REM(BaseManualObservationFacility):
def get_form(self, observation_type):
return self.observation_forms['PHOTOMETRIC_SEQUENCE']
- def submit_observation(self, observation_payload):
- # TODO: send mail
- return []
-
def validate_observation(self, observation_payload):
# TODO: ?
return []
@@ -84,39 +171,229 @@ def all_data_products(self, observation_record):
return []
-# # generate text of the email
-# def generate_email_text(params...):
+ def submit_observation(self, observation_payload):
+ #print(observation_payload)
+ # Retrieve target information using the target_id
+ target_id = observation_payload['target_id']
+ target = Target.objects.get(id=target_id)
+ # Extract target details
+ # removing spaces in target name (REM requirement)
+ target_name = target.name.replace(" ", "_") # or use .replace(" ", "")
+ ra = target.ra
+ dec = target.dec
-# #TODO: add S/N parameter (default = 100?)
- def exposure_time_calculator(self, mag, filter, instrument):
- if instrument not in valid_instruments:
- return -1
- if filter in [item for sublist in valid_filters for item in sublist]:
- pass
+ template = """
+[STARTREMOB]
+
+
+
+# Target data
+[TARGET]
+
+# Category
+TargetCategory: NotClassifiedSource
+# Available categories:
+# SCHGRB (#Scheduled GRB), Star, AGN, LMXRB (# LMXRB), HMXRB (# HMXRB),
+# FlaringStar, OpenCluster, GlobularCluster, Planetary Nebula,
+# Supernova Remnant, NotClassifiedSource, Galaxy, SoftGamma-RayRepeater
+# SolarSystemObject, ActiveSupernova (# Supernova still active), Nebula
+
+# no spaces are allowed in name
+TargetName: {target_name}
+
+# RA degrees.dddd, J2000
+RA: {ra}
+
+# DEC degrees.dddd, J2000
+DEC: {dec}
+
+# Equinox year.dd (this parameter is optional, else is 2000.0)
+Equinox: 2000.0
+
+# Optical camera data
+[ROSS]
+
+# 1 if optical data are desidered, else 0
+OptFlag: 1
+
+# seconds, total requested time must be less than 1 hour
+Exptime: {exptime}
+
+# Camera focus (optional)
+OptFocus: 0
+
+# CCD sensitivity (optional)
+# Sensitivity options:
+# CCDslowsens, CCDslowhigh, CCDfastsens, CCDfasthigh, CCDultrasens, CCDultrahigh
+OptSensitivity: CCDslowsens
+
+# number of exposures
+OptNInt: {expcount}
+
+
+# Infrared camera data
+
+[REMIR]
+
+# 1 if infrared data are desidered, else 0
+IRFlag: 1
+
+# detector integration time, seconds
+DIT: {exptime_ir}
+
+# filter
+IRFilter: {ir_filter}
+
+# number of exposure DITx5 long
+IRNInt: {expcount_ir}
+
+# Available filter are: z_IRCam, J_IRCam, H_IRCam, K_IRCam,
+# H2_IRCam, JH_IRCam, JK_IRCam, HK_IRCam, JHK_IRCam, KH2_IRCam
+
+
+# PI data
+
+[PI]
+
+# PI name, no spaces are allowed
+PIName: BHTOM
+
+# PI institute, no spaces are allowed
+PIInst: Warsaw
+
+# PI e-mail
+PIEmail: {email}
+
+
+
+# Observation data and access permission
+
+[DATA]
+
+# your proposal Id
+PropId: {proposal_id}
+
+# Password for OBS activation
+PassWd: REMObsPwd
+
+# Minimum airmass (this item is optional)
+MinAirmass: 0.0
+
+# Maximum airmass (this item is optional)
+MaxAirmass: 2.5
+
+# Minimum Julian Date (this item is optional, 0 means no constraints)
+MinJD: {start_jd}
+
+# Maximum Julian Date (this item is optional, 0 means no constraints)
+MaxJD: {end_jd}
+
+# Strict starting Julian Date (this item is optional, 0 means no constraints)
+StrictJD: 0.
+
+# Maximum Moon fraction (this item is optional)
+MaxMoonFraction: 1.0
+
+# Periodical target? (this item is optional, 0 means no periodicity)
+PeriodicalTarget: 1
+
+# Period (this item is optional, days)
+Period: {cadence}
+
+# Priority (this item is optional, 0 is the maximum priority, then 1, 2, etc.)
+Priority: 1
+
+
+
+# Jitter data (optional)
+# LW: NO JITTER
+#[JITTER]
+
+# Number of jittered OBs to create
+#JitteredOBs: 0
+
+# Min jittering radius (arcmin)
+#MinJitteringRadius: 0.1
+
+# Max jittering radius (arcmin)
+#MaxJitteringRadius: 2.0
+
+[ENDREMOB]
+ """
+
+ email = settings.FACILITIES.get('REM', {}).get('email', ['wyrzykow@gmail.com'])
+ # Get start and end dates from observation_payload
+ start_date_str = observation_payload['params']['start']
+ end_date_str = observation_payload['params']['end']
+
+ # Convert to Julian Dates
+ start_jd = self.date_to_julian_date(start_date_str)
+ end_jd = self.date_to_julian_date(end_date_str)
+
+ selected_filter = observation_payload['params']['filter']
+ # Parse the selected filter to get the infrared part
+ filter_parts = selected_filter.split('+')
+ if len(filter_parts) > 1:
+ filter_ir = f"{filter_parts[1]}_IRCam"
else:
- return -1
+ # Handle case if there is no "+" in the filter (optional)
+ filter_ir = "JHK_IRCam" # default value
- base_exposure_time = 100
- adjusted_exposure_time = base_exposure_time * (10**((mag-14)/2.5))
- return adjusted_exposure_time
+ # Format the template
+ filled_template = template.format(
+ target_name=target_name,
+ ra=ra,
+ dec=dec,
+ proposal_id = observation_payload['params']['proposal_id'],
+ email=email,
+ cadence = observation_payload['params']['cadence'],
+ exptime = observation_payload['params']['exposure_time'],
+ exptime_ir = observation_payload['params']['exposure_time_ir'],
+ expcount = observation_payload['params']['exposure_count'],
+ expcount_ir = observation_payload['params']['exposure_count_ir'],
+ start_jd=start_jd,
+ end_jd=end_jd,
+ ir_filter=filter_ir
- def return_valid_filters():
- return valid_filters
-
- def return_valid_instruments():
- return valid_instruments
-
- def compute_for_all(self):
- text_outputs = []
- ff=valid_filters
+ )
+
+ # Now, the filled_template contains the complete formatted text
+ # print(filled_template)
+
+ recipient_email = ["remobs@www.rem.inaf.it","wyrzykow@gmail.com"]
+ # Send the email
+ self.send_template_email(filled_template, recipient_email)
+ return []
+
+
+
+ def date_to_julian_date(self,date_str):
+ """
+ Convert a date string in 'YYYY-MM-DD' format to Julian Date.
+ """
+ # Parse the date string into a datetime object
+ dt = datetime.strptime(date_str, "%Y-%m-%d")
- mag=15
- instrument='ROS2'
- for filter_pair in ff:
- f = filter_pair[0] # use only the first filter in each pair
- exposure_time = self.exposure_time_calculator(mag, f, instrument)
- text_output = f"{f}: {exposure_time} seconds"
- text_outputs.append(text_output)
-
- print(text_outputs)
\ No newline at end of file
+ # Calculate Julian Date
+ julian_date = dt.toordinal() + 1721424.5 + (dt.hour + dt.minute / 60 + dt.second / 3600) / 24
+ return julian_date
+
+ #recipients can be a single string or a list of strings
+ def send_template_email(self,filled_template, recipients):
+ if isinstance(recipients, str):
+ recipients = [recipients] # Convert single email to list
+
+ subject = "REM_OBS" #don't change!
+ message = filled_template # The filled template string
+ from_email = settings.EMAIL_HOST_USER # From email address
+ recipient_list = recipients
+
+ # Send the email
+ send_mail(
+ subject,
+ message,
+ from_email,
+ recipient_list,
+ fail_silently=False, # Set to True in production to avoid raising errors
+ )
\ No newline at end of file
diff --git a/settings/settings.py b/settings/settings.py
index 2fa6b0cc..332ced4f 100644
--- a/settings/settings.py
+++ b/settings/settings.py
@@ -469,6 +469,10 @@ def generate_name_tuple(data_source: DataSource) -> tuple:
'portal_url': 'https://observe.lco.global',
'api_key': PROPOSALS_API_KEYS['LCO'],
},
+ 'REM': {
+ 'proposalIDs': ((50823, "ORP-PI:Mariusz Gromadzki"),(50712,"CNTAC-PI:Rene Mendez") ),
+ 'email': "wyrzykow@gmail.com",
+ },
}
TOM_FACILITY_CLASSES = [