pyobs-core 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (459) hide show
  1. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/PKG-INFO +1 -1
  2. pyobs_core-2.2.0/pyobs/images/processors/calibration/_calibration_cache.py +60 -0
  3. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/calibration/_ccddata_calibrator.py +17 -4
  4. pyobs_core-2.2.0/pyobs/images/processors/calibration/calibration.py +513 -0
  5. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/camera/dummycamera.py +31 -8
  6. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/utils/__init__.py +11 -1
  7. pyobs_core-2.2.0/pyobs/modules/utils/dummystructuredconfig.py +137 -0
  8. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/object.py +1 -1
  9. pyobs_core-2.2.0/pyobs/robotic/scripts/calibration/darkbias.py +305 -0
  10. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/portal/observationarchive.py +9 -2
  11. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/portal/taskarchive.py +9 -2
  12. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/archive/archive.py +3 -0
  13. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/archive/local_archive.py +38 -2
  14. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/archive/pyobs_archive.py +42 -3
  15. pyobs_core-2.2.0/pyobs/robotic/utils/calibration.py +112 -0
  16. pyobs_core-2.2.0/pyobs/utils/exptime_grouping.py +80 -0
  17. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/fits.py +22 -0
  18. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/http.py +51 -2
  19. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/pipeline/pipeline.py +32 -2
  20. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/pipeline/progress.py +3 -0
  21. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/pipeline/reduction.py +143 -42
  22. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/pipeline/reduction_base.py +21 -6
  23. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyproject.toml +1 -1
  24. pyobs_core-2.1.0/pyobs/images/processors/calibration/_calibration_cache.py +0 -46
  25. pyobs_core-2.1.0/pyobs/images/processors/calibration/calibration.py +0 -302
  26. pyobs_core-2.1.0/pyobs/robotic/scripts/calibration/darkbias.py +0 -98
  27. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/.gitignore +0 -0
  28. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/LICENSE +0 -0
  29. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/__init__.py +0 -0
  30. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/application.py +0 -0
  31. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/background_task.py +0 -0
  32. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/cli/__init__.py +0 -0
  33. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/cli/_cli.py +0 -0
  34. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/cli/pyobs.py +0 -0
  35. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/cli/pyobsd.py +0 -0
  36. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/cli/pyobsw.py +0 -0
  37. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/__init__.py +0 -0
  38. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/comm.py +0 -0
  39. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/commlogging.py +0 -0
  40. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/dummy/__init__.py +0 -0
  41. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/dummy/dummycomm.py +0 -0
  42. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/local/__init__.py +0 -0
  43. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/local/localcomm.py +0 -0
  44. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/local/localnetwork.py +0 -0
  45. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/proxy.py +0 -0
  46. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/__init__.py +0 -0
  47. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/rpc.py +0 -0
  48. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/serializer.py +0 -0
  49. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/xep_0009/__init__.py +0 -0
  50. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/xep_0009/binding.py +0 -0
  51. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/xep_0009/rpc.py +0 -0
  52. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/xep_0009_timeout/__init__.py +0 -0
  53. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/xep_0009_timeout/rpc.py +0 -0
  54. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/xep_0009_timeout/stanza/RPC.py +0 -0
  55. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/xep_0009_timeout/stanza/__init__.py +0 -0
  56. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/xmppclient.py +0 -0
  57. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/comm/xmpp/xmppcomm.py +0 -0
  58. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/__init__.py +0 -0
  59. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/badweather.py +0 -0
  60. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/event.py +0 -0
  61. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/exposurestatuschanged.py +0 -0
  62. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/filterchanged.py +0 -0
  63. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/focusfound.py +0 -0
  64. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/goodweather.py +0 -0
  65. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/log.py +0 -0
  66. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/modechanged.py +0 -0
  67. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/moduleclosed.py +0 -0
  68. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/moduleopened.py +0 -0
  69. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/motionstatuschanged.py +0 -0
  70. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/move.py +0 -0
  71. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/newimage.py +0 -0
  72. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/newspectrum.py +0 -0
  73. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/offsets.py +0 -0
  74. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/roofclosing.py +0 -0
  75. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/roofopened.py +0 -0
  76. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/taskfailed.py +0 -0
  77. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/taskfinished.py +0 -0
  78. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/taskstarted.py +0 -0
  79. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/events/testevent.py +0 -0
  80. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/__init__.py +0 -0
  81. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/image.py +0 -0
  82. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/meta/__init__.py +0 -0
  83. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/meta/altazoffsets.py +0 -0
  84. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/meta/exptime.py +0 -0
  85. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/meta/genericoffset.py +0 -0
  86. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/meta/onskydistance.py +0 -0
  87. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/meta/pixeloffsets.py +0 -0
  88. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/meta/radecoffsets.py +0 -0
  89. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/meta/skyoffsets.py +0 -0
  90. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processor.py +0 -0
  91. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/__init__.py +0 -0
  92. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/_daobackgroundremover.py +0 -0
  93. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/annotation/__init__.py +0 -0
  94. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/annotation/_pillow.py +0 -0
  95. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/annotation/circle.py +0 -0
  96. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/annotation/crosshair.py +0 -0
  97. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/annotation/text.py +0 -0
  98. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/astrometry/__init__.py +0 -0
  99. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/astrometry/_dotnet_request.py +0 -0
  100. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/astrometry/_dotnet_request_builder.py +0 -0
  101. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/astrometry/_dotnet_request_logger.py +0 -0
  102. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/astrometry/_dotnet_response_saver.py +0 -0
  103. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/astrometry/astrometry.py +0 -0
  104. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/astrometry/dotnet.py +0 -0
  105. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/calibration/__init__.py +0 -0
  106. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/detection/__init__.py +0 -0
  107. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/detection/_pysep_stats_calculator.py +0 -0
  108. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/detection/_source_catalog.py +0 -0
  109. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/detection/daophot.py +0 -0
  110. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/detection/pysep.py +0 -0
  111. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/detection/simpledisk.py +0 -0
  112. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/detection/sourcedetection.py +0 -0
  113. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/exptime/__init__.py +0 -0
  114. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/exptime/exptime.py +0 -0
  115. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/exptime/star.py +0 -0
  116. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/image/__init__.py +0 -0
  117. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/image/addfitsheaders.py +0 -0
  118. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/image/download.py +0 -0
  119. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/image/flip.py +0 -0
  120. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/image/grayscale.py +0 -0
  121. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/image/httpserver.py +0 -0
  122. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/image/normalize.py +0 -0
  123. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/image/save.py +0 -0
  124. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/image/saveimage.py +0 -0
  125. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/image/smooth.py +0 -0
  126. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/image/softbin.py +0 -0
  127. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/misc/__init__.py +0 -0
  128. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/misc/addmask.py +0 -0
  129. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/misc/catalogcircularmask.py +0 -0
  130. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/misc/circularmask.py +0 -0
  131. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/misc/createfilename.py +0 -0
  132. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/misc/imagesourcefilter.py +0 -0
  133. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/misc/removebackground.py +0 -0
  134. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/modules/__init__.py +0 -0
  135. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/modules/getfitsheaders.py +0 -0
  136. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/offsets/__init__.py +0 -0
  137. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/offsets/astrometry.py +0 -0
  138. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/offsets/brighteststar.py +0 -0
  139. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/offsets/brighteststar_guiding.py +0 -0
  140. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/offsets/dummyoffsets.py +0 -0
  141. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/offsets/dummyskyoffsets.py +0 -0
  142. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/offsets/fitsheader.py +0 -0
  143. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/offsets/offsets.py +0 -0
  144. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/offsets/pixeloffsets.py +0 -0
  145. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/offsets/projected.py +0 -0
  146. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/offsets/spilled_light.py +0 -0
  147. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/photometry/__init__.py +0 -0
  148. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/photometry/_photometry_calculator.py +0 -0
  149. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/photometry/_photutil_aperture_photometry.py +0 -0
  150. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/photometry/_sep_aperture_photometry.py +0 -0
  151. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/photometry/aperture_photometry.py +0 -0
  152. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/photometry/photometry.py +0 -0
  153. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/photometry/photutil.py +0 -0
  154. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/photometry/pysep.py +0 -0
  155. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/wcs/__init__.py +0 -0
  156. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/images/processors/wcs/solarhelioprojective.py +0 -0
  157. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IAbortable.py +0 -0
  158. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IAcquisition.py +0 -0
  159. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IAutoFocus.py +0 -0
  160. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IAutoGuiding.py +0 -0
  161. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IAutonomous.py +0 -0
  162. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IBinning.py +0 -0
  163. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/ICalibrate.py +0 -0
  164. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/ICamera.py +0 -0
  165. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IConfig.py +0 -0
  166. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/ICooling.py +0 -0
  167. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IData.py +0 -0
  168. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IDataSequence.py +0 -0
  169. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IDome.py +0 -0
  170. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IExposure.py +0 -0
  171. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IExposureTime.py +0 -0
  172. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IFilters.py +0 -0
  173. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IFitsHeaderAfter.py +0 -0
  174. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IFitsHeaderBefore.py +0 -0
  175. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IFlatField.py +0 -0
  176. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IFocusModel.py +0 -0
  177. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IFocuser.py +0 -0
  178. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IGain.py +0 -0
  179. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IImageFormat.py +0 -0
  180. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IImageType.py +0 -0
  181. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IMode.py +0 -0
  182. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IModule.py +0 -0
  183. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IMotion.py +0 -0
  184. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IMultiFiber.py +0 -0
  185. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IOffsetsAltAz.py +0 -0
  186. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IOffsetsRaDec.py +0 -0
  187. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IPointingAltAz.py +0 -0
  188. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IPointingBody.py +0 -0
  189. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IPointingHeliocentricPolar.py +0 -0
  190. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IPointingHeliographicStonyhurst.py +0 -0
  191. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IPointingHelioprojective.py +0 -0
  192. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IPointingOrbitalElements.py +0 -0
  193. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IPointingRaDec.py +0 -0
  194. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IPointingSeries.py +0 -0
  195. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IReady.py +0 -0
  196. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IRobotic.py +0 -0
  197. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IRoboticScheduler.py +0 -0
  198. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IRoof.py +0 -0
  199. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IRotation.py +0 -0
  200. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IRunnable.py +0 -0
  201. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IRunning.py +0 -0
  202. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IScriptRunner.py +0 -0
  203. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/ISpectrograph.py +0 -0
  204. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IStartStop.py +0 -0
  205. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IStructuredConfig.py +0 -0
  206. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/ISyncTarget.py +0 -0
  207. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/ITelescope.py +0 -0
  208. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/ITemperatures.py +0 -0
  209. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/ITrackingMode.py +0 -0
  210. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/ITrackingRate.py +0 -0
  211. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IVideo.py +0 -0
  212. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IWeather.py +0 -0
  213. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/IWindow.py +0 -0
  214. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/__init__.py +0 -0
  215. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/interfaces/interface.py +0 -0
  216. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/mixins/__init__.py +0 -0
  217. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/mixins/camerasettings.py +0 -0
  218. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/mixins/fitsheader.py +0 -0
  219. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/mixins/fitsnamespace.py +0 -0
  220. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/mixins/follow.py +0 -0
  221. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/mixins/motionstatus.py +0 -0
  222. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/mixins/pipeline.py +0 -0
  223. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/mixins/waitformotion.py +0 -0
  224. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/mixins/weatheraware.py +0 -0
  225. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/__init__.py +0 -0
  226. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/camera/__init__.py +0 -0
  227. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/camera/adaptive.py +0 -0
  228. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/camera/basecamera.py +0 -0
  229. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/camera/basespectrograph.py +0 -0
  230. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/camera/basevideo.py +0 -0
  231. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/camera/dummyspectrograph.py +0 -0
  232. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/camera/dummyvideo.py +0 -0
  233. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/camera/pipelinecamera.py +0 -0
  234. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/flatfield/__init__.py +0 -0
  235. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/flatfield/flatfield.py +0 -0
  236. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/flatfield/pointing.py +0 -0
  237. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/flatfield/scheduler.py +0 -0
  238. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/focus/__init__.py +0 -0
  239. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/focus/dummyautofocus.py +0 -0
  240. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/focus/focusmodel.py +0 -0
  241. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/focus/focusseries.py +0 -0
  242. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/image/__init__.py +0 -0
  243. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/image/imagewatcher.py +0 -0
  244. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/image/imagewriter.py +0 -0
  245. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/image/pipeline.py +0 -0
  246. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/image/seeing.py +0 -0
  247. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/module.py +0 -0
  248. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/__init__.py +0 -0
  249. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/_base.py +0 -0
  250. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/_baseguiding.py +0 -0
  251. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/acquisition.py +0 -0
  252. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/autoguiding.py +0 -0
  253. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/dummyacquisition.py +0 -0
  254. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/dummyguiding.py +0 -0
  255. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/guidingstatistics/__init__.py +0 -0
  256. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/guidingstatistics/guidingstatistics.py +0 -0
  257. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/guidingstatistics/pixeloffset.py +0 -0
  258. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/guidingstatistics/skyoffsets.py +0 -0
  259. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/guidingstatistics/uptime.py +0 -0
  260. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/pointing/scienceframeguiding.py +0 -0
  261. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/robotic/__init__.py +0 -0
  262. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/robotic/dummymastermind.py +0 -0
  263. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/robotic/dummyscheduler.py +0 -0
  264. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/robotic/mastermind.py +0 -0
  265. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/robotic/pointing.py +0 -0
  266. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/robotic/scheduler.py +0 -0
  267. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/robotic/scriptrunner.py +0 -0
  268. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/roof/__init__.py +0 -0
  269. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/roof/basedome.py +0 -0
  270. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/roof/baseroof.py +0 -0
  271. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/roof/dummyroof.py +0 -0
  272. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/telescope/__init__.py +0 -0
  273. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/telescope/_dummytelescopebase.py +0 -0
  274. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/telescope/basetelescope.py +0 -0
  275. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/telescope/dummyaltaztelescope.py +0 -0
  276. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/telescope/dummyradectelescope.py +0 -0
  277. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/telescope/dummysolartelescope.py +0 -0
  278. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/test/__init__.py +0 -0
  279. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/test/standalone.py +0 -0
  280. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/utils/dummymode.py +0 -0
  281. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/utils/fluentlogger.py +0 -0
  282. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/utils/httpfilecache.py +0 -0
  283. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/utils/kiosk.py +0 -0
  284. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/utils/matrix.py +0 -0
  285. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/utils/stellarium.py +0 -0
  286. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/utils/telegram.py +0 -0
  287. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/utils/trigger.py +0 -0
  288. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/weather/__init__.py +0 -0
  289. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/weather/mockweather.py +0 -0
  290. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/weather/weather.py +0 -0
  291. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/weather/weather_api.py +0 -0
  292. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/modules/weather/weather_state.py +0 -0
  293. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/py.typed +0 -0
  294. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/__init__.py +0 -0
  295. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/observation.py +0 -0
  296. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/__init__.py +0 -0
  297. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/_executor.py +0 -0
  298. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/astroplanscheduler.py +0 -0
  299. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/constraints/__init__.py +0 -0
  300. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/constraints/airmassconstraint.py +0 -0
  301. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/constraints/constraint.py +0 -0
  302. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/constraints/moonilluminationconstraint.py +0 -0
  303. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/constraints/moonseparationconstraint.py +0 -0
  304. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/constraints/solarelevationconstraint.py +0 -0
  305. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/constraints/timeconstraint.py +0 -0
  306. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/dataprovider.py +0 -0
  307. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/merits/__init__.py +0 -0
  308. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/merits/aftertime.py +0 -0
  309. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/merits/beforetime.py +0 -0
  310. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/merits/constant.py +0 -0
  311. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/merits/follow.py +0 -0
  312. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/merits/interval.py +0 -0
  313. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/merits/merit.py +0 -0
  314. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/merits/pernight.py +0 -0
  315. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/merits/random.py +0 -0
  316. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/merits/timewindow.py +0 -0
  317. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/merits/transit.py +0 -0
  318. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/observationarchiveevolution.py +0 -0
  319. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/ondemandscheduler.py +0 -0
  320. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/targets/__init__.py +0 -0
  321. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/targets/dynamictarget.py +0 -0
  322. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/targets/heliocentricpolartarget.py +0 -0
  323. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/targets/helioprojectivetarget.py +0 -0
  324. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/targets/picker/__init__.py +0 -0
  325. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/targets/picker/csvpicker.py +0 -0
  326. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/targets/picker/picker.py +0 -0
  327. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/targets/siderealtarget.py +0 -0
  328. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/targets/target.py +0 -0
  329. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scheduler/taskscheduler.py +0 -0
  330. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/__init__.py +0 -0
  331. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/calibration/__init__.py +0 -0
  332. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/calibration/pointing.py +0 -0
  333. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/calibration/skyflats.py +0 -0
  334. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/control/__init__.py +0 -0
  335. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/control/cases.py +0 -0
  336. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/control/conditional.py +0 -0
  337. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/control/parallel.py +0 -0
  338. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/control/selector.py +0 -0
  339. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/control/sequential.py +0 -0
  340. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/exceptions.py +0 -0
  341. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/imaging/__init__.py +0 -0
  342. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/imaging/autofocus.py +0 -0
  343. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/imaging/imaging.py +0 -0
  344. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/imaging/transitimaging.py +0 -0
  345. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/script.py +0 -0
  346. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/utils/__init__.py +0 -0
  347. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/utils/callmodule.py +0 -0
  348. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/utils/debugtrigger.py +0 -0
  349. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/scripts/utils/log.py +0 -0
  350. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/__init__.py +0 -0
  351. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/filesystem/__init__.py +0 -0
  352. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/filesystem/observationarchive.py +0 -0
  353. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/filesystem/taskarchive.py +0 -0
  354. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/__init__.py +0 -0
  355. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/_portal.py +0 -0
  356. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/_schedulereader.py +0 -0
  357. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/_schedulewriter.py +0 -0
  358. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/configdb.py +0 -0
  359. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/mockobservationarchive.py +0 -0
  360. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/observationarchive.py +0 -0
  361. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/scripts/__init__.py +0 -0
  362. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/scripts/autofocus.py +0 -0
  363. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/scripts/default.py +0 -0
  364. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/scripts/script.py +0 -0
  365. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/task.py +0 -0
  366. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/taskarchive.py +0 -0
  367. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/lco/taskrunner.py +0 -0
  368. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/memory/__init__.py +0 -0
  369. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/memory/observationarchive.py +0 -0
  370. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/memory/taskarchive.py +0 -0
  371. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/observationarchive.py +0 -0
  372. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/portal/__init__.py +0 -0
  373. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/storage/taskarchive.py +0 -0
  374. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/task.py +0 -0
  375. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/taskrunner.py +0 -0
  376. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/__init__.py +0 -0
  377. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/archive/__init__.py +0 -0
  378. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/exptime/__init__.py +0 -0
  379. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/exptime/exptime.py +0 -0
  380. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/exptime/stellarexptime.py +0 -0
  381. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/skyflats/__init__.py +0 -0
  382. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/skyflats/exptimeeval.py +0 -0
  383. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/skyflats/flatfielder.py +0 -0
  384. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/skyflats/pointing/__init__.py +0 -0
  385. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/skyflats/pointing/base.py +0 -0
  386. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/skyflats/pointing/static.py +0 -0
  387. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/skyflats/priorities/__init__.py +0 -0
  388. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/skyflats/priorities/archive.py +0 -0
  389. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/skyflats/priorities/base.py +0 -0
  390. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/skyflats/priorities/const.py +0 -0
  391. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/robotic/utils/skyflats/scheduler.py +0 -0
  392. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/__init__.py +0 -0
  393. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/average.py +0 -0
  394. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/cache.py +0 -0
  395. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/config.py +0 -0
  396. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/config_schema.py +0 -0
  397. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/coordinates.py +0 -0
  398. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/curvefit.py +0 -0
  399. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/enums.py +0 -0
  400. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/exceptions.py +0 -0
  401. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/focusseries/__init__.py +0 -0
  402. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/focusseries/base.py +0 -0
  403. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/focusseries/photometry.py +0 -0
  404. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/focusseries/projection.py +0 -0
  405. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/grids/__init__.py +0 -0
  406. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/grids/filters.py +0 -0
  407. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/grids/grid.py +0 -0
  408. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/grids/gridnode.py +0 -0
  409. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/grids/pipeline.py +0 -0
  410. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/gui/__init__.py +0 -0
  411. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/gui/camera/__init__.py +0 -0
  412. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/gui/camera/binningwidget.py +0 -0
  413. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/gui/camera/datadisplaywidget.py +0 -0
  414. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/gui/camera/exposewidget.py +0 -0
  415. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/gui/camera/exposuretimewidget.py +0 -0
  416. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/gui/camera/imageformatwidget.py +0 -0
  417. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/gui/camera/listpickerdialog.py +0 -0
  418. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/gui/camera/windowingwidget.py +0 -0
  419. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/influxdb.py +0 -0
  420. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/logger.py +0 -0
  421. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/logging/__init__.py +0 -0
  422. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/logging/context.py +0 -0
  423. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/logging/resolvableerror.py +0 -0
  424. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/modulegui.py +0 -0
  425. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/offsets/__init__.py +0 -0
  426. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/offsets/applyaltazoffsets.py +0 -0
  427. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/offsets/applyoffsets.py +0 -0
  428. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/offsets/applyradecoffsets.py +0 -0
  429. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/parallel.py +0 -0
  430. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/pipeline/__init__.py +0 -0
  431. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/publisher/__init__.py +0 -0
  432. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/publisher/csv.py +0 -0
  433. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/publisher/http.py +0 -0
  434. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/publisher/log.py +0 -0
  435. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/publisher/multi.py +0 -0
  436. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/publisher/publisher.py +0 -0
  437. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/serialization.py +0 -0
  438. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/shellcommand.py +0 -0
  439. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/threads/__init__.py +0 -0
  440. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/threads/lockwithabort.py +0 -0
  441. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/time.py +0 -0
  442. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/units.py +0 -0
  443. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/utils/versions.py +0 -0
  444. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/version.py +0 -0
  445. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/__init__.py +0 -0
  446. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/archivefile.py +0 -0
  447. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/bufferedfile.py +0 -0
  448. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/file.py +0 -0
  449. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/filelists/__init__.py +0 -0
  450. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/filelists/filelist.py +0 -0
  451. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/filelists/testing.py +0 -0
  452. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/httpfile.py +0 -0
  453. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/localfile.py +0 -0
  454. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/memfile.py +0 -0
  455. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/sftpfile.py +0 -0
  456. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/smbfile.py +0 -0
  457. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/sshfile.py +0 -0
  458. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/tempfile.py +0 -0
  459. {pyobs_core-2.1.0 → pyobs_core-2.2.0}/pyobs/vfs/vfs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyobs-core
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: robotic telescope software
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from typing import cast
5
+
6
+ from pyobs.images import Image
7
+ from pyobs.utils.enums import ImageType
8
+
9
+
10
+ class _CalibrationCache:
11
+ BINNING_FORMAT = "{0}x{0}"
12
+
13
+ def __init__(self, max_size: int):
14
+ self._cache: deque[tuple[tuple[ImageType, str, str, str | None, float | None], Image]] = deque([], max_size)
15
+
16
+ def add_to_cache(self, image: Image, image_type: ImageType, exptime: float | None = None) -> None:
17
+ cache_keys = self._get_cache_keys(image, image_type, exptime)
18
+ cache_entry = (cache_keys, image)
19
+ self._cache.append(cache_entry)
20
+
21
+ def get_from_cache(self, image: Image, image_type: ImageType, exptime: float | None = None) -> Image:
22
+ cache_keys = self._get_cache_keys(image, image_type, exptime)
23
+ return self._find_cache_entry(cache_keys)
24
+
25
+ def _find_cache_entry(self, keys: tuple[ImageType, str, str, str | None, float | None]) -> Image:
26
+ for m, item in self._cache:
27
+ if m == keys:
28
+ return item
29
+
30
+ raise ValueError("Calibration not found in cache.")
31
+
32
+ def _get_cache_keys(
33
+ self, image: Image, image_type: ImageType, exptime: float | None = None
34
+ ) -> tuple[ImageType, str, str, str | None, float | None]:
35
+ instrument, binning, filter_name = self._get_image_cache_keys(image)
36
+ if image_type in (ImageType.BIAS, ImageType.DARK):
37
+ # BIAS/DARK lookups ignore filter (matching Calibration._find_master_in_archive,
38
+ # which never passes a science image's FILTER to Pipeline.find_master for these two
39
+ # types). Deriving filter_name from whichever image was passed -- the science image
40
+ # on a get, the master on an add -- would otherwise near-guarantee a cache miss:
41
+ # a master dark/bias frequently has no FILTER header at all (or a stale one from
42
+ # whatever was mounted at the time) while the science image usually does.
43
+ filter_name = None
44
+ # exptime is not derived from `image`'s own header: for DARK, callers pass the exptime
45
+ # they searched for (the science exptime for an exact match, or the configured
46
+ # reference exptime for a scale-down lookup), which generally differs from either the
47
+ # science image's or the master's own EXPTIME. None for BIAS/SKYFLAT, unchanged.
48
+ cache_keys = (image_type, instrument, binning, filter_name, exptime)
49
+
50
+ return cache_keys
51
+
52
+ def _get_image_cache_keys(self, image: Image) -> tuple[str, str, str | None]:
53
+ instrument = image.header["INSTRUME"]
54
+ binning = self.BINNING_FORMAT.format(image.header["XBINNING"]) # noqa: UP031
55
+
56
+ filter_name = None
57
+ if "FILTER" in image.header:
58
+ filter_name = cast(str, image.header["FILTER"])
59
+
60
+ return instrument, binning, filter_name
@@ -7,7 +7,14 @@ from pyobs.images import Image
7
7
 
8
8
 
9
9
  class _CCDDataCalibrator:
10
- def __init__(self, image: Image, bias: Image | None = None, dark: Image | None = None, flat: Image | None = None):
10
+ def __init__(
11
+ self,
12
+ image: Image,
13
+ bias: Image | None = None,
14
+ dark: Image | None = None,
15
+ flat: Image | None = None,
16
+ dark_scale: bool = True,
17
+ ):
11
18
  # to_ccddata() below never carries a catalog through anyway, so drop it before trim() to
12
19
  # avoid its stale-catalog guard rejecting an image whose catalog wouldn't survive regardless.
13
20
  if image.safe_catalog is not None:
@@ -17,10 +24,16 @@ class _CCDDataCalibrator:
17
24
  self._bias = self._optional_to_ccddata(bias)
18
25
  self._dark = self._optional_to_ccddata(dark)
19
26
  self._flat = self._optional_to_ccddata(flat)
27
+ # whether ccd_process() should rescale the dark to the science exptime -- False for an
28
+ # exact exptime-match master, which is used as-is (see Calibration._find_dark_master).
29
+ self._dark_scale = dark_scale
20
30
 
21
31
  self._ccd_data = self._image.to_ccddata()
22
32
 
23
- if dark is not None:
33
+ # only read EXPTIME when it's actually needed -- an unscaled exact-match dark
34
+ # (dark_scale=False) doesn't require it, and a legacy master combined from raw frames
35
+ # with no EXPTIME at all (see Reduction._create_master_darks' fallback) doesn't have it
36
+ if dark is not None and dark_scale:
24
37
  self._dark_exp_time = dark.header["EXPTIME"]
25
38
 
26
39
  @staticmethod
@@ -47,8 +60,8 @@ class _CCDDataCalibrator:
47
60
  bad_pixel_mask=None,
48
61
  gain=self._image.header["DET-GAIN"] * u.electron / u.adu,
49
62
  readnoise=self._image.header["DET-RON"] * u.electron,
50
- dark_exposure=self._dark_exp_time * u.second if self._dark is not None else None,
63
+ dark_exposure=self._dark_exp_time * u.second if (self._dark is not None and self._dark_scale) else None,
51
64
  data_exposure=self._image.header["EXPTIME"] * u.second,
52
- dark_scale=True,
65
+ dark_scale=self._dark_scale,
53
66
  gain_corrected=False,
54
67
  )
@@ -0,0 +1,513 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any, cast
5
+
6
+ import astropy.units as u
7
+
8
+ from pyobs.images import Image
9
+ from pyobs.images.processor import ImageProcessor
10
+ from pyobs.images.processors.calibration._calibration_cache import _CalibrationCache
11
+ from pyobs.images.processors.calibration._ccddata_calibrator import _CCDDataCalibrator
12
+ from pyobs.robotic.utils.archive import Archive
13
+ from pyobs.utils.enums import ImageType
14
+ from pyobs.utils.exptime_grouping import exptimes_close
15
+ from pyobs.utils.pipeline import Pipeline
16
+ from pyobs.utils.time import Time
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ class Calibration(ImageProcessor):
22
+ """
23
+ Calibrate an image using master bias, dark, and flat frames fetched from an archive.
24
+
25
+ This processor locates appropriate master calibration frames
26
+ (bias, dark, flat) based on the input image metadata, applies calibration to the
27
+ image, and annotates the FITS header with provenance and reduction information.
28
+ Calibration frames are looked up via an :class:`~pyobs.robotic.utils.archive.Archive` and are
29
+ cached in a class-wide cache to reduce repeated lookups. If required calibration
30
+ frames cannot be found, the original image is returned unchanged.
31
+
32
+ :param dict | Archive archive: Archive configuration or an already constructed
33
+ :class:`~pyobs.robotic.utils.archive.Archive` instance used to
34
+ locate master calibration frames. If a dictionary
35
+ is provided, it is instantiated via
36
+ :func:`~pyobs.object.get_object`.
37
+ :param int max_cache_size: Maximum number of master frames kept in the shared
38
+ calibration cache. Default: ``20``.
39
+ :param bool require_bias: If ``True``, a master bias must be found; otherwise
40
+ calibration is aborted and the image is returned
41
+ unchanged. If ``False``, bias subtraction is skipped.
42
+ Default: ``True``.
43
+ :param bool require_dark: If ``True``, a master dark must be found; otherwise
44
+ calibration is aborted. If ``False``, dark subtraction
45
+ is skipped. Default: ``True``.
46
+ :param bool require_flat: If ``True``, a master flat must be found; otherwise
47
+ calibration is aborted. If ``False``, flat-fielding is
48
+ skipped. Default: ``True``.
49
+ :param float | None max_days_bias: Maximum age difference in days allowed when
50
+ selecting the master bias relative to the
51
+ science image ``DATE-OBS``. If ``None``, no
52
+ explicit age limit is applied. Default: ``None``.
53
+ :param float | None max_days_dark: Same as ``max_days_bias`` for dark frames.
54
+ Default: ``None``.
55
+ :param float | None max_days_flat: Same as ``max_days_bias`` for flat frames.
56
+ Default: ``None``.
57
+ :param float dark_exptime_tolerance: Relative tolerance for matching a science frame's
58
+ ``EXPTIME`` against a dark master's, and for the
59
+ scale-down-only ceiling around ``dark_scale_exptime``.
60
+ Default: ``0.01`` (1%).
61
+ :param float | None dark_scale_exptime: Reference dark exposure time (seconds) that may be
62
+ scaled down to a shorter science exptime when no
63
+ exact-exptime master exists. ``None`` disables
64
+ scale-down entirely (branch 3 below never fires).
65
+ Default: ``600.0``.
66
+ :param bool allow_unmatched_dark_scale: If ``True``, a science exptime with neither an
67
+ exact-match master nor a usable reference master
68
+ falls back to today's always-scale-whatever-is-
69
+ nearest behavior instead of raising. For sites not
70
+ yet taking per-exptime darks. Default: ``False``.
71
+ :param float | None dark_min_exptime: Science exptimes below this (and with no exact-match
72
+ master) skip dark subtraction entirely and are
73
+ calibrated with bias only -- dark current is assumed
74
+ negligible there. ``None`` or ``0`` disables this
75
+ branch. Default: ``5.0``.
76
+ :param kwargs: Additional keyword arguments forwarded to
77
+ :class:`pyobs.images.processor.ImageProcessor`.
78
+
79
+ Behavior
80
+ --------
81
+ - Verifies required image header keys before searching: ``INSTRUME``, ``XBINNING``,
82
+ and ``DATE-OBS`` must be present.
83
+ - Attempts to retrieve required masters (bias, dark, flat) from a class-wide cache.
84
+ On cache miss, queries the configured archive via
85
+ :meth:`~pyobs.utils.pipeline.Pipeline.find_master`, matching:
86
+
87
+ - Instrument: exact value of ``INSTRUME``.
88
+ - Binning: string formatted as ``"{XBINNING}x{XBINNING}"`` (square binning assumed).
89
+ - Filter: ``FILTER`` only for flats; biases and darks ignore filter.
90
+ - Time constraint: centered on the image ``DATE-OBS`` with optional ``max_days_*``.
91
+
92
+ - Dark matching additionally follows ADR
93
+ ``0015-dark-master-strict-exptime-matching-reference-scale-down-only.md``, checked in
94
+ order against the science frame's own ``EXPTIME``:
95
+
96
+ 1. An exact match (within ``dark_exptime_tolerance``) is used unscaled.
97
+ 2. No exact match and ``EXPTIME < dark_min_exptime``: bias-only, no dark subtraction,
98
+ not an error.
99
+ 3. No exact match, ``EXPTIME >= dark_min_exptime``, and a reference master with
100
+ ``EXPTIME <= dark_scale_exptime`` exists: used scaled down to the science exptime.
101
+ 4. ``allow_unmatched_dark_scale=True``: fall back to whatever ``find_master`` returns,
102
+ scaled, regardless of direction (today's behavior).
103
+ 5. Otherwise: a ``ValueError`` naming the requested exptime, caught the same way as any
104
+ other missing-master case (see below).
105
+
106
+ - If any required master is missing, logs a warning and returns the original image
107
+ unchanged.
108
+ - Applies calibration using the internal ``_CCDDataCalibrator`` helper
109
+ with the found master frames (``None`` for any non-required step to be skipped).
110
+ - Copies provenance into the output FITS header:
111
+
112
+ - ``L1RAW`` set from the original ``ORIGNAME`` (file stem without ``.fits``).
113
+ - ``L1BIAS``, ``L1DARK``, ``L1FLAT`` set from the respective master frame
114
+ ``FNAME`` values (with common FITS extensions removed) and descriptive comments.
115
+
116
+ - Sets calibration metadata in the output header:
117
+
118
+ - ``BUNIT = "electron"`` to indicate calibrated pixel units.
119
+ - ``RLEVEL = 1`` to indicate reduction level.
120
+
121
+ Input/Output
122
+ ------------
123
+ - Input: :class:`pyobs.images.Image` with FITS header containing at least
124
+ ``INSTRUME``, ``XBINNING``, and ``DATE-OBS``; optional ``FILTER`` improves flat matching.
125
+ - Output: :class:`pyobs.images.Image` calibrated by subtracting bias, subtracting
126
+ dark, and dividing by flat as available. Pixel data are modified; header is
127
+ augmented with provenance and reduction keys.
128
+
129
+ Configuration (YAML)
130
+ --------------------
131
+ Use an archive object and allow calibration frames up to 7 days old:
132
+
133
+ .. code-block:: yaml
134
+
135
+ class: pyobs.images.processors.calibration.Calibration
136
+ archive:
137
+ class: your.archive.Class # replace with your Archive implementation
138
+ # ... archive-specific configuration ...
139
+ max_cache_size: 50
140
+ max_days_bias: 7
141
+ max_days_dark: 7
142
+ max_days_flat: 7
143
+ require_bias: true
144
+ require_dark: true
145
+ require_flat: true
146
+
147
+ Skip flats but require bias and dark:
148
+
149
+ .. code-block:: yaml
150
+
151
+ class: pyobs.images.processors.calibration.Calibration
152
+ archive: { class: your.archive.Class, ... }
153
+ require_flat: false
154
+
155
+ Notes
156
+ -----
157
+ - The calibration cache is shared across all instances of this class within the
158
+ process and is bounded by ``max_cache_size``.
159
+ - Only ``XBINNING`` is considered; this implementation assumes square binning.
160
+ If your data use asymmetric binning, adjust the matching logic.
161
+ - Flats are matched by filter when available; biases and darks ignore filter.
162
+ - If ``ORIGNAME`` is present in the input header, it is copied to ``L1RAW`` as a
163
+ stem without the ``.fits`` suffix. Master frame names are copied from ``FNAME``.
164
+ - Setting ``BUNIT = "electron"`` assumes calibrated units are electrons; ensure
165
+ your calibration products and gains are consistent with this convention.
166
+ """
167
+
168
+ __module__ = "pyobs.images.processors.calibration"
169
+
170
+ """Cache for calibration frames."""
171
+ _calib_cache: _CalibrationCache | None = None
172
+
173
+ def __init__(
174
+ self,
175
+ archive: dict[str, Any] | Archive,
176
+ max_cache_size: int = 20,
177
+ require_bias: bool = True,
178
+ require_dark: bool = True,
179
+ require_flat: bool = True,
180
+ max_days_bias: float | None = None,
181
+ max_days_dark: float | None = None,
182
+ max_days_flat: float | None = None,
183
+ dark_exptime_tolerance: float = 0.01,
184
+ dark_scale_exptime: float | None = 600.0,
185
+ allow_unmatched_dark_scale: bool = False,
186
+ dark_min_exptime: float | None = 5.0,
187
+ **kwargs: Any,
188
+ ):
189
+ """Init a new image calibration pipeline step.
190
+
191
+ Args:
192
+ archive: Archive to fetch calibration frames from.
193
+ """
194
+ ImageProcessor.__init__(self, **kwargs)
195
+
196
+ self._max_cache_size = max_cache_size
197
+ self._max_days_bias = max_days_bias
198
+ self._max_days_dark = max_days_dark
199
+ self._max_days_flat = max_days_flat
200
+ self._require_bias = require_bias
201
+ self._require_dark = require_dark
202
+ self._require_flat = require_flat
203
+ self._dark_exptime_tolerance = dark_exptime_tolerance
204
+ self._dark_scale_exptime = dark_scale_exptime
205
+ self._allow_unmatched_dark_scale = allow_unmatched_dark_scale
206
+ self._dark_min_exptime = dark_min_exptime
207
+
208
+ self._archive = self.pyobs_model_validate(Archive, archive)
209
+
210
+ if self._calib_cache is None:
211
+ self._calib_cache = _CalibrationCache(self._max_cache_size)
212
+
213
+ async def __call__(self, image: Image) -> Image:
214
+ """Calibrate an image.
215
+
216
+ Args:
217
+ image: Image to calibrate.
218
+
219
+ Returns:
220
+ Calibrated image.
221
+ """
222
+
223
+ try:
224
+ bias, dark, flat, dark_scale = await self._get_calibrations_masters(image)
225
+ except ValueError as e:
226
+ log.warning("Could not find calibration frames: %s", e)
227
+ return image
228
+
229
+ calibrator = _CCDDataCalibrator(image, bias, dark, flat, dark_scale=dark_scale)
230
+ calibrated = calibrator()
231
+
232
+ self._copy_original_filename(calibrated, image)
233
+ self._copy_calibration_filename(calibrated, bias, dark, flat)
234
+
235
+ self._set_calibration_headers(calibrated)
236
+
237
+ return calibrated
238
+
239
+ async def _get_calibrations_masters(self, image: Image) -> tuple[Image | None, Image | None, Image | None, bool]:
240
+ bias = (
241
+ None
242
+ if not self._require_bias
243
+ else await self._find_master(image, ImageType.BIAS, max_days=self._max_days_bias)
244
+ )
245
+ dark, dark_scale = await self._find_dark_master(image)
246
+ flat = (
247
+ None
248
+ if not self._require_flat
249
+ else await self._find_master(image, ImageType.SKYFLAT, max_days=self._max_days_flat)
250
+ )
251
+
252
+ return bias, dark, flat, dark_scale
253
+
254
+ async def _find_dark_master(self, image: Image) -> tuple[Image | None, bool]:
255
+ """Implements ADR 0015's dark-master matching/scaling policy, checked in order against
256
+ the science image's own EXPTIME:
257
+
258
+ 1. An exact match (within dark_exptime_tolerance) -- used unscaled.
259
+ 2. No exact match and EXPTIME < dark_min_exptime -- bias-only, not an error.
260
+ 3. No exact match, EXPTIME >= dark_min_exptime, and a reference master with
261
+ EXPTIME <= dark_scale_exptime *and* EXPTIME >= science_exptime exists -- used
262
+ scaled down to the science exptime. The second condition is what actually
263
+ enforces "scaled down only": without it, a site whose longest master happens to be
264
+ shorter than the science exptime would otherwise get that master scaled *up*.
265
+ 4. allow_unmatched_dark_scale=True -- fall back to today's always-scale-whatever-is-
266
+ nearest behavior. A candidate with no EXPTIME at all (a legacy master) can't be
267
+ scaled either way, so it's used unscaled (dark_scale=False) rather than skipped.
268
+ 5. Otherwise -- ValueError, caught by __call__ like any other missing-master case.
269
+
270
+ Returns:
271
+ (master, scale). master is None only for the legitimate bias-only branch (2), or
272
+ when require_dark is False. scale tells the caller whether _CCDDataCalibrator
273
+ should rescale the returned master to the science exptime.
274
+
275
+ Raises:
276
+ ValueError: no usable dark master under the configured policy (branch 5).
277
+ """
278
+ if not self._require_dark:
279
+ return None, False
280
+
281
+ self._verify_image_header(image)
282
+ science_exptime = float(image.header["EXPTIME"])
283
+
284
+ # (1) exact match, used unscaled
285
+ exact = await self._find_dark_at(image, science_exptime, self._max_days_dark)
286
+ if exact is not None:
287
+ return exact, False
288
+
289
+ # (2) below the minimum, no exact match -- bias-only, not an error
290
+ if self._dark_min_exptime is not None and science_exptime < self._dark_min_exptime:
291
+ return None, False
292
+
293
+ # (3) reference master, scaled down only (never up). exptime_max=dark_scale_exptime only
294
+ # bounds the reference search from above; a site with no master anywhere near
295
+ # dark_scale_exptime (e.g. only a 45s one) could otherwise get that shorter master back
296
+ # as the "reference" and have it scaled UP to a longer science exptime -- exactly the
297
+ # direction ADR 0015 forbids. Guard against that explicitly: reject a candidate whose
298
+ # own exptime doesn't cover science_exptime (within tolerance), falling through instead.
299
+ if self._dark_scale_exptime is not None and science_exptime <= self._dark_scale_exptime * (
300
+ 1 + self._dark_exptime_tolerance
301
+ ):
302
+ reference = await self._find_dark_at(
303
+ image, self._dark_scale_exptime, self._max_days_dark, exptime_max=self._dark_scale_exptime
304
+ )
305
+ if reference is not None and self._covers_scale_down(reference, science_exptime):
306
+ return reference, True
307
+
308
+ # (4) opt back into today's always-scale-whatever-is-nearest behavior
309
+ if self._allow_unmatched_dark_scale:
310
+ fallback = await self._find_master(image, ImageType.DARK, max_days=self._max_days_dark)
311
+ # a legacy master with no EXPTIME at all can't be scaled -- ccd_process needs the
312
+ # dark's own exposure time to compute the scale factor (see _CCDDataCalibrator)
313
+ return fallback, "EXPTIME" in fallback.header
314
+
315
+ # (5) strict -- no usable dark master under the configured policy
316
+ ceiling = (
317
+ None if self._dark_scale_exptime is None else self._dark_scale_exptime * (1 + self._dark_exptime_tolerance)
318
+ )
319
+ available = await self._available_dark_exptimes(image)
320
+ raise ValueError(
321
+ f"No usable dark master for EXPTIME={science_exptime}s (no exact match within "
322
+ f"{self._dark_exptime_tolerance:.0%}"
323
+ + (
324
+ f", no reference master <= {ceiling:.1f}s to scale down"
325
+ if ceiling is not None
326
+ else ", no reference exptime configured"
327
+ )
328
+ + f"); available master exptimes: {available if available else 'none'}."
329
+ )
330
+
331
+ def _covers_scale_down(self, reference: Image, science_exptime: float) -> bool:
332
+ """Whether `reference`'s own exptime is long enough to be scaled *down* to
333
+ science_exptime -- ADR 0015 forbids the reverse. False (not usable as a reference) for
334
+ a master shorter than science_exptime, or one with no EXPTIME to check at all (a legacy
335
+ master -- direction can't be verified, so it's rejected rather than assumed safe)."""
336
+ if "EXPTIME" not in reference.header:
337
+ return False
338
+ return float(reference.header["EXPTIME"]) >= science_exptime * (1 - self._dark_exptime_tolerance)
339
+
340
+ async def _available_dark_exptimes(self, image: Image) -> list[float]:
341
+ """Best-effort list of exptimes among this instrument/binning's archived DARK masters
342
+ near the science image's DATE-OBS, for the strict-policy error message only (ADR 0015
343
+ asks that it name what's available). Bounded to max_days_dark (falling back to a wide
344
+ 30-day window if unset) so this doesn't scan an archive's entire DARK history on every
345
+ strict-failure frame. Never raises -- an archive issue here shouldn't mask the real
346
+ ValueError being raised."""
347
+ try:
348
+ instrument = image.header["INSTRUME"]
349
+ binning = "{0}x{0}".format(image.header["XBINNING"]) # noqa: UP031
350
+ time = Time(image.header["DATE-OBS"])
351
+ max_days = self._max_days_dark if self._max_days_dark is not None else 30.0
352
+ infos = await self._archive.list_frames(
353
+ start=time - max_days * u.day,
354
+ end=time + max_days * u.day,
355
+ instrument=instrument,
356
+ image_type=ImageType.DARK,
357
+ binning=binning,
358
+ rlevel=1,
359
+ )
360
+ except Exception:
361
+ return []
362
+ return sorted({i.exptime for i in infos if i.exptime is not None})
363
+
364
+ async def _find_dark_at(
365
+ self, image: Image, target_exptime: float, max_days: float | None, exptime_max: float | None = None
366
+ ) -> Image | None:
367
+ """Looks up a DARK master near target_exptime via the shared calibration cache, keyed
368
+ by that target rather than either image's own EXPTIME (see _CalibrationCache), falling
369
+ back to an archive query on a cache miss.
370
+
371
+ Args:
372
+ target_exptime: Exptime to search near -- the science exptime for an exact-match
373
+ lookup, or dark_scale_exptime for a reference lookup.
374
+ exptime_max: If given, only accept candidates with EXPTIME <= this (scale-down-only
375
+ enforcement); the result is otherwise accepted whatever its exptime. If not
376
+ given, this is an exact-match lookup: the result is discarded (returns None)
377
+ unless it's actually within dark_exptime_tolerance of target_exptime.
378
+
379
+ Returns:
380
+ The master, or None if nothing usable was found. Never raises.
381
+ """
382
+ if self._calib_cache is None:
383
+ return None
384
+
385
+ try:
386
+ return self._calib_cache.get_from_cache(image, ImageType.DARK, exptime=target_exptime)
387
+ except ValueError:
388
+ pass
389
+
390
+ self._verify_image_header(image)
391
+ instrument = image.header["INSTRUME"]
392
+ binning = "{0}x{0}".format(image.header["XBINNING"]) # noqa: UP031
393
+ time = Time(image.header["DATE-OBS"])
394
+
395
+ master = await Pipeline.find_master(
396
+ self._archive,
397
+ ImageType.DARK,
398
+ time,
399
+ instrument,
400
+ binning,
401
+ None,
402
+ max_days=max_days,
403
+ exptime=target_exptime,
404
+ exptime_tolerance=self._dark_exptime_tolerance,
405
+ exptime_max=exptime_max,
406
+ )
407
+ if master is None:
408
+ return None
409
+
410
+ if exptime_max is None and (
411
+ "EXPTIME" not in master.header
412
+ or not exptimes_close(float(master.header["EXPTIME"]), target_exptime, self._dark_exptime_tolerance)
413
+ ):
414
+ # nearest available, but not actually an exact match (or a legacy pre-#831 master
415
+ # with no EXPTIME header at all) -- not usable for this lookup
416
+ return None
417
+
418
+ self._calib_cache.add_to_cache(master, ImageType.DARK, exptime=target_exptime)
419
+ return master
420
+
421
+ async def _find_master(self, image: Image, image_type: ImageType, max_days: float | None = None) -> Image:
422
+ """Find master calibration frame for given parameters using a cache.
423
+
424
+ Args:
425
+ image_type: image type.
426
+
427
+ Returns:
428
+ Image or None
429
+
430
+ Raises:
431
+ ValueError: if no calibration frame could be found.
432
+ """
433
+
434
+ self._verify_image_header(image)
435
+
436
+ if self._calib_cache is None:
437
+ raise ValueError("No cache.")
438
+
439
+ try:
440
+ return self._calib_cache.get_from_cache(image, image_type)
441
+ except ValueError:
442
+ master = await self._find_master_in_archive(image, image_type, max_days)
443
+ self._calib_cache.add_to_cache(master, image_type)
444
+ return master
445
+
446
+ @staticmethod
447
+ def _verify_image_header(image: Image) -> None:
448
+ has_instrument = "INSTRUME" in image.header
449
+ has_binning = "XBINNING" in image.header
450
+ has_time = "DATE-OBS" in image.header
451
+ # EXPTIME is needed by dark-exptime matching (_find_dark_master) and was already an
452
+ # implicit requirement of _CCDDataCalibrator itself (data_exposure=...EXPTIME...) --
453
+ # checked upfront here instead of surfacing as a bare KeyError mid-calibration.
454
+ has_exptime = "EXPTIME" in image.header
455
+
456
+ if not (has_instrument and has_binning and has_time and has_exptime):
457
+ raise ValueError("Could not fetch items from image header.")
458
+
459
+ async def _find_master_in_archive(
460
+ self, image: Image, image_type: ImageType, max_days: float | None = None
461
+ ) -> Image:
462
+ instrument = image.header["INSTRUME"]
463
+ binning = "{0}x{0}".format(image.header["XBINNING"]) # noqa: UP031
464
+ filter_name = cast(str, image.header["FILTER"]) if "FILTER" in image.header else None
465
+ time = Time(image.header["DATE-OBS"])
466
+
467
+ master = await Pipeline.find_master(
468
+ self._archive,
469
+ image_type,
470
+ time,
471
+ instrument,
472
+ binning,
473
+ None if image_type in [ImageType.BIAS, ImageType.DARK] else filter_name,
474
+ max_days=max_days,
475
+ )
476
+
477
+ if master is None:
478
+ raise ValueError("No master frame found.")
479
+
480
+ return master
481
+
482
+ @staticmethod
483
+ def _copy_original_filename(calibrated: Image, original: Image) -> None:
484
+ if "ORIGNAME" in original.header:
485
+ calibrated.header["L1RAW"] = original.header["ORIGNAME"].replace(".fits", "")
486
+
487
+ @staticmethod
488
+ def _copy_calibration_filename(
489
+ calibrated: Image, bias: Image | None = None, dark: Image | None = None, flat: Image | None = None
490
+ ) -> None:
491
+ if bias is not None:
492
+ calibrated.header["L1BIAS"] = (
493
+ bias.header["FNAME"].replace(".fits.fz", "").replace(".fits", ""),
494
+ "Name of BIAS frame",
495
+ )
496
+ if dark is not None:
497
+ calibrated.header["L1DARK"] = (
498
+ dark.header["FNAME"].replace(".fits.fz", "").replace(".fits", ""),
499
+ "Name of DARK frame",
500
+ )
501
+ if flat is not None:
502
+ calibrated.header["L1FLAT"] = (
503
+ flat.header["FNAME"].replace(".fits.fz", "").replace(".fits", ""),
504
+ "Name of FLAT frame",
505
+ )
506
+
507
+ @staticmethod
508
+ def _set_calibration_headers(calibrated: Image) -> None:
509
+ calibrated.header["BUNIT"] = ("electron", "Unit of pixel values")
510
+ calibrated.header["RLEVEL"] = (1, "Reduction level")
511
+
512
+
513
+ __all__ = ["Calibration"]