Warning, file /education/kstars/kstars/ekos/capture/exposurecalculator/optimalsubexposurecalculator.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
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