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

KDE's Doxygen guidelines are available online.