From 1c2a98ead863ce659d909b5073dfc12308a6aa97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20G=C3=B3mez?= Date: Wed, 23 Sep 2026 14:23:57 -0300 Subject: [PATCH] fix(backend): Get observation data from visits in the stitched timeline stats computation --- .../core/events/queue/nightchanges.py | 68 ++++++++++--------- .../core/statscalculator/statscalculator.py | 25 ++++++- changelog.d/GSCHED-1030.fix.md | 1 + 3 files changed, 60 insertions(+), 34 deletions(-) create mode 100644 changelog.d/GSCHED-1030.fix.md diff --git a/backend/scheduler/core/events/queue/nightchanges.py b/backend/scheduler/core/events/queue/nightchanges.py index 7829a2b2a..e47c36b07 100644 --- a/backend/scheduler/core/events/queue/nightchanges.py +++ b/backend/scheduler/core/events/queue/nightchanges.py @@ -345,35 +345,41 @@ def rnd_min(dt: datetime) -> datetime: def to_json(self) -> dict: utc = ZoneInfo('UTC') return { - n_idx: {site.name: [{'startTimeSlot': te.start_time_slot, - 'event': {'site': te.event.site.name, - 'time': te.event.time.strftime(self._datetime_formatter), - 'description': te.event.description, - }, - 'plan': {'start': te.plan_generated.start.astimezone(utc).strftime(self._datetime_formatter), - 'end': te.plan_generated.end.astimezone(utc).strftime(self._datetime_formatter), - 'site': te.plan_generated.site.name, - 'visits': [{"starTime": v.start_time.astimezone(utc).strftime(self._datetime_formatter), - "endTime": (v.start_time+ - v.time_slots*te.plan_generated.time_slot_length).strftime(self._datetime_formatter), - "obsId": v.observation.id.id, - "atomStartIdx": v.atom_start_idx, - "atomEndIdx": v.atom_end_idx, - "altitude": alt, - "instrument": inst.id if (inst := v.observation.instrument()) else '', - "obs_class": v.observation.obs_class.name, - "score": v.score, - "peakScore": v.peak_score, - "completion": v.completion} - for v, alt in zip(te.plan_generated.visits, te.plan_generated.alt_degs)], - 'nightStats': { - 'timeLoss': te.plan_generated.night_stats.time_loss, - 'planScore': te.plan_generated.night_stats.plan_score, - 'completionFraction': te.plan_generated.night_stats.completion_fraction, - 'programCompletion': te.plan_generated.night_stats.program_completion - } - } if te.plan_generated else {} - } for te in time_entries] - for site, time_entries in by_site.items() - } for n_idx, by_site in self.timeline.items() + n_idx: { + site.name: [ + { + 'startTimeSlot': te.start_time_slot, + 'event': { + 'site': te.event.site.name, + 'time': te.event.time.strftime(self._datetime_formatter), + 'description': te.event.description, + }, + 'plan': { + 'start': te.plan_generated.start.astimezone(utc).strftime(self._datetime_formatter), + 'end': te.plan_generated.end.astimezone(utc).strftime(self._datetime_formatter), + 'site': te.plan_generated.site.name, + 'visits': [ + { + "starTime": v.start_time.astimezone(utc).strftime(self._datetime_formatter), + "endTime": (v.start_time + v.time_slots*te.plan_generated.time_slot_length).strftime(self._datetime_formatter), + "obsId": v.observation.id.id, + "atomStartIdx": v.atom_start_idx, + "atomEndIdx": v.atom_end_idx, + "altitude": alt, + "instrument": inst.id if (inst := v.observation.instrument()) else '', + "obs_class": v.observation.obs_class.name, + "score": v.score, + "peakScore": v.peak_score, + "completion": v.completion + } for v, alt in zip(te.plan_generated.visits, te.plan_generated.alt_degs)], + 'nightStats': { + 'timeLoss': te.plan_generated.night_stats.time_loss, + 'planScore': te.plan_generated.night_stats.plan_score, + 'completionFraction': te.plan_generated.night_stats.completion_fraction, + 'programCompletion': te.plan_generated.night_stats.program_completion + } + } if te.plan_generated else {} + } for te in time_entries + ] for site, time_entries in by_site.items() + } for n_idx, by_site in self.timeline.items() } diff --git a/backend/scheduler/core/statscalculator/statscalculator.py b/backend/scheduler/core/statscalculator/statscalculator.py index c082b400e..6ddd30db8 100644 --- a/backend/scheduler/core/statscalculator/statscalculator.py +++ b/backend/scheduler/core/statscalculator/statscalculator.py @@ -58,8 +58,14 @@ def calculate_stitched_timeline_stats(timeline: NightlyTimeline, if entry_idx == len(timeline.stitched_timeline[night_idx][site]) - 1: for v in plan.visits: - obs = collector.get_observation(v.observation.id) + # The visit keeps its own Observation: in RT the collector is rebuilt on every + # event, so observations from earlier plans might not be in it anymore. + obs = v.observation program = collector.get_program(obs.belongs_to) + if program is None: + logger.warning(f'Program {obs.belongs_to.id} for observation {obs.id.id} ' + f'not found in the collector, skipping it in the run summary.') + continue # Check if program is on the table metrics_per_program.setdefault(program.id, 0.0) @@ -76,8 +82,14 @@ def calculate_stitched_timeline_stats(timeline: NightlyTimeline, plan_conditions = [] completion_fraction: Counter[Band] = Counter({b: 0 for b in Band}) + # Altitudes are only calculated once per plan: previous entries are revisited on every + # RT event, and their target info might not be in the current collector anymore. + calculate_alt_degs = len(plan.alt_degs) != len(plan.visits) + if calculate_alt_degs: + plan.alt_degs = [] + for visit in plan.visits: - obs = collector.get_observation(visit.observation.id) + obs = visit.observation # check if obs is a too if obs.too_type is not None: n_toos += 1 @@ -92,7 +104,14 @@ def calculate_stitched_timeline_stats(timeline: NightlyTimeline, completion_fraction[obs.band] += 1 # Calculate altitude data - ti = collector.get_target_info(visit.observation.id) + if not calculate_alt_degs: + continue + ti = collector.get_target_info(obs.id) + if ti is None or night_idx not in ti: + logger.warning(f'No target info for observation {obs.id.id} on night {night_idx}.') + # Keep alt_degs aligned with the visits, as they are zipped together. + plan.alt_degs.append([]) + continue end_time_slot = visit.start_time_slot + visit.time_slots values = ti[night_idx].alt[visit.start_time_slot: end_time_slot] alt_degs = [val.dms[0] + (val.dms[1] / 60) + (val.dms[2] / 3600) for val in values] diff --git a/changelog.d/GSCHED-1030.fix.md b/changelog.d/GSCHED-1030.fix.md new file mode 100644 index 000000000..a41fa6be4 --- /dev/null +++ b/changelog.d/GSCHED-1030.fix.md @@ -0,0 +1 @@ +Retrieve the observation data from the visits when calculating the timeline stats, collector may have None type observations