API
 
Loading...
Searching...
No Matches
psfAcq.hpp
Go to the documentation of this file.
1/** \file psfAcq.hpp
2 * \brief The MagAO-X PSF Fitter application header
3 *
4 * \ingroup psfAcq_files
5 */
6
7#ifndef psfAcq_hpp
8#define psfAcq_hpp
9
10#include "../../libMagAOX/libMagAOX.hpp" //Note this is included on command line to trigger pch
11#include "../../magaox_git_version.h"
12
13#include <mx/math/fit/fitGaussian.hpp>
14#include <mx/improc/imageFilters.hpp>
15
16#include <algorithm>
17#include <cmath>
18#include <cstddef>
19#include <limits>
20#include <memory>
21#include <utility>
22
23/** \defgroup psfAcq
24 * \brief The MagAO-X PSF fitter.
25 *
26 * <a href="../handbook/operating/software/apps/psfAcq.html">Application Documentation</a>
27 *
28 * \ingroup apps
29 *
30 */
31
32/** \defgroup psfAcq_files
33 * \ingroup psfAcq
34 */
35
36namespace MagAOX
37{
38namespace app
39{
40
41struct darkShmimT
42{
43 static std::string configSection()
44 {
45 return "darkShmim";
46 };
47
48 static std::string indiPrefix()
49 {
50 return "dark";
51 };
52};
53
54struct Star
55{
56public:
57
58 /// Monotonic identifier used as a stable tie-breaker across equal-brightness stars.
59 std::size_t id{ 0 };
60
61 /// Star row coordinate in image pixel space.
62 float x{ 0 };
63
64 /// Star column coordinate in image pixel space.
65 float y{ 0 };
66
67 /// Peak pixel value of the fitted star.
68 float max{ 0 };
69
70 /// FWHM returned by the Gaussian fit in pixels.
71 float fwhm{ 0 };
72
73 /// Derived seeing value in arcseconds.
74 float seeing{ 0 };
75
76 /// Consecutive frames where this star was not updated.
77 int missedFrames{ 0 };
78
79private:
80
81 /// Owned INDI property for this star's exported fit values.
82 std::unique_ptr<pcf::IndiProperty> m_prop;
83
84public:
85 /// Default constructor.
86 Star() = default;
87
88 /// Disable copy constructor because the star owns a unique INDI property instance.
89 Star( const Star & ) = delete;
90
91 /// Disable copy assignment because the star owns a unique INDI property instance.
92 Star & operator=( const Star & ) = delete;
93
94 /// Enable move constructor.
95 Star( Star && ) noexcept = default;
96
97 /// Enable move assignment.
98 Star & operator=( Star && ) noexcept = default;
99
100 /// Access the owned INDI property.
101 pcf::IndiProperty & prop()
102 {
103 if( !m_prop )
104 {
105 throw std::runtime_error( "attempt to access nullptr prop" );
106 }
107
108 return *m_prop;
109 }
110
111 /// Check whether an INDI property has been allocated.
112 bool hasProp() const
113 {
114 return static_cast<bool>( m_prop );
115 }
116
117 /// Allocate the star's INDI property if needed.
118 void allocate()
119 {
120 if( !m_prop )
121 {
122 m_prop = std::make_unique<pcf::IndiProperty>();
123 }
124 }
125
126 /// Release the star's INDI property.
128 {
129 m_prop.reset();
130 }
131};
132
133/// The MagAO-X PSF Fitter
134/**
135 * \ingroup psfAcq
136 */
137class psfAcq : public MagAOXApp<true>,
138 public dev::shmimMonitor<psfAcq>,
139 public dev::shmimMonitor<psfAcq, darkShmimT>,
140 public dev::telemeter<psfAcq>
141{
142 // Give the test harness access.
143 friend class psfAcq_test;
144
145 friend class dev::shmimMonitor<psfAcq>;
146 friend class dev::shmimMonitor<psfAcq, darkShmimT>;
147 friend class dev::telemeter<psfAcq>;
148
149 public:
150 // The base shmimMonitor type
152
154
155 // The base telemeter type
157
158 /// Floating point type in which to do all calculations.
159 typedef float realT;
160
161 /** \name app::dev Configurations
162 *@{
163 */
164 ///@}
165
166 protected:
167 /** \name Configurable Parameters
168 *@{
169 */
170
171 std::string m_fpsSource; ///< Device name for getting fps if time-based averaging is used. This device should have
172 ///< *.fps.current.
173
174 uint16_t m_fitCircBuffMaxLength{ 3600 }; ///< Maximum length of the latency measurement circular buffers
175 float m_fitCircBuffMaxTime{ 5 }; ///< Maximum time of the latency meaurement circular buffers
176
177 float m_fwhmGuess{ 4 };
178 ///@}
179
180 mx::improc::eigenImage<float> m_image;
181 mx::improc::eigenImage<float> m_sm;
182
183 mx::improc::eigenImage<float> m_dark;
184
185 double m_acqQuitTime {0};
186 double m_acqPauseTime{2};
187
188 int m_current_acq_star {-1}; // active star index used for seeing output
190 bool m_updated{ false };
191 float m_x{ 0 };
192 float m_y{ 0 };
193 int m_max_loops{ 5 }; // default to detecting a max of 5 stars
194 int m_zero_area{ 8 }; // default to zeroing an 8x8 pixel area around stars it finds
195 float m_threshold = { 7.0 }; // how many sigma away from the mean you want to classify a detection, default to
196 // 7sigma
197 float m_fwhm_threshold = { 4.0 }; // minumum fwhm to consider something a star
198 float m_max_fwhm = { 40.0 }; // max fwhm to consider a star
199
200 std::vector<float> m_first_x_vals = {};
201 std::vector<float> m_first_y_vals = {};
202 /// Tracked stars with associated INDI properties.
203 std::vector<Star> m_detectedStars;
204
205 /// Monotonic id counter used to stamp new stars for stable ordering tie-breaks.
206 std::size_t m_nextStarId{ 0 };
207
208 float m_dx{ 0 };
209 float m_dy{ 0 };
210
211 double m_plate_scale = .0795336;
213 int m_num_stars{ 0 };
214 float m_seeing{ 0 };
215 int m_acquire_star{ -1 }; // Testing for user to select star
216 int m_seeing_star { -1 }; // star index exposed for seeing source; auto-set to 0 when stars are present
217 int m_x_center{}; // 'center' of image or hot spot
219
220 float m_fps{ 0 };
221
222 /// Last observed `flipacq.presetName.out` switch state.
223 bool m_flipAcqOutWasOn{ false };
224
225 /// True once `m_flipAcqOutWasOn` has been initialized from INDI.
227
228 /// Last successful loop-exit telemetry dump time in seconds.
230
231 /// Snapshot of one star's telemetry fields for deferred emission.
233 {
234 /// Star x position in pixels.
235 float x_pos;
236
237 /// Star y position in pixels.
238 float y_pos;
239
240 /// Peak pixel value.
241 float m_pix;
242
243 /// FWHM in pixels.
244 float fwhm;
245
246 /// Seeing estimate in arcseconds.
247 float seeing;
248 };
249
250 /// Emit one telemetry record per star sample.
251 int emitStarTelemetry( const std::vector<starTelemSample> &starTelemetryValues /**< [in] per-star telemetry samples to emit. */ );
252
253 /// Remove one tracked star and its INDI property/callback registration.
254 /** Caller must hold `m_indiMutex`.
255 */
256 void removeStar( size_t index /**< [in] index of the tracked star to remove. */ );
257
258 /// Delete star INDI properties and reset tracked acquisition stars.
259 /** Caller must hold `m_indiMutex`.
260 */
261 void resetAcq();
262
263 /// Register one tracked star's read-only INDI property using a rank-based label.
264 /** Caller must hold `m_indiMutex`.
265 */
266 void registerStarProperty( Star &star, /**< [in,out] tracked star to register. */
267 std::size_t rankIndex /**< [in] brightness rank label index, `star_<rankIndex>`. */ );
268
269 /// Remove one tracked star's INDI property without erasing the star state.
270 /** Caller must hold `m_indiMutex`.
271 */
272 void unregisterStarProperty( Star &star /**< [in,out] tracked star to unregister. */ );
273
274 /// Sort tracked stars by brightness and ensure labels are `star_0`, `star_1`, ...
275 /** Caller must hold `m_indiMutex`.
276 */
278
279 // Working memory for poke fitting
280 mx::math::fit::fitGaussian2Dsym<float> m_gfit;
281
282 public:
283 /// Default c'tor.
284 psfAcq();
285
286 /// D'tor, declared and defined for noexcept.
288
289 virtual void setupConfig();
290
291 /// Implementation of loadConfig logic, separated for testing.
292 /** This is called by loadConfig().
293 */
294 int loadConfigImpl(
295 mx::app::appConfigurator &_config /**< [in] an application configuration from which to load values*/ );
296
297 virtual void loadConfig();
298
299 /// Startup function
300 /**
301 *
302 */
303 virtual int appStartup();
304
305 /// Implementation of the FSM for psfAcq.
306 /**
307 * \returns 0 on no critical error
308 * \returns -1 on an error requiring shutdown
309 */
310 virtual int appLogic();
311
312 /// Shutdown the app.
313 /**
314 *
315 */
316 virtual int appShutdown();
317
318 // shmimMonitor interface:
319 int allocate( const dev::shmimT & );
320
321 int processImage( void *curr_src, const dev::shmimT & );
322
323 // shmimMonitor interface for referenc:
324 int allocate( const darkShmimT & );
325
326 int processImage( void *curr_src, const darkShmimT & );
327
328 protected:
329 std::mutex m_imageMutex;
330
331 protected:
332 /** \name INDI
333 * @{
334 */
335
337
339
342
343 /// Subscription to `flipacq.presetName` switch updates.
346
347 // For user to select star
350
351 // For user to set what star they want to use to calc seeing
354
355 // toggling
358
361
362 ///@}
363
364 /** \name Telemeter Interface
365 *
366 * @{
367 */
368 /// No-op scheduler hook; loop-exit logic emits telemetry directly.
369 int checkRecordTimes();
370
371 /// Record telemetry for all properties of each detected star.
372 int recordTelem( const telem_psfacq *telemTag /**< [in] telemetry tag used for overload resolution. */ );
373
374 ///@}
375};
376
382
384{
385 { //mutex scope
386 std::lock_guard<std::mutex> guard( m_indiMutex );
387 m_detectedStars.clear();
388 }
389}
390
392{
395 TELEMETER_SETUP_CONFIG( config );
396
397 config.add(
398 "fitter.fpsSource",
399 "",
400 "fitter.fpsSource",
401 argType::Required,
402 "fitter",
403 "fpsSource",
404 false,
405 "string",
406 "Device name for getting fps if time-based averaging is used. This device should have *.fps.current." );
407 config.add( "fitter.max_loops",
408 "",
409 "fitter.max_loops",
410 argType::Required,
411 "fitter",
412 "max_loops",
413 false,
414 "int",
415 "Setting the number of stars to detect in processImage function." );
416 config.add( "fitter.zero_area",
417 "",
418 "fitter.zero_area",
419 argType::Required,
420 "fitter",
421 "zero_area",
422 false,
423 "int",
424 "Setting the pixel area to zero out after detecting stars in processImage function." );
425 config.add( "fitter.threshold",
426 "",
427 "fitter.threshold",
428 argType::Required,
429 "fitter",
430 "threshold",
431 false,
432 "float",
433 "setting how many sigma away from the mean you want to classify a detection." );
434 config.add( "fitter.fwhm_threshold",
435 "",
436 "fitter.fwhm_threshold",
437 argType::Required,
438 "fitter",
439 "fwhm_threshold",
440 false,
441 "float",
442 "minumum fwhm to consider something a star." );
443 config.add( "acquisition.x_center",
444 "",
445 "acquisition.x_center",
446 argType::Required,
447 "acquisition",
448 "x_center",
449 false,
450 "int",
451 "X value for 'center' of image." );
452 config.add( "acquisition.y_center",
453 "",
454 "acquisition.y_center",
455 argType::Required,
456 "acquisition",
457 "y_center",
458 false,
459 "int",
460 "Y value for 'center' of image." );
461}
462
463inline int psfAcq::loadConfigImpl( mx::app::appConfigurator &_config )
464{
468
469 _config( m_fpsSource, "fitter.fpsSource" );
470 _config( m_max_loops, "fitter.max_loops" ); // Max number of stars to detect in processImage
471 _config( m_zero_area, "fitter.zero_area" ); // pixel area to zero out in processImage when a star is detected
472 _config( m_threshold, "fitter.threshold" ); // how many sigma away from the mean you want to classify a detection
474 "fitter.fwhm_threshold" ); // how many sigma away from the mean you want to classify a detection
475
476 _config( m_x_center, "acquisition.x_center" ); // star number to acquire
477 _config( m_y_center, "acquisition.y_center" ); // star number to acquire
478
479 return 0;
480}
481
483{
484 loadConfigImpl( config );
485}
486
488{
489 if( shmimMonitorT::appStartup() < 0 )
490 {
491 return log<software_error, -1>( { __FILE__, __LINE__ } );
492 }
493
495 {
496 return log<software_error, -1>( { __FILE__, __LINE__ } );
497 }
498
500
501 if( m_fpsSource != "" )
502 {
503 REG_INDI_SETPROP( m_indiP_fpsSource, m_fpsSource, std::string( "fps" ) );
504 }
505
506 REG_INDI_SETPROP( m_indiP_flipAcqPresetName, "flipacq", "presetName" );
507
508 // creating toggling to restart the acquisition
509 createStandardIndiRequestSw( m_indiP_restartAcq, "restart_acq", "Restart Acquisition", "psfAcq");
511
512 // INDI prop for seeing
514 m_indiP_seeing.add(pcf::IndiElement("current"));
515 m_indiP_seeing["current"].setValue( m_seeing ); //m_seeing gets assigned the seeing values of the star that is acquired
517
518 // create toggling for recording seeing
519 createStandardIndiToggleSw( m_indiP_recordSeeing, "record_seeing", "Record Seeing");
520 m_indiP_recordSeeing["toggle"].set(pcf::IndiElement::On);
522 {
524 return -1;
525 }
526
527 // INDI prop for user to select star
528 CREATE_REG_INDI_NEW_NUMBERF( m_indiP_acquire_star, "acquire_star", 0, 20, 1, "%d", "", "" );
529 m_indiP_acquire_star["current"].setValue( m_acquire_star );
530 m_indiP_acquire_star["target"].setValue( m_acquire_star );
531
532 // INDI prop for user to select seeing star
533 CREATE_REG_INDI_NEW_NUMBERF( m_indiP_seeing_star, "seeing_star", 0, 20, 1, "%d", "", "" );
534 m_indiP_seeing_star["current"].setValue( m_seeing_star );
535 m_indiP_seeing_star["target"].setValue( m_seeing_star );
536
537 // number of stars INDI prop
539 m_indiP_num_stars.add(pcf::IndiElement("current"));
540 m_indiP_num_stars["current"].setValue( m_num_stars );
542
544
545 return 0;
546}
547
549{
550 if( shmimMonitorT::appLogic() < 0 )
551 {
552 return log<software_error, -1>( { __FILE__, __LINE__ } );
553 }
554
556 {
557 return log<software_error, -1>( { __FILE__, __LINE__ } );
558 }
559
560 std::unique_lock<std::mutex> lock( m_indiMutex );
561
564
569
570 for( size_t n = 0; n < m_detectedStars.size() ; ++n )
571 {
572 if( !m_detectedStars[n].hasProp() )
573 {
574 continue;
575 }
576
579 updateIfChanged( m_detectedStars[n].prop(), "peak", m_detectedStars[n].max );
580 updateIfChanged( m_detectedStars[n].prop(), "fwhm", m_detectedStars[n].fwhm );
581 }
582 return 0;
583}
584
593
595{
596 static_cast<void>( dummy );
597
598 std::lock_guard<std::mutex> guard( m_imageMutex );
599
601 m_image.setZero();
602
603 m_sm.resize( m_image.rows(), m_image.cols() );
604
605 m_updated = false;
606 return 0;
607}
608
609// Function to calculate Euclidean distance between two stars
610inline float calculateDistance( float x1, float y1, float x2, float y2 )
611{
612 return std::sqrt( ( x2 - x1 ) * ( x2 - x1 ) + ( y2 - y1 ) * ( y2 - y1 ) );
613}
614
616{
617 // Pause acquisition updates while telescope nudge is in progress.
618 if( mx::sys::get_curr_time() - m_acqQuitTime < m_acqPauseTime )
619 {
620 return 0;
621 }
622
623 static_cast<void>( dummy );
624
625 if( curr_src == nullptr )
626 {
627 return log<software_error, -1>( { __FILE__, __LINE__, "received null image pointer" } );
628 }
629
630 mx::improc::eigenImage<float> workingImage;
631 {
632 std::unique_lock<std::mutex> lock( m_imageMutex ); //mutex scope
633
634 if( m_image.rows() <= 0 || m_image.cols() <= 0 )
635 {
636 return 0;
637 }
638
639 const std::size_t pixelCount =
640 static_cast<std::size_t>( shmimMonitorT::m_width ) * static_cast<std::size_t>( shmimMonitorT::m_height );
641 if( pixelCount == 0 )
642 {
643 return 0;
644 }
645
646 const float *src = static_cast<const float *>( curr_src );
647 if( m_dark.rows() == m_image.rows() && m_dark.cols() == m_image.cols() )
648 {
649 for( std::size_t nn = 0; nn < pixelCount; ++nn )
650 {
651 m_image.data()[nn] = src[nn] - m_dark.data()[nn];
652 }
653 }
654 else
655 {
656 for( std::size_t nn = 0; nn < pixelCount; ++nn )
657 {
658 m_image.data()[nn] = src[nn];
659 }
660 }
661
663 }
664
665 const int imageRows = workingImage.rows();
666 const int imageCols = workingImage.cols();
667 if( imageRows <= 0 || imageCols <= 0 )
668 {
669 return 0;
670 }
671
672 if( m_zero_area <= 0 || imageRows < 2 * m_zero_area + 1 || imageCols < 2 * m_zero_area + 1 )
673 {
674 return 0;
675 }
676
677 int maxRow = 0;
678 int maxCol = 0;
679 float maxValue = workingImage.maxCoeff( &maxRow, &maxCol );
680
681 const int noiseRows = std::min( imageRows, 32 );
682 const int noiseCols = std::min( imageCols, 32 );
684 float mean = noiseBlock.mean();
685 float variance = ( noiseBlock.array() - mean ).square().sum() / noiseBlock.size();
686 variance = std::max( variance, 0.0f );
687 float stddev = std::sqrt( variance );
688 if( !std::isfinite( stddev ) || stddev <= std::numeric_limits<float>::epsilon() )
689 {
690 return 0;
691 }
692
693 float zScore = ( maxValue - mean ) / stddev;
694 if( !std::isfinite( zScore ) )
695 {
696 return 0;
697 }
698
699 float fwhm = m_fwhm_threshold + 1;
700 int loopCount = 0;
701
702 bool sendNudge = false;
703 double nudgeXArcsec = 0;
704 double nudgeYArcsec = 0;
705 std::vector<starTelemSample> loopExitTelemSamples;
706 const int maxTrackedStars = std::max( m_max_loops, 0 );
707
708 { //mutex scope
709 std::unique_lock<std::mutex> lock( m_indiMutex );
710
711 std::vector<bool> starUpdatedThisFrame( m_detectedStars.size(), false );
712 auto addStar = [&]( float starX, float starY, float peak, float starFwhm, float starSeeing ) {
715 newStar.x = starX;
716 newStar.y = starY;
717 newStar.max = peak;
718 newStar.fwhm = starFwhm;
719 newStar.seeing = starSeeing;
720 m_detectedStars.push_back( std::move( newStar ) );
721 starUpdatedThisFrame.push_back( true );
722 };
723
725 {
726 ++loopCount;
727 m_gfit.set_itmax( 1000 );
728
729 maxRow = std::clamp( maxRow, m_zero_area, imageRows - m_zero_area );
730 maxCol = std::clamp( maxCol, m_zero_area, imageCols - m_zero_area );
731
735 m_zero_area * 2,
736 m_zero_area * 2 );
737 m_gfit.setArray( subImage.data(), subImage.rows(), subImage.cols() );
738 m_gfit.setGuess( 0, maxValue, m_zero_area, m_zero_area, mx::math::func::fwhm2sigma( m_fwhmGuess ) );
739 m_gfit.fit();
740
741 fwhm = m_gfit.fwhm();
742 maxValue = m_gfit.G();
743 if( !std::isfinite( fwhm ) || !std::isfinite( maxValue ) )
744 {
745 break;
746 }
747
748 float starSeeing = fwhm * m_plate_scale;
749 m_x = ( maxRow - m_zero_area ) + m_gfit.x0();
750 m_y = ( maxCol - m_zero_area ) + m_gfit.y0();
751 if( !std::isfinite( m_x ) || !std::isfinite( m_y ) || !std::isfinite( starSeeing ) )
752 {
753 break;
754 }
755
756 m_first_x_vals.push_back( m_x );
757 m_first_y_vals.push_back( m_y );
758
759 int starRow = std::clamp( static_cast<int>( m_x ), m_zero_area, imageRows - m_zero_area );
760 int starCol = std::clamp( static_cast<int>( m_y ), m_zero_area, imageCols - m_zero_area );
761
762 // Only detections that satisfy configured thresholds may update or create tracked stars.
763 const bool passesStarThresholds =
764 ( zScore > m_threshold ) && ( fwhm > m_fwhm_threshold ) && ( fwhm < m_max_fwhm ) &&
765 std::isfinite( maxValue );
766
768 {
769 constexpr int thresholdDistance = 20;
770 bool matchedKnownStar = false;
771
772 for( std::size_t starIndex = 0; starIndex < m_detectedStars.size(); ++starIndex )
773 {
775 float dist = calculateDistance( star.x, star.y, starRow, starCol );
776 if( dist >= thresholdDistance )
777 {
778 continue;
779 }
780
781 star.x = m_x;
782 star.y = m_y;
783 star.max = maxValue;
784 star.fwhm = fwhm;
785 star.seeing = starSeeing;
787 matchedKnownStar = true;
788 break;
789 }
790
791 if( !matchedKnownStar && static_cast<int>( m_detectedStars.size() ) < maxTrackedStars )
792 {
793 addStar( m_x, m_y, maxValue, fwhm, starSeeing );
794 }
795 }
796
797 for( int rr = starRow - m_zero_area; rr < starRow + m_zero_area; ++rr )
798 {
799 for( int cc = starCol - m_zero_area; cc < starCol + m_zero_area; ++cc )
800 {
801 workingImage( rr, cc ) = 0;
802 }
803 }
804
805 maxValue = workingImage.maxCoeff( &maxRow, &maxCol );
806 zScore = ( maxValue - mean ) / stddev;
807 if( !std::isfinite( zScore ) )
808 {
809 break;
810 }
811 }
812
813 // Drop a tracked star as soon as it misses one full frame update.
814 constexpr int maxMissedFrames = 1;
815 for( std::size_t n = m_detectedStars.size(); n > 0; --n )
816 {
817 std::size_t starIndex = n - 1;
818
820 {
821 m_detectedStars[starIndex].missedFrames = 0;
822 continue;
823 }
824
825 ++m_detectedStars[starIndex].missedFrames;
826 if( m_detectedStars[starIndex].missedFrames >= maxMissedFrames )
827 {
829 }
830 }
831
833
834 const int starCount = static_cast<int>( m_detectedStars.size() );
836 {
837 m_acquire_star = -1;
838 }
839
841 {
842 m_acqQuitTime = mx::sys::get_curr_time();
844
845 int deltaX = static_cast<int>( m_detectedStars[m_acquire_star].x ) - m_x_center;
846 int deltaY = static_cast<int>( m_detectedStars[m_acquire_star].y ) - m_y_center;
847
848 // Negative signs move the scope opposite the current star offset.
849 nudgeXArcsec = -1 * static_cast<double>( deltaY ) * m_plate_scale;
850 nudgeYArcsec = -1 * static_cast<double>( deltaX ) * m_plate_scale;
851 sendNudge = true;
852
853 resetAcq();
854 m_acquire_star = -1;
855 }
856
857 m_num_stars = static_cast<int>( m_detectedStars.size() );
858 if( m_num_stars > 0 )
859 {
860 m_seeing_star = 0;
862 m_seeing = m_detectedStars[0].seeing;
863 if( !std::isfinite( m_seeing ) )
864 {
865 m_seeing = 0;
866 }
867
868 loopExitTelemSamples.reserve( m_detectedStars.size() );
869 for( const auto &star : m_detectedStars )
870 {
871 if( !std::isfinite( star.x ) || !std::isfinite( star.y ) || !std::isfinite( star.max ) ||
872 !std::isfinite( star.fwhm ) || !std::isfinite( star.seeing ) )
873 {
874 continue;
875 }
876
877 loopExitTelemSamples.push_back( { star.x, star.y, star.max, star.fwhm, star.seeing } );
878 }
879 }
880 else
881 {
882 m_seeing = 0;
883 m_seeing_star = -1;
885 }
886
887 m_updated = true;
888 }
889
890 if( !loopExitTelemSamples.empty() )
891 {
892 double now = mx::sys::get_curr_time();
893 if( now - m_lastLoopExitTelemTime >= 1.0 )
894 {
896 {
897 return -1;
898 }
899
901 }
902 }
903
904 if( sendNudge )
905 {
906 pcf::IndiProperty ip( pcf::IndiProperty::Number );
907 ip.setDevice( "tcsi" );
908 ip.setName( "pyrNudge" );
909 ip.add( pcf::IndiElement( "y" ) );
910 ip["y"] = nudgeXArcsec;
911 ip.add( pcf::IndiElement( "x" ) );
912 ip["x"] = nudgeYArcsec;
914 }
915
916 return 0;
917}
918
920{
921 static_cast<void>( dummy );
922
923 std::lock_guard<std::mutex> guard( m_imageMutex );
924
926 {
927 return log<software_error, -1>( { __FILE__, __LINE__, "dark is not float" } );
928 }
929
931 m_dark.setZero();
932
933 return 0;
934}
935
937{
938 static_cast<void>( dummy );
939
940 if( curr_src == nullptr )
941 {
942 return log<software_error, -1>( { __FILE__, __LINE__, "received null dark image pointer" } );
943 }
944
945 std::unique_lock<std::mutex> lock( m_imageMutex );
946 const float *src = static_cast<const float *>( curr_src );
947
949 {
950 m_dark.data()[nn] += src[nn];
951 }
952
953 lock.unlock();
954
955 log<text_log>( "dark updated", logPrio::LOG_INFO );
956
957 return 0;
958}
959
961{
962 return 0;
963}
964
965inline int psfAcq::emitStarTelemetry( const std::vector<starTelemSample> &starTelemetryValues )
966{
967 if( starTelemetryValues.empty() )
968 {
969 return 0;
970 }
971
972 int numStars = static_cast<int>( starTelemetryValues.size() );
973 for( std::size_t index = 0; index < starTelemetryValues.size(); ++index )
974 {
975 const auto &sample = starTelemetryValues[index];
976 int starNo = static_cast<int>( index ) + 1;
977
979 { starNo, numStars, sample.x_pos, sample.y_pos, sample.m_pix, sample.fwhm, sample.seeing } ) < 0 )
980 {
981 return -1;
982 }
983 }
984
985 return 0;
986}
987
989{
990 static_cast<void>( telemTag );
991
992 std::vector<starTelemSample> starTelemetryValues;
993 { //mutex scope
994 std::lock_guard<std::mutex> guard( m_indiMutex );
995
996 if( m_detectedStars.empty() )
997 {
998 return 0;
999 }
1000
1001 starTelemetryValues.reserve( m_detectedStars.size() );
1002 for( const auto &star : m_detectedStars )
1003 {
1004 if( !std::isfinite( star.x ) || !std::isfinite( star.y ) || !std::isfinite( star.max ) ||
1005 !std::isfinite( star.fwhm ) || !std::isfinite( star.seeing ) )
1006 {
1007 continue;
1008 }
1009
1010 starTelemetryValues.push_back( { star.x, star.y, star.max, star.fwhm, star.seeing } );
1011 }
1012 }
1013
1015}
1016
1018{
1019 // Caller must hold m_indiMutex.
1020 if( star.hasProp() )
1021 {
1023 }
1024
1025 star.allocate();
1026
1027 std::string starPrefix = "star_" + std::to_string( rankIndex );
1029 star.prop(),
1030 starPrefix,
1031 "Star " + std::to_string( rankIndex ) + " Properties",
1032 "Star Acq" );
1033 star.prop().add( pcf::IndiElement( "x" ) );
1034 star.prop()["x"].set( star.x );
1035 star.prop().add( pcf::IndiElement( "y" ) );
1036 star.prop()["y"].set( star.y );
1037 star.prop().add( pcf::IndiElement( "peak" ) );
1038 star.prop()["peak"].set( star.max );
1039 star.prop().add( pcf::IndiElement( "fwhm" ) );
1040 star.prop()["fwhm"].set( star.fwhm );
1042}
1043
1045{
1046 // Caller must hold m_indiMutex.
1047 if( !star.hasProp() )
1048 {
1049 return;
1050 }
1051
1052 std::string uniqueKey = star.prop().createUniqueKey();
1053 if( m_indiDriver )
1054 {
1055 m_indiDriver->sendDelProperty( star.prop() );
1056 }
1057
1058 if( !uniqueKey.empty() && !m_indiNewCallBacks.erase( uniqueKey ) )
1059 {
1060 log<software_error>( { __FILE__, __LINE__, "failed to erase " + uniqueKey } );
1061 }
1062
1063 star.deallocate();
1064}
1065
1067{
1068 // Caller must hold m_indiMutex.
1069 std::sort(
1070 m_detectedStars.begin(),
1071 m_detectedStars.end(),
1072 []( const Star &lhs, const Star &rhs ) {
1073 const bool lhsFinite = std::isfinite( lhs.max );
1074 const bool rhsFinite = std::isfinite( rhs.max );
1075
1076 if( lhsFinite != rhsFinite )
1077 {
1078 return lhsFinite;
1079 }
1080
1081 if( lhsFinite && rhsFinite && lhs.max != rhs.max )
1082 {
1083 return lhs.max > rhs.max;
1084 }
1085
1086 return lhs.id < rhs.id;
1087 } );
1088
1089 bool labelsAlreadyMatch = true;
1090 for( std::size_t n = 0; n < m_detectedStars.size(); ++n )
1091 {
1092 if( !m_detectedStars[n].hasProp() )
1093 {
1094 labelsAlreadyMatch = false;
1095 break;
1096 }
1097
1098 std::string desiredName = "star_" + std::to_string( n );
1099 if( m_detectedStars[n].prop().getName() != desiredName )
1100 {
1101 labelsAlreadyMatch = false;
1102 break;
1103 }
1104 }
1105
1106 if( labelsAlreadyMatch )
1107 {
1108 return;
1109 }
1110
1111 for( auto &star : m_detectedStars )
1112 {
1114 }
1115
1116 for( std::size_t n = 0; n < m_detectedStars.size(); ++n )
1117 {
1119 }
1120}
1121
1122void psfAcq::removeStar( size_t index )
1123{
1124 // Caller must hold m_indiMutex.
1125 if( index >= m_detectedStars.size() )
1126 {
1127 return;
1128 }
1129
1131 m_detectedStars.erase( m_detectedStars.begin() + index );
1132}
1133
1134// Delete INDI properties for tracked stars and clear acquisition state.
1136{
1137 // Caller must hold m_indiMutex.
1138 for( size_t n = m_detectedStars.size(); n > 0; --n )
1139 {
1140 removeStar( n - 1 );
1141 }
1142
1143 m_num_stars = 0;
1144 m_seeing = 0;
1145 m_seeing_star = -1;
1146 m_current_acq_star = -1;
1147}
1148
1149INDI_SETCALLBACK_DEFN( psfAcq, m_indiP_flipAcqPresetName )( const pcf::IndiProperty &ipRecv )
1150{
1151 INDI_VALIDATE_CALLBACK_PROPS( m_indiP_flipAcqPresetName, ipRecv );
1152
1153 // Auto-restart on an `out` On->Off transition.
1154 // `flipacq.presetName` is a switch property with elements `in` and `out`.
1155 if( ipRecv.find( "out" ) != true )
1156 {
1157 return 0;
1158 }
1159
1160 std::unique_lock<std::mutex> lock( m_indiMutex );
1161
1162 auto outState = ipRecv["out"].getSwitchState();
1163 bool outIsOn = ( outState == pcf::IndiElement::On );
1164 bool outIsOff = ( outState == pcf::IndiElement::Off );
1165
1166 if( !outIsOn && !outIsOff )
1167 {
1168 return 0;
1169 }
1170
1171 if( m_flipAcqOutStateValid && m_flipAcqOutWasOn && outIsOff )
1172 {
1173 resetAcq();
1174 }
1175
1176 m_flipAcqOutWasOn = outIsOn;
1177 m_flipAcqOutStateValid = true;
1178
1179 return 0;
1180}
1181
1182//for toggling Restart Acquisition
1183INDI_NEWCALLBACK_DEFN( psfAcq, m_indiP_restartAcq )(const pcf::IndiProperty &ipRecv)
1184{
1185 INDI_VALIDATE_CALLBACK_PROPS(m_indiP_restartAcq, ipRecv);
1186 if(!ipRecv.find("request")) return 0;
1187 std::unique_lock<std::mutex> lock(m_indiMutex);
1188
1189 if( ipRecv["request"].getSwitchState() == pcf::IndiElement::On)
1190 {
1191 resetAcq();
1192 return 0;
1193 }
1194 else if( ipRecv["request"].getSwitchState() == pcf::IndiElement::Off)
1195 {
1196 return 0;
1197 }
1198
1199 log<software_error>({__FILE__,__LINE__, "switch state fall through."});
1200 return -1;
1201}
1202
1203// For toggling Recording Seeing
1204INDI_NEWCALLBACK_DEFN( psfAcq, m_indiP_recordSeeing )(const pcf::IndiProperty &ipRecv)
1205{
1206 INDI_VALIDATE_CALLBACK_PROPS(m_indiP_recordSeeing, ipRecv);
1207 if(!ipRecv.find("toggle")) return 0;
1208 std::unique_lock<std::mutex> lock(m_indiMutex);
1209
1210 // Seeing updates are automatic; this toggle is informational only.
1211 if( ipRecv["toggle"].getSwitchState() == pcf::IndiElement::On)
1212 {
1213 updateSwitchIfChanged(m_indiP_recordSeeing, "toggle", pcf::IndiElement::On, INDI_OK);
1214 return 0;
1215 }
1216 else if( ipRecv["toggle"].getSwitchState() == pcf::IndiElement::Off)
1217 {
1218 updateSwitchIfChanged(m_indiP_recordSeeing, "toggle", pcf::IndiElement::Off, INDI_IDLE);
1219 return 0;
1220 }
1221
1222
1223 log<software_error>({__FILE__,__LINE__, "switch state fall through."});
1224 return -1;
1225}
1226
1227// For user to select acquisition star number
1228INDI_NEWCALLBACK_DEFN( psfAcq, m_indiP_acquire_star )( const pcf::IndiProperty &ipRecv )
1229{
1230 if( ipRecv.getName() != m_indiP_acquire_star.getName() )
1231 {
1232 log<software_error>( { __FILE__, __LINE__, "wrong INDI property received." } );
1233 return -1;
1234 }
1235
1236 float target;
1237
1238 if( indiTargetUpdate( m_indiP_acquire_star, target, ipRecv, true ) < 0 )
1239 {
1240 log<software_error>( { __FILE__, __LINE__ } );
1241 return -1;
1242 }
1243
1244 { //mutex scope
1245 std::lock_guard<std::mutex> guard( m_indiMutex );
1246 m_acquire_star = static_cast<int>( target );
1247 }
1248
1249 log<text_log>( "set acquire_star = " + std::to_string( m_acquire_star ), logPrio::LOG_NOTICE );
1250 return 0;
1251}
1252
1253// For user to select seeing star number
1254INDI_NEWCALLBACK_DEFN( psfAcq, m_indiP_seeing_star )( const pcf::IndiProperty &ipRecv )
1255{
1256 if( ipRecv.getName() != m_indiP_seeing_star.getName() )
1257 {
1258 log<software_error>( { __FILE__, __LINE__, "wrong INDI property received." } );
1259 return -1;
1260 }
1261
1262 float target;
1263
1264 if( indiTargetUpdate( m_indiP_seeing_star, target, ipRecv, true ) < 0 )
1265 {
1266 log<software_error>( { __FILE__, __LINE__ } );
1267 return -1;
1268 }
1269
1270 static_cast<void>( target );
1271
1272 { //mutex scope
1273 std::lock_guard<std::mutex> guard( m_indiMutex );
1274
1275 // Force automatic seeing source selection.
1276 if( m_num_stars > 0 )
1277 {
1278 m_seeing_star = 0;
1279 }
1280 else
1281 {
1282 m_seeing_star = -1;
1283 }
1284 }
1285
1286 log<text_log>( "set seeing_star = " + std::to_string( m_seeing_star ), logPrio::LOG_NOTICE );
1287 return 0;
1288}
1289
1290INDI_SETCALLBACK_DEFN( psfAcq, m_indiP_fpsSource )( const pcf::IndiProperty &ipRecv )
1291{
1292 if( ipRecv.getName() != m_indiP_fpsSource.getName() )
1293 {
1294 log<software_error>( { __FILE__, __LINE__, "Invalid INDI property." } );
1295 return -1;
1296 }
1297
1298 if( ipRecv.find( "current" ) != true ) // this isn't valie
1299 {
1300 return 0;
1301 }
1302
1303 std::lock_guard<std::mutex> guard( m_indiMutex );
1304
1305 realT fps = ipRecv["current"].get<float>();
1306
1307 if( fps != m_fps )
1308 {
1309 m_fps = fps;
1310 shmimMonitorT::m_restart = true;
1311 }
1312
1313 return 0;
1314}
1315
1316} // namespace app
1317} // namespace MagAOX
1318
1319#endif // psfAcq_hpp
#define IMAGESTRUCT_FLOAT
The base-class for XWCTk applications.
void updateIfChanged(pcf::IndiProperty &p, const std::string &el, const T &newVal, pcf::IndiProperty::PropertyStateType ipState=pcf::IndiProperty::Ok)
Update an INDI property element value if it has changed.
int createStandardIndiRequestSw(pcf::IndiProperty &prop, const std::string &name, const std::string &label="", const std::string &group="")
Create a standard R/W INDI switch with a single request element.
stateCodes::stateCodeT state()
Get the current state code.
int registerIndiPropertyNew(pcf::IndiProperty &prop, int(*)(void *, const pcf::IndiProperty &))
Register an INDI property which is exposed for others to request a New Property for.
int createStandardIndiToggleSw(pcf::IndiProperty &prop, const std::string &name, const std::string &label="", const std::string &group="")
Create a standard R/W INDI switch with a single toggle element.
std::unordered_map< std::string, indiCallBack > m_indiNewCallBacks
Map to hold the NewProperty indiCallBacks for this App, with fast lookup by property name.
indiDriver< MagAOXApp > * m_indiDriver
The INDI driver wrapper. Constructed and initialized by execute, which starts and stops communication...
static int log(const typename logT::messageT &msg, logPrioT level=logPrio::LOG_DEFAULT)
Make a log entry.
int createROIndiNumber(pcf::IndiProperty &prop, const std::string &propName, const std::string &propLabel="", const std::string &propGroup="")
Create a ReadOnly INDI Number property.
int registerIndiPropertyReadOnly(pcf::IndiProperty &prop)
Register an INDI property which is read only.
std::mutex m_indiMutex
Mutex for locking INDI communications.
int sendNewProperty(const pcf::IndiProperty &ipSend, const std::string &el, const T &newVal)
Send a newProperty command to another device (using the INDI Client interface)
uint32_t m_width
The width of the images in the stream.
int setupConfig(mx::app::appConfigurator &config)
Setup the configuration system.
int updateINDI()
Update the INDI properties for this device controller.
int appLogic()
Checks the shmimMonitor thread.
uint32_t m_height
The height of the images in the stream.
int appShutdown()
Shuts down the shmimMonitor thread.
uint8_t m_dataType
The ImageStreamIO type code.
int loadConfig(mx::app::appConfigurator &config)
load the configuration system results
The MagAO-X PSF Fitter.
Definition psfAcq.hpp:141
pcf::IndiProperty m_indiP_seeing_star
Definition psfAcq.hpp:352
mx::improc::eigenImage< float > m_sm
Definition psfAcq.hpp:181
double m_lastLoopExitTelemTime
Last successful loop-exit telemetry dump time in seconds.
Definition psfAcq.hpp:229
int loadConfigImpl(mx::app::appConfigurator &_config)
Implementation of loadConfig logic, separated for testing.
Definition psfAcq.hpp:463
virtual void loadConfig()
Definition psfAcq.hpp:482
void registerStarProperty(Star &star, std::size_t rankIndex)
Register one tracked star's read-only INDI property using a rank-based label.
Definition psfAcq.hpp:1017
mx::improc::eigenImage< float > m_image
Definition psfAcq.hpp:180
std::string m_fpsSource
Definition psfAcq.hpp:171
dev::shmimMonitor< psfAcq, darkShmimT > darkShmimMonitorT
Definition psfAcq.hpp:153
psfAcq()
Default c'tor.
Definition psfAcq.hpp:377
int processImage(void *curr_src, const dev::shmimT &)
Definition psfAcq.hpp:615
pcf::IndiProperty m_indiP_num_stars
Definition psfAcq.hpp:336
float realT
Floating point type in which to do all calculations.
Definition psfAcq.hpp:159
virtual int appLogic()
Implementation of the FSM for psfAcq.
Definition psfAcq.hpp:548
friend class psfAcq_test
Definition psfAcq.hpp:143
std::vector< float > m_first_x_vals
Definition psfAcq.hpp:200
std::mutex m_imageMutex
Definition psfAcq.hpp:329
pcf::IndiProperty m_indiP_flipAcqPresetName
Subscription to flipacq.presetName switch updates.
Definition psfAcq.hpp:344
void removeStar(size_t index)
Remove one tracked star and its INDI property/callback registration.
Definition psfAcq.hpp:1122
int emitStarTelemetry(const std::vector< starTelemSample > &starTelemetryValues)
Emit one telemetry record per star sample.
Definition psfAcq.hpp:965
uint16_t m_fitCircBuffMaxLength
Maximum length of the latency measurement circular buffers.
Definition psfAcq.hpp:174
float y_pos
Star y position in pixels.
Definition psfAcq.hpp:238
virtual int appStartup()
Startup function.
Definition psfAcq.hpp:487
~psfAcq() noexcept
D'tor, declared and defined for noexcept.
Definition psfAcq.hpp:383
int checkRecordTimes()
No-op scheduler hook; loop-exit logic emits telemetry directly.
Definition psfAcq.hpp:960
mx::improc::eigenImage< float > m_dark
Definition psfAcq.hpp:183
pcf::IndiProperty m_indiP_seeing
Definition psfAcq.hpp:338
pcf::IndiProperty m_indiP_recordSeeing
Definition psfAcq.hpp:359
std::size_t m_nextStarId
Monotonic id counter used to stamp new stars for stable ordering tie-breaks.
Definition psfAcq.hpp:206
float seeing
Seeing estimate in arcseconds.
Definition psfAcq.hpp:247
virtual void setupConfig()
Definition psfAcq.hpp:391
int allocate(const dev::shmimT &)
Definition psfAcq.hpp:594
virtual int appShutdown()
Shutdown the app.
Definition psfAcq.hpp:585
mx::math::fit::fitGaussian2Dsym< float > m_gfit
Definition psfAcq.hpp:280
int recordTelem(const telem_psfacq *telemTag)
Record telemetry for all properties of each detected star.
Definition psfAcq.hpp:988
dev::shmimMonitor< psfAcq > shmimMonitorT
Definition psfAcq.hpp:151
float m_fitCircBuffMaxTime
Maximum time of the latency meaurement circular buffers.
Definition psfAcq.hpp:175
void relabelStarsByBrightness()
Sort tracked stars by brightness and ensure labels are star_0, star_1, ...
Definition psfAcq.hpp:1066
float m_pix
Peak pixel value.
Definition psfAcq.hpp:241
pcf::IndiProperty m_indiP_fpsSource
Definition psfAcq.hpp:340
void resetAcq()
Delete star INDI properties and reset tracked acquisition stars.
Definition psfAcq.hpp:1135
dev::telemeter< psfAcq > telemeterT
Definition psfAcq.hpp:156
std::vector< float > m_first_y_vals
Definition psfAcq.hpp:201
pcf::IndiProperty m_indiP_acquire_star
Definition psfAcq.hpp:348
std::vector< Star > m_detectedStars
Tracked stars with associated INDI properties.
Definition psfAcq.hpp:203
void unregisterStarProperty(Star &star)
Remove one tracked star's INDI property without erasing the star state.
Definition psfAcq.hpp:1044
bool m_flipAcqOutWasOn
Last observed flipacq.presetName.out switch state.
Definition psfAcq.hpp:223
float x_pos
Star x position in pixels.
Definition psfAcq.hpp:235
pcf::IndiProperty m_indiP_restartAcq
Definition psfAcq.hpp:356
bool m_flipAcqOutStateValid
True once m_flipAcqOutWasOn has been initialized from INDI.
Definition psfAcq.hpp:226
Snapshot of one star's telemetry fields for deferred emission.
Definition psfAcq.hpp:233
#define protected
#define INDI_NEWCALLBACK_DEFN(class, prop)
Define the callback for a new property request.
#define INDI_NEWCALLBACK(prop)
Get the name of the static callback wrapper for a new property.
#define INDI_SETCALLBACK_DECL(class, prop)
Declare the callback for a set property request, and declare and define the static wrapper.
#define CREATE_REG_INDI_NEW_NUMBERF(prop, name, min, max, step, format, label, group)
Create and register a NEW INDI property as a standard number as float, using the standard callback na...
#define INDI_SETCALLBACK_DEFN(class, prop)
Define the callback for a set property request.
#define REG_INDI_SETPROP(prop, devName, propName)
Register a SET INDI property with the class, using the standard callback name.
#define INDI_NEWCALLBACK_DECL(class, prop)
Declare the callback for a new property request, and declare and define the static wrapper.
#define INDI_VALIDATE_CALLBACK_PROPS(prop1, prop2)
Standard check for matching INDI properties in a callback.
#define INDI_IDLE
Definition indiUtils.hpp:27
#define INDI_OK
Definition indiUtils.hpp:28
const pcf::IndiProperty & ipRecv
float calculateDistance(float x1, float y1, float x2, float y2)
Definition psfAcq.hpp:610
std::unique_lock< std::mutex > lock(m_indiMutex)
Definition dm.hpp:19
static constexpr logPrioT LOG_NOTICE
A normal but significant condition.
static constexpr logPrioT LOG_INFO
Informational. The info log level is the lowest level recorded during normal operations.
bool hasProp() const
Check whether an INDI property has been allocated.
Definition psfAcq.hpp:112
Star(const Star &)=delete
Disable copy constructor because the star owns a unique INDI property instance.
float x
Star row coordinate in image pixel space.
Definition psfAcq.hpp:62
std::size_t id
Monotonic identifier used as a stable tie-breaker across equal-brightness stars.
Definition psfAcq.hpp:59
void deallocate()
Release the star's INDI property.
Definition psfAcq.hpp:127
Star(Star &&) noexcept=default
Enable move constructor.
Star & operator=(const Star &)=delete
Disable copy assignment because the star owns a unique INDI property instance.
int missedFrames
Consecutive frames where this star was not updated.
Definition psfAcq.hpp:77
float y
Star column coordinate in image pixel space.
Definition psfAcq.hpp:65
float max
Peak pixel value of the fitted star.
Definition psfAcq.hpp:68
void allocate()
Allocate the star's INDI property if needed.
Definition psfAcq.hpp:118
float seeing
Derived seeing value in arcseconds.
Definition psfAcq.hpp:74
std::unique_ptr< pcf::IndiProperty > m_prop
Owned INDI property for this star's exported fit values.
Definition psfAcq.hpp:82
pcf::IndiProperty & prop()
Access the owned INDI property.
Definition psfAcq.hpp:101
float fwhm
FWHM returned by the Gaussian fit in pixels.
Definition psfAcq.hpp:71
Star()=default
Default constructor.
static std::string configSection()
Definition psfAcq.hpp:43
static std::string indiPrefix()
Definition psfAcq.hpp:48
A device base class which saves telemetry.
Definition telemeter.hpp:75
@ OPERATING
The device is operating, other than homing.
Software ERR log entry.
Log entry recording psf acquisition per-star properties.
#define TELEMETER_LOAD_CONFIG(cfig)
Call telemeter::loadConfig with error checking.
#define TELEMETER_APP_STARTUP
Call telemeter::appStartup with error checking.
#define TELEMETER_SETUP_CONFIG(cfig)
Call telemeter::setupConfig with error checking.
#define TELEMETER_APP_SHUTDOWN
Call telemeter::appShutdown with error checking.
double variance(double data[], int size, int num_skip=0)