Kstars

schedulerutils.cpp
1/*
2 SPDX-FileCopyrightText: 2023 Wolfgang Reissenberger <sterne-jaeger@openfuture.de>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "schedulerutils.h"
8#include "schedulerjob.h"
9#include "schedulermodulestate.h"
10#include "ekos/capture/sequencejob.h"
11#include "Options.h"
12#include "skypoint.h"
13#include "kstarsdata.h"
14#include <ekos_scheduler_debug.h>
15
16namespace Ekos
17{
18
19SchedulerUtils::SchedulerUtils()
20{
21
22}
23
24SchedulerJob *SchedulerUtils::createJob(XMLEle *root, SchedulerJob *leadJob)
25{
26 SchedulerJob *job = new SchedulerJob();
27 QString name, group, train;
28 bool isLead = true;
29 dms ra, dec;
30 double rotation = 0.0, minimumAltitude = 0.0, minimumMoonSeparation = 0.0, maxMoonAltitude = 90.0;
31 QUrl sequenceURL, fitsURL;
32 StartupCondition startup = START_ASAP;
33 CompletionCondition completion = FINISH_SEQUENCE;
34 QDateTime startupTime, completionTime;
35 int completionRepeats = 0;
36 bool enforceWeather = false, enforceTwilight = false, enforceArtificialHorizon = false,
37 track = false, focus = false, align = false, guide = false;
38
39
40 XMLEle *ep, *subEP;
41 // We expect all data read from the XML to be in the C locale - QLocale::c()
42 QLocale cLocale = QLocale::c();
43
44 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
45 {
46 if (!strcmp(tagXMLEle(ep), "Name"))
47 name = pcdataXMLEle(ep);
48 else if (!strcmp(tagXMLEle(ep), "JobType"))
49 {
50 isLead = (QString(findXMLAttValu(ep, "lead")) == "true");
51 if (isLead == false)
52 job->setLeadJob(leadJob);
53 }
54 else if (!strcmp(tagXMLEle(ep), "Group"))
55 group = pcdataXMLEle(ep);
56 else if (!strcmp(tagXMLEle(ep), "OpticalTrain"))
57 train = pcdataXMLEle(ep);
58 else if (!strcmp(tagXMLEle(ep), "Coordinates"))
59 {
60 subEP = findXMLEle(ep, "J2000RA");
61 if (subEP)
62 ra.setH(cLocale.toDouble(pcdataXMLEle(subEP)));
63
64 subEP = findXMLEle(ep, "J2000DE");
65 if (subEP)
66 dec.setD(cLocale.toDouble(pcdataXMLEle(subEP)));
67 }
68 else if (!strcmp(tagXMLEle(ep), "Sequence"))
69 {
70 sequenceURL = QUrl::fromUserInput(pcdataXMLEle(ep));
71 }
72 else if (!strcmp(tagXMLEle(ep), "FITS"))
73 {
74 fitsURL = QUrl::fromUserInput(pcdataXMLEle(ep));
75 }
76 else if (!strcmp(tagXMLEle(ep), "PositionAngle"))
77 {
78 rotation = cLocale.toDouble(pcdataXMLEle(ep));
79 }
80 else if (!strcmp(tagXMLEle(ep), "StartupCondition"))
81 {
82 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
83 {
84 if (!strcmp("ASAP", pcdataXMLEle(subEP)))
85 startup = START_ASAP;
86 else if (!strcmp("At", pcdataXMLEle(subEP)))
87 {
88 startup = START_AT;
89 startupTime = QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate);
90 // Todo sterne-jaeger 2024-01-01: setting time spec from KStars necessary?
91 }
92 }
93 }
94 else if (!strcmp(tagXMLEle(ep), "Constraints"))
95 {
96 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
97 {
98 if (!strcmp("MinimumAltitude", pcdataXMLEle(subEP)))
99 {
100 Options::setEnableAltitudeLimits(true);
101 minimumAltitude = cLocale.toDouble(findXMLAttValu(subEP, "value"));
102 }
103 else if (!strcmp("MoonSeparation", pcdataXMLEle(subEP)))
104 {
105 Options::setSchedulerMoonSeparation(true);
106 minimumMoonSeparation = cLocale.toDouble(findXMLAttValu(subEP, "value"));
107 }
108 else if (!strcmp("MoonMaxAltitude", pcdataXMLEle(subEP)))
109 {
110 Options::setSchedulerMoonAltitude(true);
111 maxMoonAltitude = cLocale.toDouble(findXMLAttValu(subEP, "value"));
112 }
113 else if (!strcmp("EnforceWeather", pcdataXMLEle(subEP)))
114 enforceWeather = true;
115 else if (!strcmp("EnforceTwilight", pcdataXMLEle(subEP)))
116 enforceTwilight = true;
117 else if (!strcmp("EnforceArtificialHorizon", pcdataXMLEle(subEP)))
118 enforceArtificialHorizon = true;
119 }
120 }
121 else if (!strcmp(tagXMLEle(ep), "CompletionCondition"))
122 {
123 for (subEP = nextXMLEle(ep, 1); subEP != nullptr; subEP = nextXMLEle(ep, 0))
124 {
125 if (!strcmp("Sequence", pcdataXMLEle(subEP)))
126 completion = FINISH_SEQUENCE;
127 else if (!strcmp("Repeat", pcdataXMLEle(subEP)))
128 {
129 completion = FINISH_REPEAT;
130 completionRepeats = cLocale.toInt(findXMLAttValu(subEP, "value"));
131 }
132 else if (!strcmp("Loop", pcdataXMLEle(subEP)))
133 completion = FINISH_LOOP;
134 else if (!strcmp("At", pcdataXMLEle(subEP)))
135 {
136 completion = FINISH_AT;
137 completionTime = QDateTime::fromString(findXMLAttValu(subEP, "value"), Qt::ISODate);
138 }
139 }
140 }
141 else if (!strcmp(tagXMLEle(ep), "Steps"))
142 {
143 XMLEle *module;
144
145 for (module = nextXMLEle(ep, 1); module != nullptr; module = nextXMLEle(ep, 0))
146 {
147 const char *proc = pcdataXMLEle(module);
148
149 if (!strcmp(proc, "Track"))
150 track = true;
151 else if (!strcmp(proc, "Focus"))
152 focus = true;
153 else if (!strcmp(proc, "Align"))
154 align = true;
155 else if (!strcmp(proc, "Guide"))
156 guide = true;
157 }
158 }
159 }
160 SchedulerUtils::setupJob(*job, name, isLead, group, train, ra, dec,
161 KStarsData::Instance()->ut().djd(),
162 rotation, sequenceURL, fitsURL,
163
164 startup, startupTime,
165 completion, completionTime, completionRepeats,
166
167 minimumAltitude,
168 minimumMoonSeparation,
169 maxMoonAltitude,
170 enforceWeather,
171 enforceTwilight,
172 enforceArtificialHorizon,
173
174 track,
175 focus,
176 align,
177 guide);
178
179 return job;
180}
181
182void SchedulerUtils::setupJob(SchedulerJob &job, const QString &name, bool isLead, const QString &group,
183 const QString &train, const dms &ra, const dms &dec, double djd, double rotation, const QUrl &sequenceUrl,
184 const QUrl &fitsUrl, StartupCondition startup, const QDateTime &startupTime, CompletionCondition completion,
185 const QDateTime &completionTime, int completionRepeats, double minimumAltitude, double minimumMoonSeparation,
186 double maxMoonAltitude, bool enforceWeather, bool enforceTwilight, bool enforceArtificialHorizon, bool track, bool focus,
187 bool align, bool guide)
188{
189 /* Configure or reconfigure the observation job */
190
191 job.setIsLead(isLead);
192 job.setOpticalTrain(train);
193 job.setPositionAngle(rotation);
194
195 if (isLead)
196 {
197 job.setName(name);
198 job.setGroup(group);
199 job.setLeadJob(nullptr);
200 // djd should be ut.djd
201 job.setTargetCoords(ra, dec, djd);
202 job.setFITSFile(fitsUrl);
203
204 // #1 Startup conditions
205 job.setStartupCondition(startup);
206 if (startup == START_AT)
207 {
208 job.setStartupTime(startupTime);
209 }
210 /* Store the original startup condition */
211 job.setFileStartupCondition(job.getStartupCondition());
212 job.setStartAtTime(job.getStartupTime());
213
214 // #2 Constraints
215 job.setMinAltitude(minimumAltitude);
216 job.setMinMoonSeparation(minimumMoonSeparation);
217 job.setMaxMoonAltitude(maxMoonAltitude);
218
219 // Check enforce weather constraints
220 job.setEnforceWeather(enforceWeather);
221 // twilight constraints
222 job.setEnforceTwilight(enforceTwilight);
223 job.setEnforceArtificialHorizon(enforceArtificialHorizon);
224
225 // Job steps
226 job.setStepPipeline(SchedulerJob::USE_NONE);
227 if (track)
228 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_TRACK));
229 if (focus)
230 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_FOCUS));
231 if (align)
232 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_ALIGN));
233 if (guide)
234 job.setStepPipeline(static_cast<SchedulerJob::StepPipeline>(job.getStepPipeline() | SchedulerJob::USE_GUIDE));
235
236 /* Store the original startup condition */
237 job.setFileStartupCondition(job.getStartupCondition());
238 job.setStartAtTime(job.getStartupTime());
239 }
240
241 /* Consider sequence file is new, and clear captured frames map */
242 job.setCapturedFramesMap(CapturedFramesMap());
243 job.setSequenceFile(sequenceUrl);
244 job.setCompletionCondition(completion);
245 if (completion == FINISH_AT)
246 job.setFinishAtTime(completionTime);
247 else if (completion == FINISH_REPEAT)
248 {
249 job.setRepeatsRequired(completionRepeats);
250 job.setRepeatsRemaining(completionRepeats);
251 }
252
253 /* Reset job state to evaluate the changes */
254 job.reset();
255}
256
257uint16_t SchedulerUtils::fillCapturedFramesMap(const CapturedFramesMap &expected,
258 const CapturedFramesMap &capturedFramesCount, SchedulerJob &schedJob, CapturedFramesMap &capture_map,
259 int &completedIterations)
260{
261 uint16_t totalCompletedCount = 0;
262
263 // Figure out which repeat this is for the key with the least progress.
264 int minIterationsCompleted = -1, currentIteration = 0;
265 if (Options::rememberJobProgress())
266 {
267 completedIterations = 0;
268 for (const QString &key : expected.keys())
269 {
270 const int iterationsCompleted = capturedFramesCount[key] / expected[key];
271 if (minIterationsCompleted == -1 || iterationsCompleted < minIterationsCompleted)
272 minIterationsCompleted = iterationsCompleted;
273 }
274 // If this condition is FINISH_REPEAT, and we've already completed enough iterations
275 // Then set the currentIteratiion as 1 more than required. No need to go higher.
276 if (schedJob.getCompletionCondition() == FINISH_REPEAT
277 && minIterationsCompleted >= schedJob.getRepeatsRequired())
278 currentIteration = schedJob.getRepeatsRequired() + 1;
279 else
280 // Otherwise set it to one more than the number completed (i.e. the one it'll be working on).
281 currentIteration = minIterationsCompleted + 1;
282 completedIterations = std::max(0, currentIteration - 1);
283 }
284 else
285 // If we are not remembering progress, we'll only know the iterations completed
286 // by the current job's run.
287 completedIterations = schedJob.getCompletedIterations();
288
289 for (const QString &key : expected.keys())
290 {
291 if (Options::rememberJobProgress())
292 {
293 // If we're remembering progress, then figure out how many captures have not yet been captured.
294 const int diff = expected[key] * currentIteration - capturedFramesCount[key];
295
296 // Already captured more than required? Then don't capture any this round.
297 if (diff <= 0)
298 capture_map[key] = expected[key];
299 // Need more captures than one cycle could capture? If so, capture the full amount.
300 else if (diff >= expected[key])
301 capture_map[key] = 0;
302 // Otherwise we know that 0 < diff < expected[key]. Capture just the number needed.
303 else
304 capture_map[key] = expected[key] - diff;
305 }
306 else
307 // If we are not remembering progress, then the capture module, which reads this
308 // Will capture all requirements in the .esq file.
309 capture_map[key] = 0;
310
311 // collect all captured frames counts
312 if (schedJob.getCompletionCondition() == FINISH_LOOP)
313 totalCompletedCount += capturedFramesCount[key];
314 else
315 totalCompletedCount += std::min(capturedFramesCount[key],
316 static_cast<uint16_t>(expected[key] * schedJob.getRepeatsRequired()));
317 }
318 return totalCompletedCount;
319}
320
321void SchedulerUtils::updateLightFramesRequired(SchedulerJob * oneJob, const QList<SequenceJob *> &seqjobs,
322 const CapturedFramesMap &framesCount)
323{
324 bool lightFramesRequired = false;
325 CapturedFramesMap expected;
326 switch (oneJob->getCompletionCondition())
327 {
328 case FINISH_SEQUENCE:
329 case FINISH_REPEAT:
330 // Step 1: determine expected frames
331 SchedulerUtils::calculateExpectedCapturesMap(seqjobs, expected);
332 // Step 2: compare with already captured frames
333 for (SequenceJob *oneSeqJob : seqjobs)
334 {
335 QString const signature = oneSeqJob->getSignature();
336 /* If frame is LIGHT, how many do we have left? */
337 if (oneSeqJob->getFrameType() == FRAME_LIGHT && expected[signature] * oneJob->getRepeatsRequired() > framesCount[signature])
338 {
339 lightFramesRequired = true;
340 // exit the loop, one found is sufficient
341 break;
342 }
343 }
344 break;
345 default:
346 // in all other cases it does not depend on the number of captured frames
347 lightFramesRequired = true;
348 }
349 oneJob->setLightFramesRequired(lightFramesRequired);
350}
351
352SequenceJob *SchedulerUtils::processSequenceJobInfo(XMLEle * root, SchedulerJob * schedJob)
353{
354 SequenceJob *job = new SequenceJob(root, schedJob->getName());
355 if (schedJob)
356 {
357 if (FRAME_LIGHT == job->getFrameType())
358 schedJob->setLightFramesRequired(true);
359 if (job->getCalibrationPreAction() & CAPTURE_PREACTION_PARK_MOUNT)
360 schedJob->setCalibrationMountPark(true);
361 }
362
363 auto placeholderPath = Ekos::PlaceholderPath();
364 placeholderPath.processJobInfo(job);
365
366 return job;
367}
368
369bool SchedulerUtils::loadSequenceQueue(const QString &fileURL, SchedulerJob * schedJob, QList<SequenceJob *> &jobs,
370 bool &hasAutoFocus, ModuleLogger * logger)
371{
372 QFile sFile;
373 sFile.setFileName(fileURL);
374
375 if (!sFile.open(QIODevice::ReadOnly))
376 {
377 if (logger != nullptr) logger->appendLogText(i18n("Unable to open sequence queue file '%1'", fileURL));
378 return false;
379 }
380
381 LilXML *xmlParser = newLilXML();
382 char errmsg[MAXRBUF];
383 XMLEle *root = nullptr;
384 XMLEle *ep = nullptr;
385 char c;
386
387 while (sFile.getChar(&c))
388 {
389 root = readXMLEle(xmlParser, c, errmsg);
390
391 if (root)
392 {
393 for (ep = nextXMLEle(root, 1); ep != nullptr; ep = nextXMLEle(root, 0))
394 {
395 if (!strcmp(tagXMLEle(ep), "Autofocus"))
396 hasAutoFocus = (!strcmp(findXMLAttValu(ep, "enabled"), "true"));
397 else if (!strcmp(tagXMLEle(ep), "Job"))
398 {
399 SequenceJob *thisJob = processSequenceJobInfo(ep, schedJob);
400 jobs.append(thisJob);
401 if (jobs.count() == 1)
402 {
403 auto &firstJob = jobs.first();
404 if (FRAME_LIGHT == firstJob->getFrameType() && nullptr != schedJob)
405 {
406 schedJob->setInitialFilter(firstJob->getCoreProperty(SequenceJob::SJ_Filter).toString());
407 }
408
409 }
410 }
411 }
412 delXMLEle(root);
413 }
414 else if (errmsg[0])
415 {
416 if (logger != nullptr) logger->appendLogText(QString(errmsg));
417 delLilXML(xmlParser);
418 qDeleteAll(jobs);
419 return false;
420 }
421 }
422
423 return true;
424}
425
426bool SchedulerUtils::estimateJobTime(SchedulerJob * schedJob, const CapturedFramesMap &capturedFramesCount,
427 ModuleLogger * logger)
428{
429 static SchedulerJob *jobWarned = nullptr;
430
431 // Load the sequence job associated with the argument scheduler job.
432 QList<SequenceJob *> seqJobs;
433 bool hasAutoFocus = false;
434 bool result = loadSequenceQueue(schedJob->getSequenceFile().toLocalFile(), schedJob, seqJobs, hasAutoFocus, logger);
435 if (result == false)
436 {
437 qCWarning(KSTARS_EKOS_SCHEDULER) <<
438 QString("Warning: Failed estimating the duration of job '%1', its sequence file is invalid.").arg(
439 schedJob->getSequenceFile().toLocalFile());
440 return result;
441 }
442
443 // FIXME: setting in-sequence focus should be done in XML processing.
444 schedJob->setInSequenceFocus(hasAutoFocus);
445
446 // Stop spam of log on re-evaluation. If we display the warning once, then that's it.
447 if (schedJob != jobWarned && hasAutoFocus && !(schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS))
448 {
449 logger->appendLogText(
450 i18n("Warning: Job '%1' has its focus step disabled, periodic and/or HFR procedures currently set in its sequence will not occur.",
451 schedJob->getName()));
452 jobWarned = schedJob;
453 }
454
455 /* This is the map of captured frames for this scheduler job, keyed per storage signature.
456 * It will be forwarded to the Capture module in order to capture only what frames are required.
457 * If option "Remember Job Progress" is disabled, this map will be empty, and the Capture module will process all requested captures unconditionally.
458 */
459 CapturedFramesMap capture_map;
460 bool const rememberJobProgress = Options::rememberJobProgress();
461
462 double totalImagingTime = 0;
463 double imagingTimePerRepeat = 0, imagingTimeLeftThisRepeat = 0;
464
465 // Determine number of captures in the scheduler job
466 CapturedFramesMap expected;
467 uint16_t allCapturesPerRepeat = calculateExpectedCapturesMap(seqJobs, expected);
468
469 // fill the captured frames map
470 int completedIterations;
471 uint16_t totalCompletedCount = fillCapturedFramesMap(expected, capturedFramesCount, *schedJob, capture_map,
472 completedIterations);
473 schedJob->setCompletedIterations(completedIterations);
474 // Loop through sequence jobs to calculate the number of required frames and estimate duration.
475 foreach (SequenceJob *seqJob, seqJobs)
476 {
477 // FIXME: find a way to actually display the filter name.
478 QString seqName = i18n("Job '%1' %2x%3\" %4", schedJob->getName(), seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt(),
479 seqJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble(),
480 seqJob->getCoreProperty(SequenceJob::SJ_Filter).toString());
481
482 if (seqJob->getUploadMode() == ISD::Camera::UPLOAD_REMOTE)
483 {
484 qCInfo(KSTARS_EKOS_SCHEDULER) <<
485 QString("%1 duration cannot be estimated time since the sequence saves the files remotely.").arg(seqName);
486 schedJob->setEstimatedTime(-2);
487 qDeleteAll(seqJobs);
488 return true;
489 }
490
491 // Note that looping jobs will have zero repeats required.
492 QString const signature = seqJob->getSignature();
493 QString const signature_path = QFileInfo(signature).path();
494 int captures_required = seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt() * schedJob->getRepeatsRequired();
495 int captures_completed = capturedFramesCount[signature];
496 const int capturesRequiredPerRepeat = std::max(1, seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt());
497 int capturesLeftThisRepeat = std::max(0, capturesRequiredPerRepeat - (captures_completed % capturesRequiredPerRepeat));
498 if (captures_completed >= (1 + completedIterations) * capturesRequiredPerRepeat)
499 {
500 // Something else is causing this iteration to be incomplete. Nothing left to do for this seqJob.
501 capturesLeftThisRepeat = 0;
502 }
503
504 if (rememberJobProgress && schedJob->getCompletionCondition() != FINISH_LOOP)
505 {
506 /* Enumerate sequence jobs associated to this scheduler job, and assign them a completed count.
507 *
508 * The objective of this block is to fill the storage map of the scheduler job with completed counts for each capture storage.
509 *
510 * Sequence jobs capture to a storage folder, and are given a count of captures to store at that location.
511 * The tricky part is to make sure the repeat count of the scheduler job is properly transferred to each sequence job.
512 *
513 * For instance, a scheduler job repeated three times must execute the full list of sequence jobs three times, thus
514 * has to tell each sequence job it misses all captures, three times. It cannot tell the sequence job three captures are
515 * missing, first because that's not how the sequence job is designed (completed count, not required count), and second
516 * because this would make the single sequence job repeat three times, instead of repeating the full list of sequence
517 * jobs three times.
518 *
519 * The consolidated storage map will be assigned to each sequence job based on their signature when the scheduler job executes them.
520 *
521 * For instance, consider a RGBL sequence of single captures. The map will store completed captures for R, G, B and L storages.
522 * If R and G have 1 file each, and B and L have no files, map[storage(R)] = map[storage(G)] = 1 and map[storage(B)] = map[storage(L)] = 0.
523 * When that scheduler job executes, only B and L captures will be processed.
524 *
525 * In the case of a RGBLRGB sequence of single captures, the second R, G and B map items will count one less capture than what is really in storage.
526 * If R and G have 1 file each, and B and L have no files, map[storage(R1)] = map[storage(B1)] = 1, and all others will be 0.
527 * When that scheduler job executes, B1, L, R2, G2 and B2 will be processed.
528 *
529 * This doesn't handle the case of duplicated scheduler jobs, that is, scheduler jobs with the same storage for capture sets.
530 * Those scheduler jobs will all change state to completion at the same moment as they all target the same storage.
531 * This is why it is important to manage the repeat count of the scheduler job, as stated earlier.
532 */
533
534 captures_required = expected[seqJob->getSignature()] * schedJob->getRepeatsRequired();
535
536 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 sees %2 captures in output folder '%3'.").arg(seqName).arg(
537 captures_completed).arg(QFileInfo(signature).path());
538
539 // Enumerate sequence jobs to check how many captures are completed overall in the same storage as the current one
540 foreach (SequenceJob *prevSeqJob, seqJobs)
541 {
542 // Enumerate seqJobs up to the current one
543 if (seqJob == prevSeqJob)
544 break;
545
546 // If the previous sequence signature matches the current, skip counting to take duplicates into account
547 if (!signature.compare(prevSeqJob->getSignature()))
548 captures_required = 0;
549
550 // And break if no captures remain, this job does not need to be executed
551 if (captures_required == 0)
552 break;
553 }
554
555 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("%1 has completed %2/%3 of its required captures in output folder '%4'.").arg(
556 seqName).arg(captures_completed).arg(captures_required).arg(signature_path);
557
558 }
559 // Else rely on the captures done during this session
560 else if (0 < allCapturesPerRepeat)
561 {
562 captures_completed = schedJob->getCompletedCount() / allCapturesPerRepeat * seqJob->getCoreProperty(
563 SequenceJob::SJ_Count).toInt();
564 }
565 else
566 {
567 captures_completed = 0;
568 }
569
570 // Check if we still need any light frames. Because light frames changes the flow of the observatory startup
571 // Without light frames, there is no need to do focusing, alignment, guiding...etc
572 // We check if the frame type is LIGHT and if either the number of captures_completed frames is less than required
573 // OR if the completion condition is set to LOOP so it is never complete due to looping.
574 // Note that looping jobs will have zero repeats required.
575 // FIXME: As it is implemented now, FINISH_LOOP may loop over a capture-complete, therefore inoperant, scheduler job.
576 bool const areJobCapturesComplete = (0 == captures_required || captures_completed >= captures_required);
577 if (seqJob->getFrameType() == FRAME_LIGHT)
578 {
579 if(areJobCapturesComplete)
580 {
581 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 completed its sequence of %2 light frames.").arg(seqName).arg(
582 captures_required);
583 }
584 }
585 else
586 {
587 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 captures calibration frames.").arg(seqName);
588 }
589
590 /* If captures are not complete, we have imaging time left */
591 if (!areJobCapturesComplete || schedJob->getCompletionCondition() == FINISH_LOOP)
592 {
593 unsigned int const captures_to_go = captures_required - captures_completed;
594 const double secsPerCapture = (seqJob->getCoreProperty(SequenceJob::SJ_Exposure).toDouble() +
595 (seqJob->getCoreProperty(SequenceJob::SJ_Delay).toInt() / 1000.0));
596 totalImagingTime += fabs(secsPerCapture * captures_to_go);
597 imagingTimePerRepeat += fabs(secsPerCapture * seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt());
598 imagingTimeLeftThisRepeat += fabs(secsPerCapture * capturesLeftThisRepeat);
599 /* If we have light frames to process, add focus/dithering delay */
600 if (seqJob->getFrameType() == FRAME_LIGHT)
601 {
602 // If inSequenceFocus is true
603 if (hasAutoFocus)
604 {
605 // Wild guess, 10s of autofocus for each capture required. Can vary a lot, but this is just a completion estimate.
606 constexpr int afSecsPerCapture = 10;
607 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a focus procedure.").arg(seqName);
608 totalImagingTime += captures_to_go * afSecsPerCapture;
609 imagingTimePerRepeat += capturesRequiredPerRepeat * afSecsPerCapture;
610 imagingTimeLeftThisRepeat += capturesLeftThisRepeat * afSecsPerCapture;
611 }
612 // If we're dithering after each exposure, that's another 10-20 seconds
613 if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE && Options::ditherEnabled())
614 {
615 constexpr int ditherSecs = 15;
616 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("%1 requires a dither procedure.").arg(seqName);
617 totalImagingTime += (captures_to_go * ditherSecs) / Options::ditherFrames();
618 imagingTimePerRepeat += (capturesRequiredPerRepeat * ditherSecs) / Options::ditherFrames();
619 imagingTimeLeftThisRepeat += (capturesLeftThisRepeat * ditherSecs) / Options::ditherFrames();
620 }
621 }
622 }
623 }
624
625 schedJob->setCapturedFramesMap(capture_map);
626 schedJob->setSequenceCount(allCapturesPerRepeat * schedJob->getRepeatsRequired());
627
628 // only in case we remember the job progress, we change the completion count
629 if (rememberJobProgress)
630 schedJob->setCompletedCount(totalCompletedCount);
631
632 qDeleteAll(seqJobs);
633
634 schedJob->setEstimatedTimePerRepeat(imagingTimePerRepeat);
635 schedJob->setEstimatedTimeLeftThisRepeat(imagingTimeLeftThisRepeat);
636 if (schedJob->getLightFramesRequired())
637 schedJob->setEstimatedStartupTime(timeHeuristics(schedJob));
638
639 // FIXME: Move those ifs away to the caller in order to avoid estimating in those situations!
640
641 // We can't estimate times that do not finish when sequence is done
642 if (schedJob->getCompletionCondition() == FINISH_LOOP)
643 {
644 // We can't know estimated time if it is looping indefinitely
645 schedJob->setEstimatedTime(-2);
646 qCDebug(KSTARS_EKOS_SCHEDULER) <<
647 QString("Job '%1' is configured to loop until Scheduler is stopped manually, has undefined imaging time.")
648 .arg(schedJob->getName());
649 }
650 // If we know startup and finish times, we can estimate time right away
651 else if (schedJob->getStartupCondition() == START_AT &&
652 schedJob->getCompletionCondition() == FINISH_AT)
653 {
654 // FIXME: SchedulerJob is probably doing this already
655 qint64 const diff = schedJob->getStartupTime().secsTo(schedJob->getFinishAtTime());
656 schedJob->setEstimatedTime(diff);
657
658 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' has a startup time and fixed completion time, will run for %2.")
659 .arg(schedJob->getName())
660 .arg(dms(diff * 15.0 / 3600.0f).toHMSString());
661 }
662 // If we know finish time only, we can roughly estimate the time considering the job starts now
663 else if (schedJob->getStartupCondition() != START_AT &&
664 schedJob->getCompletionCondition() == FINISH_AT)
665 {
666 qint64 const diff = SchedulerModuleState::getLocalTime().secsTo(schedJob->getFinishAtTime());
667 schedJob->setEstimatedTime(diff);
668 qCDebug(KSTARS_EKOS_SCHEDULER) <<
669 QString("Job '%1' has no startup time but fixed completion time, will run for %2 if started now.")
670 .arg(schedJob->getName())
671 .arg(dms(diff * 15.0 / 3600.0f).toHMSString());
672 }
673 // Rely on the estimated imaging time to determine whether this job is complete or not - this makes the estimated time null
674 else if (totalImagingTime <= 0)
675 {
676 schedJob->setEstimatedTime(0);
677 schedJob->setEstimatedTimePerRepeat(1);
678 schedJob->setEstimatedTimeLeftThisRepeat(0);
679
680 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' will not run, complete with %2/%3 captures.")
681 .arg(schedJob->getName()).arg(schedJob->getCompletedCount()).arg(schedJob->getSequenceCount());
682 }
683 // Else consolidate with step durations
684 else
685 {
686 if (schedJob->getLightFramesRequired())
687 {
688 totalImagingTime += timeHeuristics(schedJob);
689 schedJob->setEstimatedStartupTime(timeHeuristics(schedJob));
690 }
691 dms const estimatedTime(totalImagingTime * 15.0 / 3600.0);
692 schedJob->setEstimatedTime(std::ceil(totalImagingTime));
693
694 qCInfo(KSTARS_EKOS_SCHEDULER) << QString("Job '%1' estimated to take %2 to complete.").arg(schedJob->getName(),
695 estimatedTime.toHMSString());
696 }
697
698 return true;
699}
700
701int SchedulerUtils::timeHeuristics(const SchedulerJob * schedJob)
702{
703 double imagingTime = 0;
704 /* FIXME: estimation should base on actual measure of each step, eventually with preliminary data as what it used now */
705 // Are we doing tracking? It takes about 30 seconds
706 if (schedJob->getStepPipeline() & SchedulerJob::USE_TRACK)
707 imagingTime += 30;
708 // Are we doing initial focusing? That can take about 2 minutes
709 if (schedJob->getStepPipeline() & SchedulerJob::USE_FOCUS)
710 imagingTime += 120;
711 // Are we doing astrometry? That can take about 60 seconds
712 if (schedJob->getStepPipeline() & SchedulerJob::USE_ALIGN)
713 {
714 imagingTime += 60;
715 }
716 // Are we doing guiding?
717 if (schedJob->getStepPipeline() & SchedulerJob::USE_GUIDE)
718 {
719 // Looping, finding guide star, settling takes 15 sec
720 imagingTime += 15;
721
722 // Add guiding settle time from dither setting (used by phd2::guide())
723 imagingTime += Options::ditherSettle();
724 // Add guiding settle time from ekos sccheduler setting
725 imagingTime += Options::guidingSettle();
726
727 // If calibration always cleared
728 // then calibration process can take about 2 mins
729 if(Options::resetGuideCalibration())
730 imagingTime += 120;
731 }
732 return imagingTime;
733
734}
735
736uint16_t SchedulerUtils::calculateExpectedCapturesMap(const QList<SequenceJob *> &seqJobs,
737 CapturedFramesMap &expected)
738{
739 uint16_t capturesPerRepeat = 0;
740 for (auto &seqJob : seqJobs)
741 {
742 capturesPerRepeat += seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt();
743 QString signature = seqJob->getCoreProperty(SequenceJob::SJ_Signature).toString();
744 expected[signature] = static_cast<uint16_t>(seqJob->getCoreProperty(SequenceJob::SJ_Count).toInt()) + (expected.contains(
745 signature) ? expected[signature] : 0);
746 }
747 return capturesPerRepeat;
748}
749
750double SchedulerUtils::findAltitude(const SkyPoint &target, const QDateTime &when, bool * is_setting,
751 GeoLocation *geo, bool debug)
752{
753 // FIXME: block calculating target coordinates at a particular time is duplicated in several places
754
755 auto geoLocation = (geo == nullptr) ? SchedulerModuleState::getGeo() : geo;
756
757 // Retrieve the argument date/time, or fall back to current time - don't use QDateTime's timezone!
758 KStarsDateTime ltWhen(when.isValid() ?
759 Qt::UTC == when.timeSpec() ? geoLocation->UTtoLT(KStarsDateTime(when)) : when :
760 SchedulerModuleState::getLocalTime());
761
762 // Create a sky object with the target catalog coordinates
763 SkyObject o;
764 o.setRA0(target.ra0());
765 o.setDec0(target.dec0());
766
767 // Update RA/DEC of the target for the current fraction of the day
768 KSNumbers numbers(ltWhen.djd());
769 o.updateCoordsNow(&numbers);
770
771 // Calculate alt/az coordinates using KStars instance's geolocation
772 CachingDms const LST = geoLocation->GSTtoLST(geoLocation->LTtoUT(ltWhen).gst());
773 o.EquatorialToHorizontal(&LST, geoLocation->lat());
774
775 // Hours are reduced to [0,24[, meridian being at 0
776 double offset = LST.Hours() - o.ra().Hours();
777 if (24.0 <= offset)
778 offset -= 24.0;
779 else if (offset < 0.0)
780 offset += 24.0;
781 bool const passed_meridian = 0.0 <= offset && offset < 12.0;
782
783 if (debug)
784 qCDebug(KSTARS_EKOS_SCHEDULER) << QString("When:%9 LST:%8 RA:%1 RA0:%2 DEC:%3 DEC0:%4 alt:%5 setting:%6 HA:%7")
785 .arg(o.ra().toHMSString())
786 .arg(o.ra0().toHMSString())
787 .arg(o.dec().toHMSString())
788 .arg(o.dec0().toHMSString())
789 .arg(o.alt().Degrees())
790 .arg(passed_meridian ? "yes" : "no")
791 .arg(o.ra().Hours())
792 .arg(LST.toHMSString())
793 .arg(ltWhen.toString("HH:mm:ss"));
794
795 if (is_setting)
796 *is_setting = passed_meridian;
797
798 return o.alt().Degrees();
799}
800
801QList<SchedulerJob *> SchedulerUtils::filterLeadJobs(const QList<SchedulerJob *> &jobs)
802{
804 for (auto job : jobs)
805 if (job->isLead())
806 result.append(job);
807
808 return result;
809}
810
811} // namespace
a dms subclass that caches its sine and cosine values every time the angle is changed.
Definition cachingdms.h:19
Contains all relevant information for specifying a location on Earth: City Name, State/Province name,...
Definition geolocation.h:28
There are several time-dependent values used in position calculations, that are not specific to an ob...
Definition ksnumbers.h:43
Extension of QDateTime for KStars KStarsDateTime can represent the date/time as a Julian Day,...
Sequence Job is a container for the details required to capture a series of images.
Provides all necessary information about an object in the sky: its coordinates, name(s),...
Definition skyobject.h:50
The sky coordinates of a point in the sky.
Definition skypoint.h:45
const CachingDms & dec() const
Definition skypoint.h:269
const CachingDms & ra0() const
Definition skypoint.h:251
virtual void updateCoordsNow(const KSNumbers *num)
updateCoordsNow Shortcut for updateCoords( const KSNumbers *num, false, nullptr, nullptr,...
Definition skypoint.h:410
const CachingDms & ra() const
Definition skypoint.h:263
void EquatorialToHorizontal(const CachingDms *LST, const CachingDms *lat)
Determine the (Altitude, Azimuth) coordinates of the SkyPoint from its (RA, Dec) coordinates,...
Definition skypoint.cpp:77
void setRA0(dms r)
Sets RA0, the catalog Right Ascension.
Definition skypoint.h:94
const dms & alt() const
Definition skypoint.h:281
const CachingDms & dec0() const
Definition skypoint.h:257
void setDec0(dms d)
Sets Dec0, the catalog Declination.
Definition skypoint.h:119
An angle, stored as degrees, but expressible in many ways.
Definition dms.h:38
double Hours() const
Definition dms.h:168
virtual void setH(const double &x)
Sets floating-point value of angle, in hours.
Definition dms.h:210
const QString toHMSString(const bool machineReadable=false, const bool highPrecision=false) const
Definition dms.cpp:378
const double & Degrees() const
Definition dms.h:141
QString i18n(const char *text, const TYPE &arg...)
Ekos is an advanced Astrophotography tool for Linux.
Definition align.cpp:83
StartupCondition
Conditions under which a SchedulerJob may start.
QMap< QString, uint16_t > CapturedFramesMap
mapping signature --> frames count
CompletionCondition
Conditions under which a SchedulerJob may complete.
GeoCoordinates geo(const QVariant &location)
QString name(const QVariant &location)
QString path(const QString &relativePath)
const QList< QKeySequence > & completion()
QCA_EXPORT Logger * logger()
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid() const const
Qt::TimeSpec timeSpec() const const
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
void setFileName(const QString &name)
QString path() const const
bool getChar(char *c)
void append(QList< T > &&value)
qsizetype count() const const
T & first()
QLocale c()
double toDouble(QStringView s, bool *ok) const const
int toInt(QStringView s, bool *ok) const const
QString arg(Args &&... args) const const
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
QTextStream & dec(QTextStream &stream)
QUrl fromUserInput(const QString &userInput, const QString &workingDirectory, UserInputResolutionOptions options)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:54:28 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.