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

KDE's Doxygen guidelines are available online.