File indexing completed on 2024-05-12 03:44:33

0001 /*
0002     SPDX-FileCopyrightText: 2023 Joseph McGee <joseph.mcgee@sbcglobal.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include <QLoggingCategory>
0008 #include <cmath>
0009 #include "optimalsubexposurecalculator.h"
0010 #include "imagingcameradata.h"
0011 #include "calculatedgainsubexposuretime.h"
0012 #include "cameraexposureenvelope.h"
0013 #include "optimalexposuredetail.h"
0014 #include <ekos_capture_debug.h>
0015 
0016 namespace OptimalExposure
0017 {
0018 
0019 /*
0020  *  For UI presentation provide two calculation methods.
0021  *  1) A method for the overall calculation for
0022  *  exposure times over the range of gain read-noise pairs.
0023  *      This method would be called when changes are made to:
0024  *          Camera selection, Focal Ratio, Sky Quality, Filter Compensation, or Noise Tolerance
0025  *      This method will return an obect (CameraExposureEnvelope) containing (for ui presentation)
0026  *          lightPollutionElectronBaseRate, lightPollutionForOpticFocalRatio,
0027  *          a vector of gain sub-exposure pairs, and int values the max and min exposure time.
0028  *      This method is intented to be used to present the graphical display of exposures for the camera,
0029  *      based on a specifc SQM.  The SQM can be adjusted in the UI, and an this method will be used to refresh ui.
0030  *
0031  *  2) A method for a calculation of an exposure time (OptimalExposureDetail) at a specific gain.
0032  *      This method is intented to be used to present a specific exposure calculation, and
0033  *      resulting shot and stack noise information.  This method will interpolate for a
0034  *      read-noise value (between camera gain read-noise pairs), and will be called when
0035  *      a change is made to selected Gain on the UI.
0036  *      This method will return an object that conains:
0037  *          Precise Exposure Time in (interpolated), Shot pollution electrons, Exposure shot noise, Exposure total noise
0038  *          a Vector for stacks of 1 to n hours, which includes Exposure Count, Stack Total Time, Stack total noise
0039  *
0040  */
0041 
0042 /*
0043  *
0044  * More work is needed for the calculation of effect of a Filter on exposure noise and time.
0045  * Original effort (in Java) used an estimage of the spectrum bandwidth passed by a filter. For
0046  * example: on broadband filters for a one shot color camera:
0047  *      Optolong l-Pro apprears to pass about 165 nanometers.
0048  *      Optolong l-Enhance apprears to pass only about 33 nanometers.
0049  *
0050  * In this code the filter compensation has is applied as a reducer of the calulation of the
0051  * light pollution rate for the optic. For example, the filter compensation to the light pollution
0052  * would be 165 / 300 for an l-Pro filter.
0053  *
0054  * But this filter compensatoin approach is imprecise & and does not reflect reality. It might
0055  * be better to analyze a filter for its ability to block the distinct emmission lines found
0056  * in light pollution:
0057  *
0058  *      Hg  435.8, 546.1, 577, 578.1
0059  *      Na  598, 589.6, 615.4, 616.1
0060  *
0061  * And finally, the use of 300 nanometers (the bandwidth of the visible light spectrum) as the
0062  * basis for filter compensation may not be accurate. An unfiltered camera sensor may be able
0063  * to "see" a spectrum that is much more broad.  Is an unfiltered sensor collecting more noise
0064  * from beyond the range of the visible spectrum?  But other discussions on web forums regarding
0065  * Dr. Glovers calculations appear to use 300 as the basis for filter compensation.
0066  *
0067  */
0068 
0069 OptimalSubExposureCalculator::OptimalSubExposureCalculator(
0070     double aNoiseTolerance, double aSkyQuality, double aFocalRatio, double aFilterCompensation,
0071     ImagingCameraData &anImagingCameraData) :
0072     aNoiseTolerance(aNoiseTolerance),
0073     aSkyQuality(aSkyQuality),
0074     aFocalRatio(aFocalRatio),
0075     aFilterCompensation(aFilterCompensation),
0076     anImagingCameraData(anImagingCameraData) {}
0077 
0078 
0079 
0080 double OptimalSubExposureCalculator::getASkyQuality()
0081 {
0082     return aSkyQuality;
0083 }
0084 
0085 void OptimalSubExposureCalculator::setASkyQuality(double newASkyQuality)
0086 {
0087     aSkyQuality = newASkyQuality;
0088 }
0089 
0090 double OptimalSubExposureCalculator::getANoiseTolerance()
0091 {
0092     return aNoiseTolerance;
0093 }
0094 
0095 void OptimalSubExposureCalculator::setANoiseTolerance(double newANoiseTolerance)
0096 {
0097     aNoiseTolerance = newANoiseTolerance;
0098 }
0099 
0100 double OptimalSubExposureCalculator::getAFocalRatio()
0101 {
0102     return aFocalRatio;
0103 }
0104 
0105 void OptimalSubExposureCalculator::setAFocalRatio(double newAFocalRatio)
0106 {
0107     aFocalRatio = newAFocalRatio;
0108 }
0109 
0110 double OptimalSubExposureCalculator::getAFilterCompensation()
0111 {
0112     return aFilterCompensation;
0113 }
0114 
0115 void OptimalSubExposureCalculator::setAFilterCompensation(double newAFilterCompensation)
0116 {
0117     aFilterCompensation = newAFilterCompensation;
0118 }
0119 
0120 ImagingCameraData &OptimalSubExposureCalculator::getImagingCameraData()
0121 {
0122     return anImagingCameraData;
0123 }
0124 
0125 void OptimalSubExposureCalculator::setImagingCameraData(ImagingCameraData &newanImagingCameraData)
0126 {
0127     anImagingCameraData = newanImagingCameraData;
0128 }
0129 
0130 int OptimalSubExposureCalculator::getASelectedGain()
0131 {
0132     return aSelectedGain;
0133 }
0134 
0135 void OptimalSubExposureCalculator::setASelectedGain(int newASelectedGain)
0136 {
0137     aSelectedGain = newASelectedGain;
0138 }
0139 
0140 int OptimalSubExposureCalculator::getASelectedCameraReadMode() const
0141 {
0142     return aSelectedCameraReadMode;
0143 }
0144 
0145 void OptimalSubExposureCalculator::setASelectedCameraReadMode(int newASelectedCameraReadMode)
0146 {
0147     aSelectedCameraReadMode = newASelectedCameraReadMode;
0148 }
0149 
0150 
0151 
0152 QVector<CalculatedGainSubExposureTime> OptimalSubExposureCalculator::calculateGainSubExposureVector(double cFactor,
0153         double lightPollutionForOpticFocalRatio)
0154 {
0155     // qCInfo(KSTARS_EKOS_CAPTURE) << "Calculating gain sub-exposure vector: ";
0156     QVector<CalculatedGainSubExposureTime> aCalculatedGainSubExposureTimeVector;
0157 
0158     OptimalExposure::CameraGainReadMode aSelectedReadMode =
0159         anImagingCameraData.getCameraGainReadModeVector()[aSelectedCameraReadMode];
0160     // qCInfo(KSTARS_EKOS_CAPTURE) << "\t with camera read mode: "
0161     //  << aSelectedReadMode.getCameraGainReadModeName();
0162 
0163     QVector<OptimalExposure::CameraGainReadNoise> aCameraGainReadNoiseVector
0164         = aSelectedReadMode.getCameraGainReadNoiseVector();
0165 
0166     for(QVector<OptimalExposure::CameraGainReadNoise>::iterator rn = aCameraGainReadNoiseVector.begin();
0167             rn != aCameraGainReadNoiseVector.end(); ++rn)
0168     {
0169         int aGain = rn->getGain();
0170         // qCInfo(KSTARS_EKOS_CAPTURE) << "At a gain of: " << aGain;
0171         double aReadNoise = rn->getReadNoise();
0172         // qCInfo(KSTARS_EKOS_CAPTURE) << "\t camera read-noise is: " << aReadNoise;
0173 
0174         // initial sub exposure.
0175         double anOptimalSubExposure = 0.0;
0176 
0177 
0178         switch(anImagingCameraData.getSensorType())
0179         {
0180             case SENSORTYPE_MONOCHROME:
0181                 anOptimalSubExposure =  cFactor * pow(aReadNoise, 2) / lightPollutionForOpticFocalRatio;
0182                 break;
0183 
0184             case SENSORTYPE_COLOR:
0185                 anOptimalSubExposure = (double)3.0 * cFactor * pow(aReadNoise, 2) / lightPollutionForOpticFocalRatio;
0186                 break;
0187         }
0188 
0189         // qCInfo(KSTARS_EKOS_CAPTURE) << "anOptimalSubExposure is: " << anOptimalSubExposure;
0190 
0191         CalculatedGainSubExposureTime *aSubExposureTime = new CalculatedGainSubExposureTime(aGain, anOptimalSubExposure);
0192 
0193         aCalculatedGainSubExposureTimeVector.append(*aSubExposureTime);
0194 
0195     }
0196 
0197     return aCalculatedGainSubExposureTimeVector;
0198 }
0199 /*
0200     double OptimalSubExposureCalculator::calculateLightPolutionForOpticFocalRatio(double lightPollutionElectronBaseRate, double aFocalRatio) {
0201     // double lightPollutionRateForOptic =  lightPollutionElectronBaseRate * java.lang.Math.pow(anOptic.getFocalRatio(),-2);
0202         return((double) (lightPollutionElectronBaseRate * pow(aFocalRatio,-2)));
0203     }
0204 */
0205 
0206 double OptimalSubExposureCalculator::calculateLightPolutionForOpticFocalRatio(double lightPollutionElectronBaseRate,
0207         double aFocalRatio, double aFilterCompensation)
0208 {
0209     // double lightPollutionRateForOptic =
0210     //  lightPollutionElectronBaseRate
0211     //  * java.lang.Math.pow(anOptic.getFocalRatio(),-2);
0212     return(((double) (lightPollutionElectronBaseRate * pow(aFocalRatio, -2))) * aFilterCompensation);
0213 }
0214 
0215 double OptimalSubExposureCalculator::calculateCFactor(double aNoiseTolerance)
0216 {
0217     // cFactor = 1/( (((100+allowableNoiseIncreasePercent)/100)^2) -1)
0218 
0219     return((double) (1 / ( pow((((double)100 + (double) aNoiseTolerance) / (double)100), (double)2) - 1)));
0220 
0221 }
0222 
0223 double OptimalSubExposureCalculator::calculateLightPollutionElectronBaseRate(double skyQuality)
0224 {
0225     // Conversion curve fitted from Dr Glover data
0226     double base = std::stod("1.25286030612621E+27");
0227     double power = (double) -19.3234809465887;
0228 
0229     // New version of Dr Glover's function calculates the Electron rate at thet pixel level
0230     // and requires pixel size in microns and QE of sensor.
0231     // double baseV2 = 1009110388.7838
0232     // Calculation of an initial electron rate will
0233     // use a new fitted curve based upon a 1.0 micon pixel and 100% QE
0234     // curve appears to be f(sqm) = 1009110388.7838 exp (-0.921471189594521 * sqm)
0235     //
0236 
0237 
0238     return(base * pow(skyQuality, power));
0239 }
0240 
0241 OptimalExposure::CameraExposureEnvelope OptimalSubExposureCalculator::calculateCameraExposureEnvelope()
0242 {
0243     /*
0244         This method calculates the exposures for each gain setting found in the camera gain readnoise table.
0245         It is used to refresh the ui presentation, in prparation for a calculation of sub-exposure data with a
0246         specific (probably interpolated) read-noise value.
0247     */
0248 
0249     double lightPollutionElectronBaseRate = OptimalSubExposureCalculator::calculateLightPollutionElectronBaseRate(aSkyQuality);
0250     /*
0251     qCInfo(KSTARS_EKOS_CAPTURE) << "Calculating CameraExposureEnvelope..."
0252                                 << "Using Sky Quality: " << aSkyQuality
0253                                 << "\nConverted to lightPollutionElectronBaseRate: " << lightPollutionElectronBaseRate
0254                                 << "Using an Optical Focal Ratio: " << aFocalRatio;
0255     */
0256     double lightPollutionForOpticFocalRatio = calculateLightPolutionForOpticFocalRatio(lightPollutionElectronBaseRate,
0257             aFocalRatio, aFilterCompensation);
0258     /*
0259     qCInfo(KSTARS_EKOS_CAPTURE)
0260             << "\tResulting in an Light Pollution Rate for the Optic of: "
0261             << lightPollutionForOpticFocalRatio;
0262     */
0263     // qCDebug(KSTARS_EKOS_CAPTURE) << "Using a camera Id: " << anImagingCameraData.getCameraId();
0264     OptimalExposure::CameraGainReadMode aSelectedReadMode =
0265         anImagingCameraData.getCameraGainReadModeVector()[aSelectedCameraReadMode];
0266     /*
0267     qCInfo(KSTARS_EKOS_CAPTURE) << "\t with camera read mode: "
0268                                 << aSelectedReadMode.getCameraGainReadModeName()
0269                                 << "\tWith a read-noise table of : "
0270                                 << aSelectedReadMode.getCameraGainReadNoiseVector().size() << " values";
0271     */
0272     // qCInfo(KSTARS_EKOS_CAPTURE) << "Using a Noise Tolerance of: " << aNoiseTolerance;
0273 
0274     double cFactor = calculateCFactor(aNoiseTolerance);
0275     // qCInfo(KSTARS_EKOS_CAPTURE) << "Calculated CFactor is: " << cFactor;
0276 
0277     QVector<CalculatedGainSubExposureTime> aSubExposureTimeVector = calculateGainSubExposureVector(cFactor,
0278             lightPollutionForOpticFocalRatio);
0279 
0280     double exposureTimeMin = 99999999999.9;
0281     double exposureTimeMax = -1.0;
0282     // Iterate the times to capture and store the min and max exposure times.
0283     // (But the QCustomPlot can rescale, so this may be unnecessary
0284     for(QVector<OptimalExposure::CalculatedGainSubExposureTime>::iterator oe = aSubExposureTimeVector.begin();
0285             oe != aSubExposureTimeVector.end(); ++oe)
0286     {
0287         if(exposureTimeMin > oe->getSubExposureTime()) exposureTimeMin = oe->getSubExposureTime();
0288         if(exposureTimeMax < oe->getSubExposureTime()) exposureTimeMax = oe->getSubExposureTime();
0289     }
0290 
0291     CameraExposureEnvelope aCameraExposureEnvelope = CameraExposureEnvelope(
0292                 lightPollutionElectronBaseRate,
0293                 lightPollutionForOpticFocalRatio,
0294                 aSubExposureTimeVector,
0295                 exposureTimeMin,
0296                 exposureTimeMax);
0297 
0298     return(aCameraExposureEnvelope);
0299 }
0300 
0301 // OptimalExposure::OptimalExposureDetail OptimalSubExposureCalculator::calculateSubExposureDetail(int aSelectedGainValue)
0302 OptimalExposure::OptimalExposureDetail OptimalSubExposureCalculator::calculateSubExposureDetail()
0303 {
0304     /*
0305         This method calculates some details for a sub-exposure. It will interpolate between
0306         points in the camera read-noise table, to find a reasonable appoximation of the read-noise
0307         for a non-listed gain value.
0308     */
0309 
0310     // qCDebug(KSTARS_EKOS_CAPTURE) << "aSelectedGainValue: " << aSelectedGainValue;
0311 
0312     int aSelectedGain = getASelectedGain();
0313 
0314     // Look for a matching gain from the camera gain read-noise table, or identify a bracket for interpolation.
0315     // Interpolation may result in slight errors when the read-noise data is curve. (there's probably a better way to code this)
0316     double aReadNoise = 100.0;  // defaulted high just to make an error in the interpolation obvious
0317     bool matched = false;
0318     int lowerReadNoiseIndex = 0;
0319 
0320     OptimalExposure::CameraGainReadMode aSelectedReadMode =
0321         anImagingCameraData.getCameraGainReadModeVector()[aSelectedCameraReadMode];
0322 
0323     QVector<OptimalExposure::CameraGainReadNoise> aCameraGainReadNoiseVector =
0324         aSelectedReadMode.getCameraGainReadNoiseVector();
0325 
0326 
0327 
0328     for(int readNoiseIndex = 0; readNoiseIndex < aSelectedReadMode.getCameraGainReadNoiseVector().size(); readNoiseIndex++)
0329     {
0330         if(!matched)
0331         {
0332             CameraGainReadNoise aCameraGainReadNoise = aSelectedReadMode.getCameraGainReadNoiseVector()[readNoiseIndex];
0333             if(aCameraGainReadNoise.getGain() == aSelectedGain)
0334             {
0335                 matched = true;
0336                 // qCInfo(KSTARS_EKOS_CAPTURE) << " matched a camera gain ";
0337                 aReadNoise = aCameraGainReadNoise.getReadNoise();
0338                 break;
0339             }
0340             if(aCameraGainReadNoise.getGain() < aSelectedGain)
0341             {
0342                 lowerReadNoiseIndex = readNoiseIndex;
0343             }
0344         }
0345     }
0346 
0347     if(!matched)
0348     {
0349         int upperReadNoiseIndex = lowerReadNoiseIndex + 1;
0350         // qCInfo(KSTARS_EKOS_CAPTURE)
0351         //  << "Interpolating read noise gain bracket: " << lowerReadNoiseIndex
0352         //  << " and " << upperReadNoiseIndex;
0353 
0354         if (lowerReadNoiseIndex < aSelectedReadMode.getCameraGainReadNoiseVector().size() - 1)
0355         {
0356 
0357             CameraGainReadNoise aLowerIndexCameraReadNoise = aSelectedReadMode.getCameraGainReadNoiseVector()[lowerReadNoiseIndex];
0358             CameraGainReadNoise anUpperIndexCameraReadNoise = aSelectedReadMode.getCameraGainReadNoiseVector()[upperReadNoiseIndex];
0359 
0360             // interpolate a read-noise value
0361             double m = (anUpperIndexCameraReadNoise.getReadNoise() - aLowerIndexCameraReadNoise.getReadNoise()) /
0362                        (anUpperIndexCameraReadNoise.getGain() - aLowerIndexCameraReadNoise.getGain());
0363             aReadNoise = aLowerIndexCameraReadNoise.getReadNoise() + (m * (aSelectedGain - aLowerIndexCameraReadNoise.getGain()));
0364             // qCInfo(KSTARS_EKOS_CAPTURE)
0365             //        << "The camera read-noise for the selected gain value is interpolated to: " << aReadNoise;
0366         }
0367         else
0368         {
0369             // qCInfo(KSTARS_EKOS_CAPTURE) << " using max camera gain ";
0370             int maxIndex = aSelectedReadMode.getCameraGainReadNoiseVector().size() - 1;
0371             CameraGainReadNoise aMaxIndexCameraReadNoise = aSelectedReadMode.getCameraGainReadNoiseVector()[maxIndex];
0372             aReadNoise = aMaxIndexCameraReadNoise.getReadNoise();
0373         }
0374     }
0375     else
0376     {
0377         // qCInfo(KSTARS_EKOS_CAPTURE)
0378         // << "The camera read-noise for the selected gain value was matched: " << aReadNoise;
0379     }
0380 
0381     double anOptimalSubExposure = 0.0;
0382 
0383     double lightPollutionElectronBaseRate = OptimalSubExposureCalculator::calculateLightPollutionElectronBaseRate(aSkyQuality);
0384     double lightPollutionForOpticFocalRatio = calculateLightPolutionForOpticFocalRatio(lightPollutionElectronBaseRate,
0385             aFocalRatio, aFilterCompensation);
0386     double cFactor = calculateCFactor(aNoiseTolerance);
0387 
0388     switch(anImagingCameraData.getSensorType())
0389     {
0390         case SENSORTYPE_MONOCHROME:
0391             anOptimalSubExposure =  cFactor * pow(aReadNoise, 2) / lightPollutionForOpticFocalRatio;
0392             break;
0393 
0394         case SENSORTYPE_COLOR:
0395             anOptimalSubExposure = (double)3.0 * cFactor * pow(aReadNoise, 2) / lightPollutionForOpticFocalRatio;
0396             break;
0397     }
0398 
0399     // qCInfo(KSTARS_EKOS_CAPTURE) << "an Optimal Sub Exposure at gain: "
0400     //  << aSelectedGainValue << " is " << anOptimalSubExposure << " seconds";
0401 
0402     // Add the single exposure noise calcs to the response
0403     double anExposurePollutionElectrons = lightPollutionForOpticFocalRatio * anOptimalSubExposure;
0404     // qCInfo(KSTARS_EKOS_CAPTURE) << "Exposure Pollution Electrons: " << anExposurePollutionElectrons;
0405 
0406     double anExposureShotNoise = sqrt(lightPollutionForOpticFocalRatio * anOptimalSubExposure);
0407     // qCInfo(KSTARS_EKOS_CAPTURE) << "Exposure Shot Noise: " << anExposureShotNoise;
0408 
0409     double anExposureTotalNoise = sqrt( pow(aReadNoise, 2)
0410                                         + lightPollutionForOpticFocalRatio * anOptimalSubExposure);
0411     // qCInfo(KSTARS_EKOS_CAPTURE) << "Exposure Total Noise: " << anExposureTotalNoise;
0412 
0413     // Need to build and populate the stack estimations
0414     QVector<OptimalExposureStack> aStackSummary;
0415 
0416     // Loop through planned sessions for hours of stacking
0417     for(int sessionHours = 1; sessionHours < 41; sessionHours++)
0418     {
0419         // rounding up to ensure that exposure count is at least "sessionHours" of data.
0420         int anExposureCount = (int) ceil( (double)(sessionHours * 3600) /
0421                                           anOptimalSubExposure );
0422 
0423         int aStackTime = anExposureCount * anOptimalSubExposure;
0424         double aStackTotalNoise = sqrt( (double)anExposureCount * pow(aReadNoise, 2)
0425                                         + lightPollutionForOpticFocalRatio * (anExposureCount * anOptimalSubExposure));
0426 
0427         OptimalExposureStack *anOptimalExposureStack
0428             = new OptimalExposureStack(sessionHours, anExposureCount, aStackTime, aStackTotalNoise);
0429 
0430         /*
0431                 qCInfo(KSTARS_EKOS_CAPTURE) << sessionHours << "\t" << anExposureCount
0432                                             << "\t" << aStackTime << "\t" << aStackTotalNoise
0433                                             << "\t" << aStackTime / aStackTotalNoise;
0434 
0435                 //  << aSelectedGainValue << " is " << anOptimalSubExposure << " seconds";
0436         */
0437         aStackSummary.push_back(*anOptimalExposureStack);
0438 
0439 
0440     }
0441 
0442     OptimalExposureDetail anOptimalExposureDetail = OptimalExposureDetail(getASelectedGain(), anOptimalSubExposure,
0443             anExposurePollutionElectrons, anExposureShotNoise,  anExposureTotalNoise, aStackSummary);
0444 
0445     return(anOptimalExposureDetail);
0446 }
0447 
0448 
0449 }
0450