Commit d9da346b42ee86e4b7b148f9d2b60db6bd547344

Authored by Alexis Koralewski
2 parents c6c12930 df477bca
Exists in dev

Merge branch 'dev' of https://gitlab.irap.omp.eu/pyros-irap/pyros into dev

Showing 1 changed file with 543 additions and 96 deletions   Show diff stats
src/core/pyros_django/scheduling/A_Scheduler.py
1 1 #!/usr/bin/env python3
  2 +#
  3 +# To launch this agent from the root of Pyros:
  4 +#
  5 +# Linux console:
  6 +# cd /srv/develop/pyros/docker
  7 +# ./PYROS_DOCKER_START.sh
  8 +#
  9 +# Launch from Power Shell:
  10 +# To go from docker to Powershell: pyros_user@ORION:~/app$ exit (or Ctrl+d)
  11 +# Prompt is now PS ...>
  12 +# cd \srv\develop\pyros
  13 +# .\PYROS -t new-start -o tnc -fg -a A_Scheduler
  14 +#
  15 +# Launch from docker:
  16 +# To go from Powershell to docker: PS ...> .\PYROS_DOCKER_SHELL
  17 +# Prompt is now pyros_user@ORION:~/app$
  18 +# ./PYROS -t new-start -o tnc -fg -a A_Scheduler
  19 +# ./PYROS -d -t new-start -o tnc -fg -a A_Scheduler
  20 +#
  21 +# ---------------------------------------------------
2 22  
3 23 import sys
4   -##import utils.Logger as L
5   -#import threading #, multiprocessing, os
6 24 import time
  25 +import argparse
  26 +import os
  27 +import pickle
  28 +import socket
  29 +pwd = os.environ['PROJECT_ROOT_PATH']
  30 +if pwd not in sys.path:
  31 + sys.path.append(pwd)
7 32  
8   -#from django.db import transaction
9   -#from common.models import Command
  33 +short_paths = ['src', 'src/core/pyros_django']
  34 +for short_path in short_paths:
  35 + path = os.path.join(pwd, short_path)
  36 + if path not in sys.path:
  37 + sys.path.insert(0, path)
10 38  
11   -sys.path.append("..")
12   -sys.path.append("../../../..")
13   -from src.core.pyros_django.majordome.agent.Agent import Agent, build_agent, log
14   -
15   -# PM 20190416 recycle code
16   -#from common.models import *
  39 +from src.core.pyros_django.majordome.agent.Agent import Agent, build_agent, log, parse_args
17 40 from seq_submit.models import Sequence
  41 +from user_mgmt.models import Period, ScientificProgram, SP_Period
  42 +# = Specials
  43 +import glob
  44 +import shutil
  45 +import guitastro
  46 +import datetime
  47 +from decimal import Decimal
  48 +import zoneinfo
  49 +import numpy as np
18 50  
19   -##log = L.setupLogger("AgentXTaskLogger", "AgentX")
20   -
21   -
22   -
23   -class A_Scheduler(Agent):
24   -
  51 +class AgentScheduler(Agent):
25 52  
26   - # FOR TEST ONLY
27   - # Run this agent in simulator mode
28   - TEST_MODE = False
29   - # Run the assertion tests at the end
30   - TEST_WITH_FINAL_TEST = True
31   - TEST_MAX_DURATION_SEC = None
32   - #TEST_MAX_DURATION_SEC = 120
33   -
34   - # PM 20190416 fucking config path starting: /home/patrick/Dev/PYROS/start_agent.py agentM
35   - ##_path_data = 'config'
36   - _path_data = 'config/old_config'
  53 + DPRINT = False
  54 +
  55 + # - status of the sequence after schedule computation
  56 + SEQ_NOT_PROCESSED = 0
  57 + SEQ_SCHEDULED = 1
  58 + SEQ_SCHEDULED_OVER_QUOTA = 2
  59 + SEQ_REJECTED_NO_QUOTA_ENOUGH = -1
  60 + SEQ_REJECTED_NO_SLOT_AVAILABLE = -2
  61 +
  62 + # - enum of the matrix line
  63 + SEQ_K = 0
  64 + SEQ_SEQ_ID = 1
  65 + SEQ_KOBS0 = 2
  66 + SEQ_SP_ID = 3
  67 + SEQ_PRIORITY = 4
  68 + SEQ_DURATION = 5
  69 + SEQ_STATUS = 6
  70 + NB_SEQ = 7
  71 +
  72 + # - All possible running states
  73 + RUNNING_NOTHING = 0
  74 + RUNNING_SCHEDULE_PROCESSING = 1
37 75  
38   - log.debug("PLC instanciated")
  76 + _AGENT_SPECIFIC_COMMANDS = {
  77 + # Format : โ€œcmd_nameโ€ : (timeout, exec_mode)
  78 + "do_compute_schedule_1" : (60, Agent.EXEC_MODE.SEQUENTIAL, ''),
  79 + "do_create_seq_1" : (60, Agent.EXEC_MODE.SEQUENTIAL, ''),
  80 + }
39 81  
40   - _AGENT_SPECIFIC_COMMANDS = [
41   - #"do_replan",
42   - "do_stop_replan",
  82 + # Scenario to be executed
  83 + # "self do_stop_current_processing"
  84 + # AgentCmd.CMD_STATUS_CODE.CMD_EXECUTED
  85 + _TEST_COMMANDS_LIST = [
  86 + # Format : ("self cmd_name cmd_args", timeout, "expected_result", expected_status),
  87 + (True, "self do_create_seq_1 6", 200, '', Agent.CMD_STATUS.CMD_EXECUTED),
  88 + (True, "self do_stop asap", 500, "STOPPING", Agent.CMD_STATUS.CMD_EXECUTED),
43 89 ]
44 90  
45   -
46 91 """
47 92 =================================================================
48   - FUNCTIONS RUN INSIDE MAIN THREAD
  93 + Methods running inside main thread
49 94 =================================================================
50 95 """
51   - # old config
52   - # @override
53   - #def __init__(self, name:str=None, config_filename=None, RUN_IN_THREAD=True):
54   - # def __init__(self, config_filename=None, RUN_IN_THREAD=True):
55   - # ##if name is None: name = self.__class__.__name__
56   - # super().__init__(config_filename, RUN_IN_THREAD)
57   -
58   - # new config (obsconfig)
59   - #def __init__(self, name:str=None, RUN_IN_THREAD=True):
60   - def __init__(self, name:str=None):
  96 + def __init__(self, name:str=None,simulated_computer=None):
61 97 if name is None:
62 98 name = self.__class__.__name__
63   - super().__init__()
64   - #super().__init__(RUN_IN_THREAD)
  99 + super().__init__(simulated_computer=simulated_computer)
65 100  
66   - # @override
67 101 def _init(self):
68 102 super()._init()
69   - log.debug("end init()")
70   - # --- Set the mode according the startmode value
71   - ##agent_alias = self.__class__.__name__
72   - ##self.set_mode_from_config(agent_alias)
  103 + log.debug("end super init()")
  104 + log.info(f"self.TEST_MODE = {self.TEST_MODE}")
73 105  
74   - '''
75   - # @override
76   - def load_config(self):
77   - super().load_config()
78   - '''
  106 + # === Get the config object
  107 + self.config = self._oc['config']
  108 + self.pconfig = self._oc['pyros_config']
  109 + # === Get agent_alias
  110 + hostname = socket.gethostname()
  111 + log.info(f"{hostname=}")
  112 + log.info(f"{self.name=}")
  113 + agent_alias = self.config.get_agent_real_name(self.name, hostname)
  114 + log.info(f"{agent_alias=}")
79 115  
80   - '''
81   - # @override
82   - def update_survey(self):
83   - super().update_survey()
84   - '''
  116 + # === Get self._home of current unit
  117 + self._home = self.config.getHome()
  118 + home = guitastro.Home(self._home)
85 119  
86   - '''
87   - # @override
88   - def get_next_command(self):
89   - return super().get_next_command()
90   - '''
  120 + self._fn = self.pconfig.fn
  121 + self._fn.pathnaming("PyROS.seq.1")
91 122  
92   - # @override
93   - def do_log(self):
94   - super().do_log()
95   -
96   - def replan_sequences(self):
97   - print("\n start of sequences (re-)planning...\n")
98   - time.sleep(5)
99   - sequences = Sequence.objects.filter(status="TBP")
100   - print("List of sequences to be planned :")
101   - for seq in sequences:
102   - print('-', seq.name, '('+seq.status+') :')
103   - print('-- albums : ', seq.albums.all())
104   - print('-- plans : ')
105   - for album in seq.albums.all():
106   - print(album)
107   - for plan in album.plans.all():
108   - print('plan id', plan.id)
109   -
110   - print("\n ...end of sequences (re-)planning\n")
  123 + # === Set longitude to ima object to generate the night yyyymmdd and subdirectories yyyy/mm/dd
  124 + longitude = home.longitude
  125 + log.info(f"{longitude=}")
  126 + self._fn.longitude(longitude)
  127 +
  128 + # === Status of routine processing
  129 + self._routine_running = self.RUNNING_NOTHING
  130 + log.debug("end init()")
111 131  
112 132 # Note : called by _routine_process() in Agent
113 133 # @override
114 134 def _routine_process_iter_start_body(self):
115   - print("The Observatory configuration :")
116   - self.show_config()
117 135 log.debug("in routine_process_before_body()")
118   - #self.replan_sequences()
119 136  
120 137 # Note : called by _routine_process() in Agent
121 138 # @override
122 139 def _routine_process_iter_end_body(self):
123   - print("The Observatory configuration :")
124   - #self.show_config()
125 140 log.debug("in routine_process_after_body()")
126   - self.replan_sequences()
  141 + # TODO EP est-ce utile ?
  142 + if self._routine_running == self.RUNNING_NOTHING:
  143 + # Get files to process
  144 + # - Thread TODO
  145 + self._routine_running = self.RUNNING_SCHEDULE_PROCESSING
  146 + self.do_compute_schedule_1()
127 147  
128   - '''
129   - # @override
130   - def exec_specific_cmd_end(self, cmd:Command, from_thread=True):
131   - super().exec_specific_cmd_end(cmd, from_thread)
132   - '''
  148 + """
  149 + =================================================================
  150 + Methods of specific commands
  151 + =================================================================
  152 + """
  153 +
  154 + def do_ccreate_seq_1(self, nb_seq:int):
  155 + """Create sequences to debug
  156 + """
  157 + self._create_seq_1(nb_seq)
  158 +
  159 + def do_compute_schedule_1(self):
  160 + """Compute a schedule
  161 +
  162 + According the current time, select the night directory.
  163 + List the *.p file list (.p for sequences)
  164 + Read the *.p, *.f file contents (.f for ephemeris)
  165 + Compute the schedule
  166 +
  167 + Output is a matrix to unpack in the database.
  168 + Each line of the matrix is a sequence
  169 + Columns are defined by the enum SEQ_* (see the python code itself).
  170 +
  171 + """
  172 + self._compute_schedule_1()
  173 +
  174 + """
  175 + =================================================================
  176 + Methods called by commands or routine. Overload these methods
  177 + =================================================================
  178 + # ---
  179 + # osp = ScientificProgram.objects.get(id=scientific_program_id)
  180 + # --- ospperiod is the SP object
  181 + # ospperiod = SP_Period.objects.get(period = period_id, scientific_program = osp)
  182 + # print(f"dir(ospperiod)={dir(ospperiod)}")
  183 + # dir(spperiod)=['DoesNotExist',
  184 + # 'IS_VALID', 'IS_VALID_ACCEPTED', 'IS_VALID_REJECTED',
  185 + # 'MultipleObjectsReturned', 'SP_Period_Guests', 'SP_Period_Users',
  186 + # 'STATUSES', 'STATUSES_ACCEPTED', 'STATUSES_DRAFT',
  187 + # 'STATUSES_EVALUATED', 'STATUSES_REJECTED', 'STATUSES_SUBMITTED',
  188 + # 'VISIBILITY_CHOICES', 'VISIBILITY_NO', 'VISIBILITY_YES',
  189 + # 'VOTES', 'VOTES_NO', 'VOTES_TO_DISCUSS', 'VOTES_YES',
  190 + # 'can_submit_sequence', 'check', 'clean', 'clean_fields',
  191 + # 'date_error_message', 'delete', 'from_db', 'full_clean',
  192 + # 'get_constraints', 'get_deferred_fields', 'get_is_valid_display',
  193 + # 'get_public_visibility_display', 'get_status_display',
  194 + # 'get_vote_referee1_display', 'get_vote_referee2_display',
  195 + # 'id', 'is_currently_active', 'is_valid', 'objects',
  196 + # 'over_quota_duration', 'over_quota_duration_allocated',
  197 + # 'over_quota_duration_remaining', 'period', 'period_id',
  198 + # 'pk', 'prepare_database_save', 'priority', 'public_visibility',
  199 + # 'quota_allocated', 'quota_minimal', 'quota_nominal',
  200 + # 'quota_remaining', 'reason_referee1', 'reason_referee2',
  201 + # 'referee1', 'referee1_id', 'referee2', 'referee2_id',
  202 + # 'refresh_from_db', 'save', 'save_base', 'scientific_program',
  203 + # 'scientific_program_id', 'serializable_value', 'status', 'token',
  204 + # 'token_allocated', 'token_remaining', 'unique_error_message',
  205 + # 'validate_constraints', 'validate_unique', 'vote_referee1',
  206 + # 'vote_referee2'
  207 + """
  208 +
  209 + def _compute_schedule_1(self):
  210 + """Simple scheduler based on selection-insertion one state algorithm.
  211 +
  212 + Quotas are available only fo the night.
  213 + No token.
  214 + """
  215 + t0 = time.time()
  216 + #self.DPRINT = True
  217 + # --- Get the incoming directory of the night
  218 + info = self.get_infos()
  219 + rootdir = info['rootdir']
  220 + subdir = info['subdir']
  221 + # --- Build the wildcard to list the sequences
  222 + wildcard = os.path.join(rootdir, subdir, "*.p")
  223 + self.dprint(f"{wildcard=}")
  224 + # --- List the sequences from the incoming directory
  225 + seqfiles = glob.glob(wildcard)
  226 + log.info(f"{len(seqfiles)} file sequences to process")
  227 + # --- Initialize the schedule
  228 + schedule = np.zeros(86400, dtype=int) -1
  229 + schedule_binary = np.ones(86400, dtype=int)
  230 + # ===================================================================
  231 + # --- Loop over the sequences of the night to extract useful infos
  232 + # ===================================================================
  233 + self.dprint("\n" + "="*70 + f"\n=== Read {len(seqfiles)} sequence files of the night {info['night']}\n" + "="*70 + "\n")
  234 + sequence_infos = []
  235 + # --- Initialize the list of scientific_program_ids
  236 + scientific_program_ids = []
  237 + kseq = 0
  238 + for seqfile in seqfiles:
  239 + # --- seqfile = sequence file name
  240 + kseq += 1
  241 + sequence_info = {}
  242 + sequence_info['id'] = -1 # TBD replace by idseq of the database
  243 + sequence_info['seqfile'] = seqfile
  244 + sequence_info['error'] = ""
  245 + sequence_info['kobs0'] = -1
  246 + # --- ephfile = ephemeris file name
  247 + ephfile = os.path.splitext(seqfile)[0] + ".f"
  248 + # --- If ephemeris file exists, read files
  249 + if os.path.exists(ephfile):
  250 + self.dprint(f"Read file {seqfile}")
  251 + # --- seq_info = sequence dictionary
  252 + # --- eph_info = ephemeris dictionary
  253 + seq_info = pickle.load(open(seqfile,"rb"))
  254 + #print("="*20 + "\n" + f"{seq_info=}")
  255 + eph_info = pickle.load(open(ephfile,"rb"))
  256 + #print("="*20 + "\n" + f"{eph_info=}")
  257 + # ---
  258 + param = self._fn.naming_get(seqfile)
  259 + sequence_info['id'] = int(param['id_seq'])
  260 + # --- scientific_program_id is an integer
  261 + scientific_program_id = seq_info['sequence']['scientific_program']
  262 + # --- Dictionary of informations about the sequence
  263 + sequence_info['seq_dico'] = seq_info # useful for duration
  264 + # --- Search the last time when the start of the sequence is observable (visibility > 0)
  265 + visibility_duration = eph_info['visibility_duration']
  266 + kobss = np.where(visibility_duration > 0)
  267 + kobss = list(kobss[0])
  268 + if len(kobss) == 0:
  269 + self.dprint(" Sequence has no visibility")
  270 + sequence_info['error'] = f"Sequence has no visibility_duration"
  271 + sequence_infos.append(sequence_info)
  272 + continue
  273 + kobs0 = kobss[0]
  274 + sequence_info['kobs0'] = kobs0
  275 + sequence_info['visibility'] = eph_info['visibility'] # total slots
  276 + sequence_info['visibility_duration'] = visibility_duration # total slots - duration
  277 + sequence_info['duration'] = seq_info['sequence']['duration']
  278 + sequence_info['scientific_program_id'] = scientific_program_id
  279 + self.dprint(f" {scientific_program_id=} range to start={len(kobss)}")
  280 + if scientific_program_id not in scientific_program_ids:
  281 + scientific_program_ids.append(scientific_program_id)
  282 + else:
  283 + sequence_info['error'] = f"File {ephfile} not exists"
  284 + sequence_infos.append(sequence_info)
  285 +
  286 + # ===================================================================
  287 + # --- Get informations of priority and quota from scientific programs
  288 + # ===================================================================
  289 + self.dprint("\n" + "="*70 + f"\n=== Get information from {len(scientific_program_ids)} scientific programs of the night\n" + "="*70 + "\n")
  290 + scientific_program_infos = {}
  291 + period_id = info['operiod'].id
  292 + self.dprint(f"{scientific_program_ids=}")
  293 + for scientific_program_id in scientific_program_ids:
  294 + scientific_program_info = {}
  295 + try:
  296 + osp = ScientificProgram.objects.get(id=scientific_program_id)
  297 + # --- ospperiod is the SP object
  298 + ospperiod = SP_Period.objects.get(period = period_id, scientific_program = osp)
  299 + scientific_program_info['priority'] = ospperiod.priority
  300 + scientific_program_info['over_quota_duration'] = ospperiod.over_quota_duration
  301 + scientific_program_info['over_quota_duration_allocated'] = ospperiod.over_quota_duration_allocated
  302 + scientific_program_info['over_quota_duration_remaining'] = ospperiod.over_quota_duration_remaining
  303 + scientific_program_info['quota_allocated'] = ospperiod.quota_allocated
  304 + scientific_program_info['quota_minimal'] = ospperiod.quota_minimal
  305 + scientific_program_info['quota_nominal'] = ospperiod.quota_nominal
  306 + scientific_program_info['quota_remaining'] = ospperiod.quota_remaining
  307 + scientific_program_info['token_allocated'] = ospperiod.token_allocated
  308 + scientific_program_info['token_remaining'] = ospperiod.token_allocated
  309 + except:
  310 + # --- simulation
  311 + scientific_program_info['priority'] = 0
  312 + if scientific_program_info['priority'] == 0:
  313 + # --- simulation
  314 + priority = 50 + scientific_program_id*5
  315 + scientific_program_info['priority'] = priority
  316 + scientific_program_info['quota_allocated'] = 12000
  317 + scientific_program_info['quota_remaining'] = 12000
  318 + scientific_program_infos[str(scientific_program_id)] = scientific_program_info
  319 + self.dprint(f"{scientific_program_id=} priority={scientific_program_info['priority']} quota={scientific_program_info['quota_remaining']}")
  320 +
  321 + # ===================================================================
  322 + # --- Build the numpy matrix seqs to make rapid computations
  323 + # ===================================================================
  324 + self.dprint("\n" + "="*70 + f"\n=== Build the matrix for scheduling {len(sequence_infos)} sequences\n" + "="*70 + "\n")
  325 + self.dprint("Order ID_seq K_start ID_sp Priority Duration Status\n")
  326 + nseq = len(sequence_infos)
  327 + if nseq == 0:
  328 + self._routine_running = self.RUNNING_NOTHING
  329 + return
  330 + seqs = np.zeros((nseq, self.NB_SEQ), dtype=int)
  331 + k = 0
  332 + for sequence_info in sequence_infos:
  333 + if 'scientific_program_id' not in sequence_info.keys():
  334 + self.dprint(f"No scientific program for ID sequence {sequence_info['id']}")
  335 + continue
  336 + scientific_program_id = sequence_info['scientific_program_id']
  337 + scientific_program_info = scientific_program_infos[str(scientific_program_id)]
  338 + priority = scientific_program_info['priority']
  339 + # Order of the following list refers to the enum
  340 + seq = [ k, sequence_info['id'], sequence_info['kobs0'], scientific_program_id, priority, int(np.ceil(sequence_info['duration'])), self.SEQ_NOT_PROCESSED ]
  341 + self.dprint(f"{seq=}")
  342 + seqs[k] = seq
  343 + k += 1
  344 + seqs = seqs[:k]
  345 + # --- Save the matrix sequence
  346 + #print(f"{seqs=}")
  347 + fpathname = os.path.join(rootdir, subdir, "scheduler_seq_matrix1.txt")
  348 + np.savetxt(fpathname, seqs)
  349 +
  350 + # ===================================================================
  351 + # --- Compute the matrix seq_sorteds (priority and chronology)
  352 + # ===================================================================
  353 + self.dprint("\n" + "="*70 + "\n=== Sort the matrix for scheduling by priority and chronology\n" + "="*70 + "\n")
  354 + # --- Sort the matrix sequence: priority=SEQ_PRIORITY (decreasing -1) and then chronology=SEQ_KOBS0 (increasing +1)
  355 + seq_sorteds = seqs[np.lexsort(([1,-1]*seqs[:,[self.SEQ_KOBS0, self.SEQ_PRIORITY]]).T)]
  356 + # --- Save the matrix sequence
  357 + self.dprint("Order ID_seq K_start ID_sp Priority Duration Status\n")
  358 + self.dprint(f"{seq_sorteds=}")
  359 + fpathname = os.path.join(rootdir, subdir, "scheduler_seq_matrix2.txt")
  360 + np.savetxt(fpathname, seq_sorteds)
  361 +
  362 + # ===================================================================
  363 + # --- Insert sequences in the schedule. Respecting priority and quota
  364 + # ===================================================================
  365 + self.dprint("\n" + "="*70 + "\n=== Insertion of the sequences in the schedule respecting priority and quota\n" + "="*70 + "\n")
  366 + for seq in seq_sorteds:
  367 + # --- Unpack the sequence
  368 + k, sequence_id, kobs0, scientific_program_id, priority, duration, seq_status = seq
  369 +
  370 + # --- Get the quota remaining of the scientific program
  371 + quota_remaining = scientific_program_infos[str(scientific_program_id)]['quota_remaining']
  372 + self.dprint('-'*70 + "\n" + f"Process {sequence_id=} {kobs0=} {duration=} sp_id={scientific_program_id} {quota_remaining=}")
  373 +
  374 + # --- Verify if duration < quota_remaining
  375 + if duration > quota_remaining:
  376 + # --- No remaining quota to insert this sequence
  377 + self.dprint(f"{sequence_id=} cannot be inserted because no quota enough")
  378 + seqs[k][self.SEQ_STATUS] = self.SEQ_REJECTED_NO_QUOTA_ENOUGH
  379 + continue
  380 +
  381 + # --- Compute the remaining visibility and list (k1s) of the best observation start
  382 + # =0 if not possible to start observation
  383 + # =value with the highest value for the best observation start
  384 +
  385 + # --- Visibility*schedule_binary are transformed into binary
  386 + sequence_info = sequence_infos[k]
  387 + vis_binarys = sequence_info['visibility'].copy() * schedule_binary
  388 + vis_binarys[vis_binarys > 0] = 1
  389 +
  390 + # --- Cumulative sum + offset by -duration to prepare the start_binary computation
  391 + obs_starts = np.cumsum(vis_binarys)
  392 + obs_ends = obs_starts.copy()
  393 + obs_ends[0:-duration] = obs_ends[duration:]
  394 + obs_ends[-duration:] = 0
  395 +
  396 + # --- Difference and binarisation to get starts with duration
  397 + start_binary = obs_ends - obs_starts
  398 + start_binary[start_binary < duration] = 0
  399 + start_binary[start_binary == duration] = 1
  400 +
  401 + # --- Compute the remaining visibility (float)
  402 + remaining_visibility = sequence_info['visibility'] * start_binary
  403 +
  404 + # --- Check the remaining visibility
  405 + if np.sum(remaining_visibility) == 0:
  406 + # --- No remaining slot to insert this sequence
  407 + self.dprint(f"{sequence_id=} cannot inserted because no more slots available")
  408 + seqs[k][self.SEQ_STATUS] = self.SEQ_REJECTED_NO_SLOT_AVAILABLE
  409 + continue
  410 +
  411 + # --- From the index of the highest value of remaining visibility to the index of the lowest value of remaining visibility
  412 + k1s = np.flip(np.argsort(remaining_visibility))
  413 + self.dprint(f"{k1s=} => Start elevation {sequence_info['visibility'][k1s[0]]:+.2f}")
  414 +
  415 + # --- Get k1 as the highest value of remaining visibility
  416 + k1 = k1s[0]
  417 + k2 = k1 + duration
  418 + self.dprint(f"{k} : {sequence_id=} {scientific_program_id=} {priority=} inserted in the slot {k1=} {k2=} (remaining {quota_remaining - duration} s)")
  419 +
  420 + # --- Update the seqs matrix
  421 + seqs[k][self.SEQ_STATUS] = self.SEQ_SCHEDULED
  422 +
  423 + # --- Update the schedule arrays
  424 + schedule[k1:k2] = sequence_id
  425 + schedule_binary[k1:k2] = 0
  426 +
  427 + # --- Update the scientific program dict
  428 + quota_remaining -= duration
  429 + scientific_program_infos[str(scientific_program_id)]['quota_remaining'] = quota_remaining
  430 +
  431 + # ===================================================================
  432 + # --- Insert sequences in the schedule. Respecting priority but over quota
  433 + # ===================================================================
  434 + # self.dprint("\n" + "="*70 + "\n=== Insertion of the sequences in the schedule respecting priority but over quota\n" + "="*70 + "\n")
  435 + # TBD
  436 + # where are remaining free slots
  437 + # scan sequences to insert in these free slots
  438 +
  439 + # ===================================================================
  440 + # --- Save the schedule
  441 + # ===================================================================
  442 + self.dprint("\n" + "="*70 + "\n=== Save the schedule\n" + "="*70 + "\n")
  443 + self.dprint("Order ID_seq K_start ID_sp Priority Duration Status\n")
  444 + self.dprint(f"{seqs=}")
  445 + fpathname = os.path.join(rootdir, subdir, "scheduler_schedule.txt")
  446 + np.savetxt(fpathname, np.array([schedule, schedule_binary]).T)
  447 + # --- Update the running state
  448 + self._routine_running = self.RUNNING_NOTHING
  449 + print(f"_compute_schedule_1 finished in {time.time() - t0:.2f} seconds")
133 450  
  451 + def _create_seq_1(self, nb_seq):
  452 + t0 = time.time()
  453 + self.dprint("Debut _create_seq_1")
  454 + seq_template = {'sequence': {'id': 4, 'start_expo_pref': 'IMMEDIATE', 'pyros_user': 2, 'scientific_program': 1, 'name': 'seq_20230628T102140', 'desc': None, 'last_modified_by': 2, 'is_alert': False, 'status': 'TBP', 'with_drift': False, 'priority': None, 'analysis_method': None, 'moon_min': None, 'alt_min': None, 'type': None, 'img_current': None, 'img_total': None, 'not_obs': False, 'obsolete': False, 'processing': False, 'flag': None, 'period': 1, 'start_date': datetime.datetime(2023, 6, 28, 10, 21, 40, tzinfo=zoneinfo.ZoneInfo(key='UTC')), 'end_date': datetime.datetime(2023, 6, 28, 10, 21, 40, 999640, tzinfo=datetime.timezone.utc), 'jd1': Decimal('0E-8'), 'jd2': Decimal('0E-8'), 'tolerance_before': '1s', 'tolerance_after': '1min', 'duration': -1.0, 'overhead': Decimal('0E-8'), 'submitted': False, 'config_attributes': {'tolerance_before': '1s', 'tolerance_after': '1min', 'target': 'RADEC 0H10M -15D', 'conformation': 'WIDE', 'layout': 'Altogether'}, 'ra': None, 'dec': None, 'complete': True, 'night_id': '20230627'}, 'albums': {'Altogether': {'plans': [{'id': 4, 'album': 4, 'duration': 0.0, 'nb_images': 1, 'config_attributes': {'binnings': {'binxy': [1, 1], 'readouttime': 6}, 'exposuretime': 1.0}, 'complete': True}]}}}
  455 + info = self.get_infos()
  456 + rootdir = info['rootdir']
  457 + subdir = info['subdir']
  458 + # --- Prepare ephemeris object
  459 + # TBD duskelev a parametrer from obsconfig (yml)
  460 + duskelev = -7
  461 + eph = guitastro.Ephemeris()
  462 + eph.set_home(self.config.getHome())
  463 + #print("Debut _create_seq_1 SUN")
  464 + ephem_sun = eph.target2night("sun", info['night'], None, None)
  465 + #print("Debut _create_seq_1 MOON")
  466 + ephem_moon = eph.target2night("moon", info['night'], None, None)
  467 + # --- Horizon (TBD get from config)
  468 + #print("Debut _create_seq_1 Horizon")
  469 + hor = guitastro.Horizon(eph.home)
  470 + hor.horizon_altaz = [ [0,0], [360,0] ]
  471 + # --- Delete all existing *.p and *.f files in the night directory
  472 + fn_param = {
  473 + "period" : f"{info['period_id']}",
  474 + "version": "1",
  475 + "unit": self.config.unit_name,
  476 + "date": info['night'],
  477 + "id_seq": 0
  478 + }
  479 + fname = self._fn.naming_set(fn_param)
  480 + self.dprint(f":: {fname=}")
  481 + seq_file = self._fn.join(fname)
  482 + path_night = os.path.dirname(seq_file)
  483 + cards = ['*.p', '*.f']
  484 + for card in cards:
  485 + wildcard = os.path.join(path_night, card)
  486 + seq_dfiles = glob.glob(wildcard)
  487 + #print(f"::: {seq_dfiles=}")
  488 + for seq_dfile in seq_dfiles:
  489 + #print(f":::.1 : os.remove {seq_dfile=}")
  490 + os.remove(seq_dfile)
  491 + # ---
  492 + for k in range(nb_seq):
  493 + #print("B"*20 + f" {info['operiod'].id} {info['night']} {k}")
  494 + time.sleep(1)
  495 + seq = seq_template.copy()
  496 + seq['sequence']['period'] = info['operiod'].id # int
  497 + seq['sequence']['night_id'] = info['night'] # str
  498 + seq['sequence']['config_attributes']['target'] = k # int
  499 + # ---
  500 + start_expo_pref = "BESTELEV" #"IMMEDIATE"
  501 + scientific_program = int(k/2)
  502 + start_date = datetime.datetime(2023, 6, 28, 10, 21, 40)
  503 + end_date = datetime.datetime(2023, 6, 28, 10, 21, 40, 999640, tzinfo=datetime.timezone.utc)
  504 + jd1 = Decimal('0E-8')
  505 + jd2 = Decimal('0E-8')
  506 + tolerance_before = '1s'
  507 + tolerance_after = '1min'
  508 + duration = 3000.0
  509 + target = f"RADEC {k}h {10+2*k}d"
  510 + # ---
  511 + seq['sequence']['start_expo_pref'] = start_expo_pref
  512 + seq['sequence']['scientific_program'] = scientific_program
  513 + seq['sequence']['start_date'] = start_date
  514 + seq['sequence']['end_date'] = end_date
  515 + seq['sequence']['jd1'] = jd1
  516 + seq['sequence']['jd2'] = jd2
  517 + seq['sequence']['tolerance_before'] = tolerance_before
  518 + seq['sequence']['tolerance_after'] = tolerance_after
  519 + seq['sequence']['duration'] = duration
  520 + seq['sequence']['config_attributes']['target'] = target
  521 + # --- Build the path and file name of the sequence file
  522 + fn_param["id_seq"] = k
  523 + #print(f"{k} : {self._fn.fcontext=}")
  524 + self._fn.fname = self._fn.naming_set(fn_param)
  525 + #print(f"{k} : {self._fn.fname=}")
  526 + seq_file = self._fn.join(self._fn.fname)
  527 + #print(f"{k} : {seq_file=}")
  528 + # --- Build the path and file name of the ephemeris file
  529 + eph_file = f"{seq_file[:-2]}.f"
  530 + # --- Create directory if it doesn't exist
  531 + #print(f"{k} : {seq_file=}")
  532 + os.makedirs(os.path.dirname(seq_file), exist_ok=True)
  533 + # --- Compute the ephemeris of the sequence and manage errors
  534 + #print(f"{k} : TRY")
  535 + errors = []
  536 + try:
  537 + # TODO remplacer les none par les fichiers pickle de ephem_sun & ephem_moon
  538 + ephem = eph.target2night(seq["sequence"]["config_attributes"]["target"], info['night'], ephem_sun, ephem_moon, preference=seq['sequence']['start_expo_pref'], duskelev=duskelev, horizon=hor, duration=duration)
  539 + except ValueError:
  540 + errors.append("Target value is not valid")
  541 + except guitastro.ephemeris.EphemerisException as ephemException:
  542 + errors.append(str(ephemException))
  543 + if len(errors) == 0 and np.sum(ephem["visibility"]) == 0 :
  544 + errors.append("Target is not visible.")
  545 + if len(errors) == 0:
  546 + pickle.dump(ephem, open(eph_file,"wb"))
  547 + pickle.dump(seq, open(seq_file,"wb"))
  548 + #print(f"{errors=}")
  549 + #print("C"*20)
  550 + print(f"_create_seq_1 finished in {time.time() - t0:.2f} seconds")
  551 +
  552 + def load_sequence(self):
  553 + sequence = ""
  554 + return sequence
134 555  
  556 + def get_infos(self):
  557 + rootdir = self._fn.rootdir
  558 + operiod = Period.objects.exploitation_period()
  559 + if operiod == None:
  560 + log.info("No period valid in the database")
  561 + self._routine_running = self.RUNNING_NOTHING
  562 + return
  563 + period_id = str(operiod.id)
  564 + if len(str(operiod.id)) < 3:
  565 + while len(period_id) < 3:
  566 + period_id = "0" + period_id
  567 + period_id = "P" + period_id
  568 + night_id = self._fn.date2night("now")
  569 + subdir = os.path.join(period_id, night_id)
  570 + dico = {}
  571 + dico['rootdir'] = rootdir
  572 + dico['subdir'] = subdir
  573 + dico['operiod'] = operiod # object
  574 + dico['period_id'] = period_id # str formated (P000)
  575 + dico['night'] = night_id # str (YYYYMMDD)
  576 + return dico
  577 +
  578 + def dprint(self, *args, **kwargs):
  579 + if self.DPRINT:
  580 + print(*args, **kwargs)
  581 +
135 582 if __name__ == "__main__":
136 583  
137   - agent = build_agent(A_Scheduler)
  584 + agent = build_agent(AgentScheduler)
138 585 print(agent)
139 586 agent.run()
... ...