lstosa 0.10.13__py3-none-any.whl → 0.10.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ import logging
2
+ from argparse import ArgumentParser
3
+ from datetime import datetime, timedelta
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+ from astropy.table import Table
8
+
9
+ from osa.configs import options
10
+ from osa.configs.config import cfg
11
+ from osa.nightsummary.nightsummary import run_summary_table
12
+ from osa.paths import DEFAULT_CFG
13
+ from osa.scripts.sequencer_webmaker import html_content
14
+ from osa.utils.utils import date_to_dir, date_to_iso
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ def valid_date(string):
20
+ """Check if the string is a valid date and return a datetime object."""
21
+ return datetime.strptime(string, "%Y-%m-%d")
22
+
23
+
24
+ common_parser = ArgumentParser(add_help=False)
25
+ common_parser.add_argument(
26
+ "-c",
27
+ "--config",
28
+ type=Path,
29
+ default=DEFAULT_CFG,
30
+ help="Use specific config file [default configs/sequencer.cfg]",
31
+ )
32
+ common_parser.add_argument(
33
+ "-d",
34
+ "--date",
35
+ help="Date of the start of the night in ISO format (YYYY-MM-DD). Defaults to yesterday",
36
+ type=valid_date,
37
+ )
38
+
39
+
40
+ def check_gainsel_jobs_runwise(date: datetime, run_id: int) -> bool:
41
+ """Search for failed jobs in the log directory."""
42
+ base_dir = Path(cfg.get("LST1", "BASE"))
43
+ flat_date = date_to_dir(date)
44
+ log_dir = base_dir / f"R0G/log/{flat_date}"
45
+ history_files = log_dir.glob(f"gain_selection_{run_id:05d}.????.history")
46
+
47
+ success_subruns = 0
48
+ failed_subruns = 0
49
+ pending_subruns = 0
50
+
51
+ for file in history_files:
52
+ if file.read_text() != "":
53
+ gainsel_rc = file.read_text().splitlines()[-1][-1]
54
+
55
+ if gainsel_rc == "1":
56
+ failed_subruns += 1
57
+
58
+ elif gainsel_rc == "0":
59
+ success_subruns += 1
60
+
61
+ else:
62
+ pending_subruns += 1
63
+
64
+ return {"pending": pending_subruns, "success": success_subruns, "failed": failed_subruns}
65
+
66
+
67
+ def check_failed_jobs(date: datetime) -> pd.DataFrame:
68
+ """Search for failed jobs in the log directory."""
69
+ summary_table = run_summary_table(date)
70
+ data_runs = summary_table[summary_table["run_type"] == "DATA"]
71
+
72
+ gainsel_status_dict = {}
73
+ for run in data_runs:
74
+ run_id = run["run_id"]
75
+ gainsel_job_status = check_gainsel_jobs_runwise(date, run_id)
76
+ gainsel_status_dict[run_id] = gainsel_job_status
77
+
78
+ gainsel_df = pd.DataFrame(gainsel_status_dict.values(), index=gainsel_status_dict.keys())
79
+ gainsel_df.reset_index(inplace=True)
80
+ gainsel_df.rename(columns={"index": "run_id"}, inplace=True)
81
+ summary_table = summary_table.to_pandas()
82
+
83
+ final_table = pd.merge(summary_table, gainsel_df, on="run_id")[
84
+ [
85
+ "run_id",
86
+ "n_subruns",
87
+ "pending",
88
+ "success",
89
+ "failed",
90
+ ]
91
+ ]
92
+
93
+ def determine_status(row):
94
+ if row["failed"] > 0:
95
+ return "FAILED"
96
+ elif row["pending"] == row["n_subruns"]:
97
+ return "PENDING"
98
+ elif row["success"] == row["n_subruns"]:
99
+ return "COMPLETED"
100
+ elif row["pending"] > 0:
101
+ return "RUNNING"
102
+ else:
103
+ return "NOT STARTED"
104
+
105
+ final_table["GainSel%"] = round(final_table["success"] * 100 / final_table["n_subruns"])
106
+ final_table["GainSelStatus"] = final_table.apply(determine_status, axis=1)
107
+
108
+ return final_table
109
+
110
+
111
+ def main():
112
+ """Produce the html file with the processing OSA Gain Selection status.
113
+
114
+ It creates an HTML file osa_gainsel_status_YYYY-MM-DD.html
115
+ """
116
+ args = ArgumentParser(
117
+ description=(
118
+ "Script to create an HTML file with the gain selection status "
119
+ "(osa_gainsel_status_YYYY-MM-DD.html)"
120
+ ),
121
+ parents=[common_parser],
122
+ ).parse_args()
123
+
124
+ if args.date:
125
+ flat_date = date_to_dir(args.date)
126
+ options.date = args.date
127
+
128
+ else:
129
+ # yesterday by default
130
+ yesterday = datetime.now() - timedelta(days=1)
131
+ options.date = yesterday
132
+ flat_date = date_to_dir(yesterday)
133
+
134
+ date = date_to_iso(options.date)
135
+ run_summary_directory = Path(cfg.get("LST1", "RUN_SUMMARY_DIR"))
136
+ run_summary_file = run_summary_directory / f"RunSummary_{flat_date}.ecsv"
137
+
138
+ gain_selection_web_directory = Path(cfg.get("LST1", "GAIN_SELECTION_WEB_DIR"))
139
+ gain_selection_web_directory.mkdir(parents=True, exist_ok=True)
140
+ html_file = gain_selection_web_directory / f"osa_gainsel_status_{date}.html"
141
+
142
+ # Create and save the HTML file
143
+ if not run_summary_file.is_file() or len(Table.read(run_summary_file)["run_id"]) == 0:
144
+ content = "<p>No data found</p>"
145
+ log.warning(f"No data found for date {date}, creating an empty HTML file.")
146
+
147
+ else:
148
+ # Get the table with the gain selection check report in HTML format:
149
+ table_gain_selection_jobs = check_failed_jobs(options.date)
150
+ content = table_gain_selection_jobs.to_html(justify="left")
151
+
152
+ html_file.write_text(html_content(content, date, "OSA Gain Selection"))
153
+ log.info(f"Created HTML file {html_file}")
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()
@@ -22,7 +22,7 @@ def number_of_pending_jobs():
22
22
 
23
23
 
24
24
  def run_script(
25
- script: str, date, config: Path, no_dl2: bool, no_calib: bool, simulate: bool, force: bool
25
+ script: str, date, config: Path, no_dl2: bool, no_gainsel: bool, no_calib: bool, simulate: bool, force: bool
26
26
  ):
27
27
  """Run the sequencer for a given date."""
28
28
  osa_config = Path(config).resolve()
@@ -32,6 +32,9 @@ def run_script(
32
32
  if no_dl2:
33
33
  cmd.append("--no-dl2")
34
34
 
35
+ if no_gainsel:
36
+ cmd.append("--no-gainsel")
37
+
35
38
  if no_calib:
36
39
  cmd.append("--no-calib")
37
40
 
@@ -64,6 +67,7 @@ def get_list_of_dates(dates_file):
64
67
 
65
68
  @click.command()
66
69
  @click.option("--no-dl2", is_flag=True, help="Do not run the DL2 step.")
70
+ @click.option("--no-gainsel", is_flag=True, help="Do not require gain selection to be finished.")
67
71
  @click.option("--no-calib", is_flag=True, help="Do not run the calibration step.")
68
72
  @click.option("-s", "--simulate", is_flag=True, help="Activate simulation mode.")
69
73
  @click.option("-f", "--force", is_flag=True, help="Force the autocloser to close the day.")
@@ -83,6 +87,7 @@ def main(
83
87
  dates_file: Path = None,
84
88
  config: Path = DEFAULT_CFG,
85
89
  no_dl2: bool = False,
90
+ no_gainsel: bool = False,
86
91
  no_calib: bool = False,
87
92
  simulate: bool = False,
88
93
  force: bool = False,
@@ -102,7 +107,7 @@ def main(
102
107
  # Avoid running jobs while it is still night time
103
108
  wait_for_daytime()
104
109
 
105
- run_script(script, date, config, no_dl2, no_calib, simulate, force)
110
+ run_script(script, date, config, no_dl2, no_gainsel, no_calib, simulate, force)
106
111
  log.info("Waiting 1 minute to launch the process for the next date...\n")
107
112
  time.sleep(60)
108
113
 
osa/scripts/sequencer.py CHANGED
@@ -9,6 +9,7 @@ import logging
9
9
  import os
10
10
  import sys
11
11
  from decimal import Decimal
12
+ import datetime
12
13
 
13
14
  from osa import osadb
14
15
  from osa.configs import options
@@ -28,7 +29,7 @@ from osa.paths import analysis_path
28
29
  from osa.report import start
29
30
  from osa.utils.cliopts import sequencer_cli_parsing
30
31
  from osa.utils.logging import myLogger
31
- from osa.utils.utils import is_day_closed, gettag, date_to_iso, date_to_dir
32
+ from osa.utils.utils import is_day_closed, gettag, date_to_iso
32
33
  from osa.veto import get_closed_list, get_veto_list
33
34
  from osa.scripts.gain_selection import GainSel_finished
34
35
 
@@ -98,9 +99,9 @@ def single_process(telescope):
98
99
  log.warning("No runs found for this date. Nothing to do. Exiting.")
99
100
  sys.exit(0)
100
101
 
101
- if not options.no_gainsel and not GainSel_finished(date_to_dir(options.date)):
102
+ if not options.no_gainsel and not GainSel_finished(options.date):
102
103
  log.info(
103
- f"Gain selection did not finish successfully for date {options.date}."
104
+ f"Gain selection did not finish successfully for date {date_to_iso(options.date)}. "
104
105
  "Try again later, once gain selection has finished."
105
106
  )
106
107
  sys.exit()
@@ -109,6 +110,15 @@ def single_process(telescope):
109
110
  log.info(f"Date {date_to_iso(options.date)} is already closed for {options.tel_id}")
110
111
  return sequence_list
111
112
 
113
+ if not options.test and not options.simulate:
114
+ if is_sequencer_running(options.date):
115
+ log.info(f"Sequencer is still running for date {date_to_iso(options.date)}. Try again later.")
116
+ sys.exit(0)
117
+
118
+ elif is_sequencer_completed(options.date) and not options.force_submit:
119
+ log.info(f"Sequencer already finished for date {date_to_iso(options.date)}. Exiting")
120
+ sys.exit(0)
121
+
112
122
  # Build the sequences
113
123
  sequence_list = build_sequences(options.date)
114
124
 
@@ -306,5 +316,39 @@ def output_matrix(matrix: list, padding_space: int):
306
316
  log.info(stringrow)
307
317
 
308
318
 
319
+ def is_sequencer_running(date: datetime.datetime) -> bool:
320
+ """Check if the jobs launched by sequencer are running or pending for the given date."""
321
+ summary_table = run_summary_table(date)
322
+ sacct_output = run_sacct()
323
+ sacct_info = get_sacct_output(sacct_output)
324
+
325
+ for run in summary_table["run_id"]:
326
+ jobs_run = sacct_info[sacct_info["JobName"]==f"LST1_{run:05d}"]
327
+ queued_jobs = jobs_run[(jobs_run["State"] == "RUNNING") | (jobs_run["State"] == "PENDING")]
328
+ if len(queued_jobs) != 0:
329
+ return True
330
+
331
+ return False
332
+
333
+
334
+ def is_sequencer_completed(date: datetime.datetime) -> bool:
335
+ """Check if the jobs launched by sequencer are running or pending for the given date."""
336
+ summary_table = run_summary_table(date)
337
+ data_runs = summary_table[summary_table["run_type"] == "DATA"]
338
+ sacct_output = run_sacct()
339
+ sacct_info = get_sacct_output(sacct_output)
340
+
341
+ for run in data_runs["run_id"]:
342
+ jobs_run = sacct_info[sacct_info["JobName"]==f"LST1_{run:05d}"]
343
+ if len(jobs_run["JobID"].unique())>1:
344
+ last_job_id = sorted(jobs_run["JobID"].unique())[-1]
345
+ jobs_run = sacct_info[sacct_info["JobID"]==last_job_id]
346
+ incomplete_jobs = jobs_run[(jobs_run["State"] != "COMPLETED")]
347
+ if len(jobs_run) == 0 or len(incomplete_jobs) != 0:
348
+ return False
349
+
350
+ return True
351
+
352
+
309
353
  if __name__ == "__main__":
310
354
  main()
@@ -20,7 +20,7 @@ from osa.utils.utils import is_day_closed, date_to_iso, date_to_dir
20
20
  log = myLogger(logging.getLogger())
21
21
 
22
22
 
23
- def html_content(body: str, date: str) -> str:
23
+ def html_content(body: str, date: str, title: str) -> str:
24
24
  """Build the HTML content.
25
25
 
26
26
  Parameters
@@ -43,11 +43,11 @@ def html_content(body: str, date: str) -> str:
43
43
  <html xmlns="http://www.w3.org/1999/xhtml">
44
44
  <head>
45
45
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
46
- <title>OSA Sequencer status</title><link href="osa.css" rel="stylesheet"
46
+ <title>{title} status</title><link href="osa.css" rel="stylesheet"
47
47
  type="text/css" /><style>table{{width:152ex;}}</style>
48
48
  </head>
49
49
  <body>
50
- <h1>OSA processing status</h1>
50
+ <h1>{title} processing status</h1>
51
51
  <p>Processing data from: {date}. Last updated: {time_update} UTC</p>
52
52
  {body}
53
53
  </body>
@@ -159,7 +159,7 @@ def main():
159
159
  directory.mkdir(parents=True, exist_ok=True)
160
160
 
161
161
  html_file = directory / Path(f"osa_status_{flat_date}.html")
162
- html_file.write_text(html_content(html_table, date), encoding="utf-8")
162
+ html_file.write_text(html_content(html_table, date, "OSA Sequencer"), encoding="utf-8")
163
163
 
164
164
  log.info("Done")
165
165
 
@@ -23,6 +23,7 @@ ALL_SCRIPTS = [
23
23
  "theta2_significance",
24
24
  "source_coordinates",
25
25
  "sequencer_webmaker",
26
+ "gainsel_webmaker",
26
27
  ]
27
28
 
28
29
  options.date = datetime.datetime.fromisoformat("2020-01-17")
@@ -397,3 +398,29 @@ def test_sequencer_webmaker(
397
398
  # Running without test option will make the script fail
398
399
  output = sp.run(["sequencer_webmaker", "-d", "2020-01-17"])
399
400
  assert output.returncode != 0
401
+
402
+
403
+ def test_gainsel_webmaker(
404
+ base_test_dir,
405
+ ):
406
+
407
+ output = sp.run(["gainsel_webmaker", "-d", "2020-01-17"])
408
+ assert output.returncode == 0
409
+ directory = base_test_dir / "OSA" / "GainSelWeb"
410
+ expected_file = directory / "osa_gainsel_status_2020-01-17.html"
411
+ assert expected_file.exists()
412
+
413
+ # Test a date with non-existing run summary
414
+ output = sp.run(["gainsel_webmaker", "-d", "2024-01-12"])
415
+ assert output.returncode == 0
416
+ directory = base_test_dir / "OSA" / "GainSelWeb"
417
+ expected_file = directory / "osa_gainsel_status_2024-01-12.html"
418
+ assert expected_file.exists()
419
+
420
+
421
+ def test_gainsel_web_content():
422
+ from osa.scripts.gainsel_webmaker import check_failed_jobs
423
+
424
+ table = check_failed_jobs(options.date)
425
+ assert table["GainSelStatus"][0] == "NOT STARTED"
426
+ assert table["GainSel%"][0] == 0.0
osa/tests/test_jobs.py CHANGED
@@ -71,6 +71,7 @@ def test_scheduler_env_variables(sequence_list, running_analysis_dir):
71
71
  "#SBATCH --error=log/Run01809.%4a_jobid_%A.err",
72
72
  f'#SBATCH --partition={cfg.get("SLURM", "PARTITION_PEDCALIB")}',
73
73
  "#SBATCH --mem-per-cpu=3GB",
74
+ "#SBATCH --account=dpps",
74
75
  ]
75
76
  # Extract the second sequence
76
77
  second_sequence = sequence_list[1]
@@ -83,7 +84,8 @@ def test_scheduler_env_variables(sequence_list, running_analysis_dir):
83
84
  "#SBATCH --error=log/Run01807.%4a_jobid_%A.err",
84
85
  "#SBATCH --array=0-10",
85
86
  f'#SBATCH --partition={cfg.get("SLURM", "PARTITION_DATA")}',
86
- "#SBATCH --mem-per-cpu=16GB",
87
+ "#SBATCH --mem-per-cpu=6GB",
88
+ "#SBATCH --account=dpps",
87
89
  ]
88
90
 
89
91
 
@@ -104,7 +106,8 @@ def test_job_header_template(sequence_list, running_analysis_dir):
104
106
  #SBATCH --output=log/Run01809.%4a_jobid_%A.out
105
107
  #SBATCH --error=log/Run01809.%4a_jobid_%A.err
106
108
  #SBATCH --partition={cfg.get('SLURM', 'PARTITION_PEDCALIB')}
107
- #SBATCH --mem-per-cpu=3GB"""
109
+ #SBATCH --mem-per-cpu=3GB
110
+ #SBATCH --account=dpps"""
108
111
  )
109
112
  assert header == output_string1
110
113
 
@@ -122,7 +125,8 @@ def test_job_header_template(sequence_list, running_analysis_dir):
122
125
  #SBATCH --error=log/Run01807.%4a_jobid_%A.err
123
126
  #SBATCH --array=0-10
124
127
  #SBATCH --partition={cfg.get('SLURM', 'PARTITION_DATA')}
125
- #SBATCH --mem-per-cpu=16GB"""
128
+ #SBATCH --mem-per-cpu=6GB
129
+ #SBATCH --account=dpps"""
126
130
  )
127
131
  assert header == output_string2
128
132
 
@@ -154,6 +158,7 @@ def test_create_job_template_scheduler(
154
158
  #SBATCH --array=0-10
155
159
  #SBATCH --partition={cfg.get('SLURM', 'PARTITION_DATA')}
156
160
  #SBATCH --mem-per-cpu={cfg.get('SLURM', 'MEMSIZE_DATA')}
161
+ #SBATCH --account={cfg.get('SLURM', 'ACCOUNT')}
157
162
 
158
163
  import os
159
164
  import subprocess
@@ -199,6 +204,7 @@ def test_create_job_template_scheduler(
199
204
  #SBATCH --array=0-8
200
205
  #SBATCH --partition={cfg.get('SLURM', 'PARTITION_DATA')}
201
206
  #SBATCH --mem-per-cpu={cfg.get('SLURM', 'MEMSIZE_DATA')}
207
+ #SBATCH --account={cfg.get('SLURM', 'ACCOUNT')}
202
208
 
203
209
  import os
204
210
  import subprocess
osa/utils/cliopts.py CHANGED
@@ -280,6 +280,13 @@ def sequencer_argparser():
280
280
  default=False,
281
281
  help="Do not check if the gain selection finished correctly (default False)",
282
282
  )
283
+ parser.add_argument(
284
+ "-f",
285
+ "--force-submit",
286
+ action="store_true",
287
+ default=False,
288
+ help="Force sequencer to submit jobs"
289
+ )
283
290
  parser.add_argument(
284
291
  "tel_id",
285
292
  choices=["ST", "LST1", "LST2", "all"],
@@ -299,6 +306,7 @@ def sequencer_cli_parsing():
299
306
  options.no_calib = opts.no_calib
300
307
  options.no_dl2 = opts.no_dl2
301
308
  options.no_gainsel = opts.no_gainsel
309
+ options.force_submit = opts.force_submit
302
310
 
303
311
  log.debug(f"the options are {opts}")
304
312