exosim 2.2.0__tar.gz → 2.2.1__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 (182) hide show
  1. exosim-2.2.1/CHANGELOG.rst +267 -0
  2. {exosim-2.2.0 → exosim-2.2.1}/CITATION.cff +2 -2
  3. {exosim-2.2.0 → exosim-2.2.1}/PKG-INFO +2 -10
  4. {exosim-2.2.0 → exosim-2.2.1}/RELEASING.md +6 -6
  5. {exosim-2.2.0 → exosim-2.2.1}/codemeta.json +2 -2
  6. {exosim-2.2.0 → exosim-2.2.1}/pyproject.toml +5 -9
  7. exosim-2.2.1/scripts/changelog_extract.py +74 -0
  8. {exosim-2.2.0 → exosim-2.2.1}/scripts/release.py +40 -11
  9. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/__about__.py +4 -1
  10. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/models/channel.py +8 -2
  11. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/models/signal.py +5 -5
  12. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/plots/focal_plane_plotter.py +1 -1
  13. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/recipes/create_focal_plane.py +19 -11
  14. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/recipes/radiometric_model.py +11 -5
  15. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/astrosignal/estimate_planetary_signal.py +4 -1
  16. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/accumulate_sub_exposures.py +7 -3
  17. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/add_cosmic_rays.py +1 -0
  18. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/add_gain_drift.py +5 -0
  19. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/analog_to_digital.py +4 -2
  20. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/apply_pixels_non_linearity.py +3 -1
  21. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/merge_groups.py +11 -0
  22. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/foregrounds/estimate_zodi.py +2 -2
  23. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/compute_sources_pointing_offset.py +33 -10
  24. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/create_focal_plane_array.py +5 -11
  25. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/compute_constant_dark_current_noise.py +5 -5
  26. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/compute_constant_read_noise.py +6 -6
  27. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/subexposures/instantaneous_read_out.py +1 -1
  28. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/subexposures/prepare_instantaneous_read_out.py +1 -44
  29. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tools/dark_current_map.py +9 -7
  30. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tools/pixels_non_linearity_from_correction.py +18 -18
  31. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/focal_plane_locations.py +17 -7
  32. {exosim-2.2.0 → exosim-2.2.1}/src/exosim.egg-info/PKG-INFO +2 -10
  33. {exosim-2.2.0 → exosim-2.2.1}/src/exosim.egg-info/requires.txt +1 -9
  34. {exosim-2.2.0 → exosim-2.2.1}/src/exosim.egg-info/scm_file_list.json +20 -11
  35. exosim-2.2.1/src/exosim.egg-info/scm_version.json +8 -0
  36. exosim-2.2.0/CHANGELOG.rst +0 -135
  37. exosim-2.2.0/scripts/changelog_extract.py +0 -51
  38. exosim-2.2.0/src/exosim.egg-info/scm_version.json +0 -8
  39. {exosim-2.2.0 → exosim-2.2.1}/.gitignore +0 -0
  40. {exosim-2.2.0 → exosim-2.2.1}/CODE_OF_CONDUCT.md +0 -0
  41. {exosim-2.2.0 → exosim-2.2.1}/LICENSE +0 -0
  42. {exosim-2.2.0 → exosim-2.2.1}/MANIFEST.in +0 -0
  43. {exosim-2.2.0 → exosim-2.2.1}/README.md +0 -0
  44. {exosim-2.2.0 → exosim-2.2.1}/setup.cfg +0 -0
  45. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/__init__.py +0 -0
  46. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/data/Zodi_map.hdf5 +0 -0
  47. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/exosim.py +0 -0
  48. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/log/__init__.py +0 -0
  49. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/log/logger.py +0 -0
  50. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/log/structlog_config.py +0 -0
  51. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/models/__init__.py +0 -0
  52. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/models/utils/__init__.py +0 -0
  53. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/models/utils/cached_data.py +0 -0
  54. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/output/__init__.py +0 -0
  55. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/output/hdf5/__init__.py +0 -0
  56. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/output/hdf5/hdf5.py +0 -0
  57. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/output/hdf5/utils.py +0 -0
  58. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/output/output.py +0 -0
  59. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/output/set_output.py +0 -0
  60. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/output/utils.py +0 -0
  61. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/plots/__init__.py +0 -0
  62. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/plots/ndrs_plotter.py +0 -0
  63. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/plots/plotter.py +0 -0
  64. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/plots/radiometric_plotter.py +0 -0
  65. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/plots/sub_exposures_plotter.py +0 -0
  66. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/plots/utils.py +0 -0
  67. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/py.typed +0 -0
  68. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/recipes/__init__.py +0 -0
  69. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/recipes/create_ndrs.py +0 -0
  70. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/recipes/create_sub_exposures.py +0 -0
  71. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/recipes/simulate_observation.py +0 -0
  72. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/__init__.py +0 -0
  73. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/astrosignal/__init__.py +0 -0
  74. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/astrosignal/apply_astronomical_signal.py +0 -0
  75. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/astrosignal/estimate_astronomical_signal.py +0 -0
  76. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/astrosignal/find_astronomical_signals.py +0 -0
  77. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/chain_task.py +0 -0
  78. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/__init__.py +0 -0
  79. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/add_constant_dark_current.py +0 -0
  80. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/add_dark_current_map_numpy.py +0 -0
  81. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/add_ktc.py +0 -0
  82. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/add_read_noise.py +0 -0
  83. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/add_read_noise_map_numpy.py +0 -0
  84. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/add_shot_noise.py +0 -0
  85. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/apply_dead_pixel_map.py +0 -0
  86. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/apply_dead_pixel_map_numpy.py +0 -0
  87. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/apply_simple_saturation.py +0 -0
  88. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/load_pixels_non_linearity_map.py +0 -0
  89. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/detector/load_pixels_non_linearity_map_numpy.py +0 -0
  90. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/foregrounds/__init__.py +0 -0
  91. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/__init__.py +0 -0
  92. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/apply_intra_pixel_response_function.py +0 -0
  93. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/compute_saturation.py +0 -0
  94. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/compute_solid_angle.py +0 -0
  95. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/create_focal_plane.py +0 -0
  96. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/create_intrapixel_response_function.py +0 -0
  97. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/create_oversampled_intrapixel_response_function.py +0 -0
  98. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/foregrounds_to_focal_plane.py +0 -0
  99. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/load_psf.py +0 -0
  100. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/load_psf_paos.py +0 -0
  101. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/load_psf_paos_time_interp.py +0 -0
  102. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/load_responsivity.py +0 -0
  103. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/load_wavelength_solution.py +0 -0
  104. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/populate_focal_plane.py +0 -0
  105. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/propagate_foregrounds.py +0 -0
  106. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/instrument/propagate_sources.py +0 -0
  107. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/load/__init__.py +0 -0
  108. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/load/load_optical_element.py +0 -0
  109. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/load/load_optical_element_hdf5.py +0 -0
  110. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/load/load_options.py +0 -0
  111. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/load/load_source_list.py +0 -0
  112. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/parse/__init__.py +0 -0
  113. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/parse/parse_optical_element.py +0 -0
  114. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/parse/parse_path.py +0 -0
  115. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/parse/parse_source.py +0 -0
  116. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/parse/parse_zodi.py +0 -0
  117. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/__init__.py +0 -0
  118. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/aperture_photometry.py +0 -0
  119. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/compute_custom_noise.py +0 -0
  120. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/compute_integration_time.py +0 -0
  121. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/compute_observation_efficiency.py +0 -0
  122. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/compute_observation_efficiency_from_dead_time.py +0 -0
  123. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/compute_photon_noise.py +0 -0
  124. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/compute_signals_channel.py +0 -0
  125. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/compute_sub_frg_signals_channel.py +0 -0
  126. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/compute_total_noise.py +0 -0
  127. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/estimate_apertures.py +0 -0
  128. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/estimate_spectral_binning.py +0 -0
  129. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/load_apertures.py +0 -0
  130. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/multiaccum.py +0 -0
  131. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/saturation_channel.py +0 -0
  132. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/utils/__init__.py +0 -0
  133. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/utils/compute_multiaccum.py +0 -0
  134. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/utils/compute_noise_column.py +0 -0
  135. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/utils/compute_photon_noise.py +0 -0
  136. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/utils/compute_saturation.py +0 -0
  137. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/utils/create_table.py +0 -0
  138. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/radiometric/utils/update_total_noise.py +0 -0
  139. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/sed/__init__.py +0 -0
  140. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/sed/create_custom_source.py +0 -0
  141. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/sed/create_planck_star.py +0 -0
  142. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/sed/download_sed.py +0 -0
  143. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/sed/load_custom.py +0 -0
  144. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/sed/load_phoenix.py +0 -0
  145. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/sed/prepare_sed.py +0 -0
  146. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/subexposures/__init__.py +0 -0
  147. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/subexposures/add_foregrounds.py +0 -0
  148. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/subexposures/apply_qe_map.py +0 -0
  149. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/subexposures/compute_reading_scheme.py +0 -0
  150. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/subexposures/estimate_ch_jitter.py +0 -0
  151. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/subexposures/estimate_pointing_jitter.py +0 -0
  152. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/subexposures/load_ils.py +0 -0
  153. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/subexposures/load_qe_map.py +0 -0
  154. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/subexposures/load_qe_map_numpy.py +0 -0
  155. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tasks/task.py +0 -0
  156. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tools/__init__.py +0 -0
  157. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tools/adc_gain_estimator.py +0 -0
  158. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tools/dead_pixels_map.py +0 -0
  159. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tools/exosim_tool.py +0 -0
  160. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tools/pixels_non_linearity.py +0 -0
  161. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tools/quantum_efficiency_map.py +0 -0
  162. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/tools/readout_scheme_calculator.py +0 -0
  163. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/__init__.py +0 -0
  164. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/aperture.py +0 -0
  165. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/ascii_arts.py +0 -0
  166. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/binning.py +0 -0
  167. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/checks.py +0 -0
  168. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/convolution.py +0 -0
  169. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/grids.py +0 -0
  170. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/iterators.py +0 -0
  171. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/klass_factory.py +0 -0
  172. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/operations.py +0 -0
  173. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/output_cleaners.py +0 -0
  174. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/prepare_recipes.py +0 -0
  175. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/psf.py +0 -0
  176. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/run_config.py +0 -0
  177. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/timed_class.py +0 -0
  178. {exosim-2.2.0 → exosim-2.2.1}/src/exosim/utils/types.py +0 -0
  179. {exosim-2.2.0 → exosim-2.2.1}/src/exosim.egg-info/SOURCES.txt +0 -0
  180. {exosim-2.2.0 → exosim-2.2.1}/src/exosim.egg-info/dependency_links.txt +0 -0
  181. {exosim-2.2.0 → exosim-2.2.1}/src/exosim.egg-info/entry_points.txt +0 -0
  182. {exosim-2.2.0 → exosim-2.2.1}/src/exosim.egg-info/top_level.txt +0 -0
@@ -0,0 +1,267 @@
1
+ ===========
2
+ Changelog
3
+ ===========
4
+
5
+ All notable changes to this project will be documented in this file.
6
+
7
+ The format is based on Keep a Changelog (keepachangelog_), and this project adheres
8
+ to Semantic Versioning (semver_).
9
+
10
+ Unreleased changes live as fragments in ``changelog.d/`` and are merged here by
11
+ ``nox -s release``. See ``docs/source/contributing/releasing.rst``.
12
+
13
+ .. scriv-insert-here
14
+
15
+ .. _changelog-2.2.1:
16
+
17
+ [2.2.1_] - Pipeline hardening
18
+ =============================
19
+
20
+ Changed
21
+ -------
22
+
23
+ - user guide and developer guide reworked for clarity: tighter prose, corrected
24
+ grammar, consistent sentence-case headings, and each mechanism now explains
25
+ the reason behind it, not just the behaviour
26
+ - home page, user-guide and developer-guide landing pages now share the same
27
+ card-based navigation with consistent line icons, rendered from
28
+ reStructuredText instead of hand-written HTML and clip-art
29
+
30
+ Removed
31
+ -------
32
+
33
+ - unused documentation dependencies from the ``docs`` extra
34
+ (``sphinx-rtd-theme``, ``sphinx-panels``, ``sphinxcontrib-napoleon``,
35
+ ``nbsphinx``, ``myst-parser`` and a few transitive-only pins)
36
+
37
+ - dead duplicate ``PrepareInstantaneousReadOut.force_power_conservation`` method
38
+ (the live implementation is in ``InstantaneousReadOut``)
39
+
40
+ Fixed
41
+ -----
42
+
43
+ - ``nox -s docs`` and ``nox -s docs-live`` now install the ``docs`` extra, so the
44
+ documentation builds on a clean checkout (Sphinx and the theme were missing)
45
+ - two broken links on the user-guide landing page
46
+ - several ``note`` and ``warning`` admonitions that were not rendering because
47
+ their content was not indented
48
+ - broken cross-references to the installation instructions from the quick-start
49
+ and FAQ pages
50
+ - malformed XML in the custom-task example (``unit`` attributes now quoted) and a
51
+ duplicate documentation label for pixel non-linearity
52
+ - stale continuous-integration details in the contributing guide (workflow name
53
+ and supported Python versions)
54
+ - title overline/underline mismatch on the sky-sources page that newer Sphinx
55
+ rejects
56
+ - corrupted LaTeX in several task docstrings (``\frac``, ``\times`` and similar
57
+ were being read as escape characters), so the rendered formulae for read
58
+ noise, dark current noise and the dark-current map are now correct
59
+ - the ExoSim logo no longer overflows and clips the page header
60
+
61
+ - ``FocalPlanePlotter`` failed with ``KeyError: 'channels'`` on a normal
62
+ focal-plane output: it looked for ``channels/channels`` instead of
63
+ ``channels``
64
+
65
+ - ``exosim.__branch__`` and ``exosim.__commit__`` were always ``None``: the git
66
+ metadata reader looked for ``.git`` one directory too shallow
67
+
68
+ - ``InstantaneousReadOut`` in memory-saving mode (``slicing=True``) crashed with
69
+ ``AttributeError: 'InstantaneousReadOut' object has no attribute
70
+ 'getOversampleFactors'`` whenever the pointing jitter needed sub-pixel
71
+ magnification; the call now uses the renamed ``get_oversample_factors``
72
+
73
+ - the target-list mode of ``RadiometricModel`` (a ``targetlist_filepath`` under
74
+ ``sky/source``) crashed with ``KeyError: component not found``: it rebinned
75
+ the channel efficiencies from the file root instead of from the per-target
76
+ ``targets/<name>/channels`` group. The recipe now runs to completion and
77
+ writes a radiometric table for every target in the list.
78
+
79
+ - ``RadiometricModel`` and ``CreateFocalPlane`` failed on a payload with a
80
+ single, unwrapped ``channel`` entry (``KeyError: component not found`` /
81
+ ``KeyError: 'Photometer'``): the efficiency-rebinning and the
82
+ foreground/source signal helpers iterated the channel dictionary as if it
83
+ were the multi-channel mapping. Single-channel configurations now run through
84
+ the whole radiometric pipeline.
85
+
86
+ - ``Signal.get_slice`` / ``Signal.set_slice`` now accept a plain ``float``
87
+ start/end time as documented (assumed to be hours); before they raised
88
+ ``AttributeError``
89
+ - ``AddGainDrift`` raises a clear ``KeyError`` when neither
90
+ ``gain_drift_amplitude`` nor the amplitude range is configured, instead of an
91
+ ``UnboundLocalError``
92
+ - ``EstimatePlanetarySignal`` raises a clear ``ValueError`` when the planet
93
+ ``rp`` is neither a file path nor a number, instead of an
94
+ ``UnboundLocalError``
95
+ - ``Channel.target_source`` raises an informative ``KeyError`` when several
96
+ sources are defined but none (or more than one) is flagged as the science
97
+ target, instead of an ``IndexError``
98
+ - ``RadiometricModel`` now passes the per-channel configuration to the
99
+ sub-foreground signal task, so the ``<contribution>_total_signal`` columns are
100
+ computed for every channel type
101
+ - ``CreateFocalPlane`` infers the wavelength grid from the channel passbands
102
+ (``channel``/``wl_min``/``wl_max``) when ``wl_grid`` is absent from the
103
+ configuration; the previous fallback looked up a non-existent ``channels`` key
104
+ and always used the 1-2 micron default
105
+ - the spectrometer focal-plane builder no longer relies on a fragile
106
+ buffer-level ``array == zeros`` comparison to detect a flat wavelength axis
107
+ (in ``CreateFocalPlaneArray._wav_osr`` and the PSF window locator); it now
108
+ checks the values explicitly
109
+ - ``locate_wavelength_windows`` raises a clear error for an unsupported PSF
110
+ dimensionality or channel type instead of returning ``None`` (which made the
111
+ caller fail with an opaque unpacking ``TypeError``)
112
+ - ``EstimateZodi`` no longer loops forever when the bundled ``data`` directory
113
+ cannot be found by walking up from the module; the search is bounded and
114
+ raises ``OSError`` if it fails
115
+ - ``AccumulateSubExposures`` corrupted the ramp of a single-exposure
116
+ acquisition: the first chunk read ``state_machine[-1]`` / ``dataset[-1]`` (the
117
+ wrapped-around last frame) as its carry-in offset. The first chunk now starts
118
+ from zero
119
+ - ``AnalogToDigital`` clips the converted signal to the ADC full scale; a bright
120
+ pixel above full scale with a fixed ADC gain used to wrap around on the
121
+ integer cast (e.g. 200000 counts became 3392 on a 16-bit ADC)
122
+ - ``AddCosmicRays`` no longer fails with an ``UnboundLocalError`` when the
123
+ configured ``interaction_shapes`` probabilities sum to less than one; the
124
+ event shape falls back to the last configured shape
125
+ - ``MergeGroups`` raises a clear error when the number of sub-exposures is not
126
+ ``n_groups * n_ndrs`` instead of silently mis-averaging a truncated group or
127
+ raising ``IndexError``
128
+ - ``ComputeSourcesPointingOffset`` computes the source-to-pointing offset with
129
+ ``SkyCoord.spherical_offsets_to``, so it handles the RA 0/360 wrap and the
130
+ ``cos(dec)`` foreshortening (a plain RA/Dec difference was wrong away from the
131
+ equator and near RA = 0); the offset is now rounded to the nearest sub-pixel
132
+ rather than truncated
133
+
134
+ Security
135
+ --------
136
+
137
+ - refreshed the locked dependency versions to pull in security fixes for
138
+ ``pillow``, ``requests``, ``urllib3``, ``cryptography``, ``idna``,
139
+ ``jupyter-server``, ``jupyterlab``, ``notebook``, ``nbconvert``, ``mistune``,
140
+ ``tornado``, ``pygments`` and the ``safety`` toolchain (``authlib``,
141
+ ``nltk``); this clears the outstanding Dependabot alerts on ``uv.lock``. The
142
+ release still installs the highest compatible versions for end users, so this
143
+ only affects reproducible development and CI environments
144
+
145
+ .. _changelog-2.2.0:
146
+
147
+ [2.2.0_] - End-to-end simulations
148
+ =================================
149
+
150
+ Added
151
+ -----
152
+
153
+ - new recipe ``SimulateObservation`` to run the full simulation pipeline from an options file to the NDRs
154
+ - radiometric model now supports target-list mode
155
+ - new documentation section for the radiometric model with a detailed description of the pipeline and operating modes
156
+ - observation efficiency can now be specified in the options file
157
+ - ``slim_output`` option for recipes to reduce output file size by dropping intermediate data products
158
+ - SED download task to fetch stellar spectra from online databases (currently Phoenix models only)
159
+
160
+ Changed
161
+ -------
162
+
163
+ - ``fast_convolution`` now uses ``scipy.signal.fftconvolve`` for better performance and accuracy
164
+ - saturation time now includes the well-depth fraction; integration time is the minimum saturation time across the detector; frame time is integration time divided by observation efficiency
165
+ - photon noise is now computed on the observation-efficiency-corrected signal
166
+ - packaging and dependency management moved from Poetry to uv
167
+ - logging moved to ``structlog``
168
+ - ``wl_interpolate`` reworked to use argsort + inverse mapping, avoiding O(n^2) lookups and extra copies
169
+ - ExoSim can now run without any source (no input SED)
170
+ - versioning is now derived from git tags via setuptools-scm; releases are cut with ``nox -s release``
171
+
172
+ Fixed
173
+ -----
174
+
175
+ - normalization in ``Signal`` now handles custom units correctly
176
+ - foreground is now decimated before being added to the focal plane instead of summed
177
+ - inverted parameters in the wavelength-centering equation, to avoid a polynomial inversion
178
+ - wrong wavelength order for ``loadPsfPaos``
179
+
180
+ [2.1.1_] - CLI Fixed
181
+ ====================
182
+ Fixed
183
+ -----
184
+ - missing summary for cli.
185
+
186
+ [2.1.0_] - Hello Python 3.12
187
+ ============================
188
+ Added
189
+ -----
190
+ - added unit normalization in Signal
191
+
192
+ Changed
193
+ -------
194
+ - dropped support for Python 3.9 and 3.10. Now supports only Python >3.12
195
+ - test framework changed into pytest
196
+ - added default value for low frequency time grid
197
+
198
+
199
+
200
+ [2.0.1_] - stable release
201
+ =========================
202
+ Added
203
+ -----
204
+ - improved exosim CLI with new click commands
205
+ - option to load optical elements from .hdf5 files (LoadOpticalElementHDF5)
206
+ - option to parse options file replacing a custom keyword with a value (LoadOptions)
207
+ - added codemeta.json file for better metadata management
208
+ - warning for misuse of oversampling factor and intra-pixel response function
209
+ - added support for wavelength dependent limb darkening
210
+ Changed
211
+ -------
212
+ - updated citation information
213
+ - rp in astronomical signal is now binned with the same binning method as Signal
214
+ Fixed
215
+ -----
216
+ - Custom SED units and documentation (added missing angle in loadCustom.py)
217
+ - find keys is not case sensitive anymore
218
+ - cosmic rays now work for low rates
219
+ - fixed elliptical PSF from PAOS
220
+ - ADC offset implemented as trigger offset (no negative values)
221
+ - astronomical signal now is weighted by the stellar flux on the detector
222
+
223
+ [2.0.0-rc2_] - ADC automode fix and minor improvements
224
+ ======================================================
225
+ Added
226
+ -----
227
+ - boundaries for random amplitude in gain noise
228
+ - option to force non-time-dependent PSF for LoadPsfPaos (`time_dependence` parameter to `False`)
229
+ - option to slice jitter to save memory when the focal plane is sampled at high frequencies (`slicing` parameter to `True`)
230
+ Changed
231
+ -------
232
+ - dependencies versions updated to support `Poetry` for Python 3.10
233
+ Fixed
234
+ -----
235
+ - added offset and "auto" mode to ADC
236
+ - removed Numba in populateFocalPlane (better numerical accuracy)
237
+ - interp2d replaced with RectBivariateSpline in LoadPsfPaos to support Scipy 1.10+
238
+
239
+ [2.0.0-rc1_] - Scipy compatibility fix
240
+ ======================================
241
+ Added
242
+ -----
243
+ - .npy input support for pixel non-linearity coefficients (LoadPixelsNonLinearityMapNumpy)
244
+ - multiplicative noise simulator (AddGainNoise)
245
+
246
+ Fixed
247
+ -----
248
+ - replaced scipy.convolve with scipy.signal.convolve
249
+ - ADC now works with unsigned integers
250
+
251
+ [v2.0.0-rc0_] - Release Candidate
252
+ ================================
253
+ Cleaned repository
254
+
255
+ .. _v2.0.0-rc0: https://github.com/arielmission-space/ExoSim2.0/releases/tag/v2.0.0-rc0
256
+ .. _2.0.0-rc1: https://github.com/arielmission-space/ExoSim2.0/releases/tag/v2.0.0-rc1
257
+ .. _2.0.0-rc2: https://github.com/arielmission-space/ExoSim2.0/releases/tag/v2.0.0-rc2
258
+ .. _2.0.1: https://github.com/arielmission-space/ExoSim2.0/releases/tag/v2.0.1
259
+ .. _2.1.0: https://github.com/arielmission-space/ExoSim2.0/releases/tag/v2.1.0
260
+ .. _2.1.1: https://github.com/arielmission-space/ExoSim2.0/releases/tag/v2.1.1
261
+
262
+ .. _2.2.0: https://github.com/arielmission-space/ExoSim2.0/releases/tag/v2.2.0
263
+
264
+ .. _2.2.1: https://github.com/arielmission-space/ExoSim2.0/releases/tag/v2.2.1
265
+
266
+ .. _keepachangelog: https://keepachangelog.com/en/1.0.0/
267
+ .. _semver: https://semver.org/spec/v2.0.0.html
@@ -18,5 +18,5 @@ authors:
18
18
 
19
19
  title: "ExoSim 2: the new exoplanet observation simulator applied to the Ariel space mission"
20
20
  doi: 10.1007/s10686-024-09976-2
21
- version: 2.2.0
22
- date-released: 2026-08-27
21
+ version: 2.2.1
22
+ date-released: 2026-08-31
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exosim
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: ExoSim 2 is the next generation of the Exoplanet Observation Simulator (ExoSim) tailored for spectro-photometric observations of transiting exoplanets from space, ground, and sub-orbital platforms. This software is a complete rewrite implemented in Python 3, embracing object-oriented design principles.
5
5
  Author: E. Pascale, A. Bocchieri, A. Lorenzani, A. Papageorgiou
6
6
  Author-email: "L. V. Mugnai" <mugnail@cardiff.ac.uk>
@@ -58,18 +58,10 @@ Requires-Dist: safety>=3.2.11; extra == "dev"
58
58
  Requires-Dist: bandit>=1.8.0; extra == "dev"
59
59
  Provides-Extra: docs
60
60
  Requires-Dist: sphinx>=8.1.3; extra == "docs"
61
- Requires-Dist: sphinx-rtd-theme>=3.0.2; extra == "docs"
62
- Requires-Dist: sphinxcontrib-napoleon; extra == "docs"
63
- Requires-Dist: sphinxcontrib-jsmath; extra == "docs"
64
- Requires-Dist: nbsphinx; extra == "docs"
65
- Requires-Dist: sphinx-panels; extra == "docs"
61
+ Requires-Dist: pydata-sphinx-theme; extra == "docs"
66
62
  Requires-Dist: sphinx-autoapi; extra == "docs"
67
63
  Requires-Dist: sphinx-autobuild; extra == "docs"
68
64
  Requires-Dist: sphinx-design; extra == "docs"
69
- Requires-Dist: sphinxcontrib-serializinghtml; extra == "docs"
70
- Requires-Dist: docutils; extra == "docs"
71
- Requires-Dist: pydata-sphinx-theme; extra == "docs"
72
- Requires-Dist: myst-parser>=4.0.0; extra == "docs"
73
65
  Provides-Extra: jupyter
74
66
  Requires-Dist: pyzmq<27,>=26; extra == "jupyter"
75
67
  Requires-Dist: notebook; extra == "jupyter"
@@ -15,14 +15,14 @@ Full guide: `docs/source/contributing/releasing.rst`
15
15
  nox -s release
16
16
  ```
17
17
 
18
- It asks major/minor/patch, folds the changelog fragments in, updates
19
- `CITATION.cff` / `codemeta.json`, commits, fast-forwards `main`, and pushes the
20
- `vX.Y.Z` tag.
18
+ It asks major/minor/patch **and a short release name**, folds the changelog
19
+ fragments in, updates `CITATION.cff` / `codemeta.json`, commits, fast-forwards
20
+ `main`, and pushes the `vX.Y.Z` tag.
21
21
 
22
22
  - The tag triggers `.github/workflows/release.yml`:
23
- build → TestPyPI → verify install → PyPI → GitHub Release.
24
- Then `sync-to-public.yml` mirrors `main` to `ExoSim2-public` and copies the
25
- release there.
23
+ build → TestPyPI → verify install → PyPI → GitHub Release → clone of the
24
+ tag + release + wheels into `ExoSim2-public`.
25
+ `sync-to-public.yml` mirrors the `main` code on every push.
26
26
 
27
27
  ## One-time setup
28
28
 
@@ -40,12 +40,12 @@
40
40
  "license": "https://spdx.org/licenses/BSD-3-Clause",
41
41
  "name": "ExoSim2",
42
42
  "programmingLanguage": "Python 3",
43
- "version": "2.2.0",
43
+ "version": "2.2.1",
44
44
  "codemeta:contIntegration": {
45
45
  "id": "https://app.codecov.io/gh/arielmission-space/ExoSim2-public"
46
46
  },
47
47
  "continuousIntegration": "https://app.codecov.io/gh/arielmission-space/ExoSim2-public",
48
48
  "issueTracker": "https://github.com/arielmission-space/ExoSim2-public/issues",
49
49
  "referencePublication": "https://link.springer.com/article/10.1007/s10686-024-09976-2",
50
- "dateModified": "2026-08-27"
50
+ "dateModified": "2026-08-31"
51
51
  }
@@ -84,18 +84,10 @@ dev = [
84
84
 
85
85
  docs = [
86
86
  "sphinx>=8.1.3",
87
- "sphinx-rtd-theme>=3.0.2",
88
- "sphinxcontrib-napoleon",
89
- "sphinxcontrib-jsmath",
90
- "nbsphinx",
91
- "sphinx-panels",
87
+ "pydata-sphinx-theme",
92
88
  "sphinx-autoapi",
93
89
  "sphinx-autobuild",
94
90
  "sphinx-design",
95
- "sphinxcontrib-serializinghtml",
96
- "docutils",
97
- "pydata-sphinx-theme",
98
- "myst-parser>=4.0.0",
99
91
  ]
100
92
 
101
93
  jupyter = [
@@ -323,6 +315,10 @@ exclude_lines = [
323
315
  "if __name__ == \\.__main__\\.",
324
316
  "class .*\\bProtocol\\):",
325
317
  "@(abc\\.)?abstractmethod",
318
+ # numba-compiled kernels: coverage.py cannot trace into JIT-compiled code,
319
+ # so the decorated function bodies are excluded. They are still exercised
320
+ # behaviourally by the detector, sub-exposure and instrument test suites.
321
+ "^\\s*@(jit|njit)\\(",
326
322
  ]
327
323
  ignore_errors = true
328
324
  show_missing = true
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ """Read the CHANGELOG.rst entry for a given version (used to build release notes).
3
+
4
+ Usage:
5
+ python scripts/changelog_extract.py 2.2.0 # print the entry body
6
+ python scripts/changelog_extract.py 2.2.0 --title # print just the title
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ CHANGELOG = Path(__file__).resolve().parent.parent / "CHANGELOG.rst"
15
+
16
+
17
+ def _find_heading(lines: list[str], version: str) -> int | None:
18
+ for i, line in enumerate(lines):
19
+ if line.startswith((f"[{version}]", f"[{version}_]")):
20
+ return i
21
+ return None
22
+
23
+
24
+ def extract_title(version: str) -> str:
25
+ """Return the human part of the heading, e.g. "CLI fixes" from
26
+ ``[2.1.1_] - CLI fixes``. Falls back to the bare version."""
27
+ lines = CHANGELOG.read_text().splitlines()
28
+ i = _find_heading(lines, version)
29
+ if i is None:
30
+ return version
31
+ heading = lines[i]
32
+ _, _, rest = heading.partition("] - ")
33
+ rest = rest.strip()
34
+ return rest or version
35
+
36
+
37
+ def extract_body(version: str) -> str:
38
+ lines = CHANGELOG.read_text().splitlines()
39
+ start = _find_heading(lines, version)
40
+ if start is None:
41
+ return f"Release {version}.\n"
42
+
43
+ body: list[str] = []
44
+ for line in lines[start + 2 :]: # skip heading + its underline
45
+ stripped = line.strip()
46
+ if stripped.startswith("[") and "]" in stripped:
47
+ break
48
+ if stripped.startswith((".. _", ".. scriv")):
49
+ break
50
+ body.append(line)
51
+
52
+ while body and not body[0].strip():
53
+ body.pop(0)
54
+ while body and not body[-1].strip():
55
+ body.pop()
56
+ return "\n".join(body) + "\n"
57
+
58
+
59
+ def main() -> int:
60
+ args = sys.argv[1:]
61
+ want_title = "--title" in args
62
+ args = [a for a in args if a != "--title"]
63
+ if len(args) != 1:
64
+ sys.stderr.write("usage: changelog_extract.py <version> [--title]\n")
65
+ return 2
66
+ version = args[0]
67
+ sys.stdout.write(extract_title(version) if want_title else extract_body(version))
68
+ if want_title:
69
+ sys.stdout.write("\n")
70
+ return 0
71
+
72
+
73
+ if __name__ == "__main__":
74
+ raise SystemExit(main())
@@ -140,6 +140,15 @@ def choose_part(cli_part: str | None) -> str:
140
140
  print(" please answer 'major', 'minor' or 'patch'")
141
141
 
142
142
 
143
+ def choose_title() -> str:
144
+ """Ask for a short human name for the release (e.g. 'CLI fixes')."""
145
+ while True:
146
+ title = input("Release name (short, e.g. 'CLI fixes'): ").strip()
147
+ if title:
148
+ return title
149
+ print(" a release name is required")
150
+
151
+
143
152
  # --------------------------------------------------------------------------- #
144
153
  # Quality gate
145
154
  # --------------------------------------------------------------------------- #
@@ -154,20 +163,33 @@ def quality_gate() -> None:
154
163
  # --------------------------------------------------------------------------- #
155
164
  # Changelog + metadata
156
165
  # --------------------------------------------------------------------------- #
157
- def collect_changelog(version: str) -> None:
158
- run("uv", "run", "scriv", "collect", "--version", version)
166
+ def collect_changelog(version: str, title: str) -> None:
167
+ heading = f"[{version}_] - {title}"
159
168
 
160
169
  changelog = REPO_ROOT / "CHANGELOG.rst"
170
+ if f"[{version}_]" in changelog.read_text():
171
+ raise ReleaseError(f"CHANGELOG.rst already has an entry for {version}")
172
+
173
+ # scriv >= 1.8 refuses --version together with --title. We want a named
174
+ # heading (not the date-based default from ``entry_title_template``), so we
175
+ # pass only --title and add the ``.. _changelog-<version>:`` cross-reference
176
+ # anchor ourselves -- scriv only emits that on the --version code path.
177
+ run("uv", "run", "scriv", "collect", "--title", heading)
178
+
161
179
  text = changelog.read_text()
162
180
 
181
+ anchor = f".. _changelog-{version}:"
182
+ if anchor not in text:
183
+ text = text.replace(f"{heading}\n", f"{anchor}\n\n{heading}\n", 1)
184
+
163
185
  link = f".. _{version}: {TAG_URL.format(version=version)}\n"
164
186
  if link not in text:
165
187
  marker = "\n.. _keepachangelog:"
166
188
  if marker not in text:
167
189
  raise ReleaseError("could not find the link block in CHANGELOG.rst")
168
190
  text = text.replace(marker, f"\n{link}{marker}", 1)
169
- changelog.write_text(text)
170
191
 
192
+ changelog.write_text(text)
171
193
  (REPO_ROOT / "docs" / "source" / "CHANGELOG.rst").write_text(text)
172
194
 
173
195
 
@@ -204,10 +226,10 @@ def update_codemeta(version: str, today: str) -> None:
204
226
  # --------------------------------------------------------------------------- #
205
227
  # Git release
206
228
  # --------------------------------------------------------------------------- #
207
- def commit_and_tag(version: str) -> None:
229
+ def commit_and_tag(version: str, title: str) -> None:
208
230
  tag = f"v{version}"
209
231
  git("add", "-A", capture=False)
210
- git("commit", "-m", f"docs: release {version}", capture=False)
232
+ git("commit", "-m", f"docs: release {version} — {title}", capture=False)
211
233
 
212
234
  print("\nChanges staged for the release commit:")
213
235
  print(git("show", "--stat", "HEAD"))
@@ -240,11 +262,17 @@ def commit_and_tag(version: str) -> None:
240
262
  f"git push {REMOTE} {tag}"
241
263
  )
242
264
 
243
- git("tag", "-a", tag, "-m", f"Release {version}", capture=False)
265
+ git("tag", "-a", tag, "-m", f"{version} — {title}", capture=False)
244
266
  git("push", REMOTE, tag, capture=False)
245
267
 
246
- print(f"\n✅ Tag {tag} pushed. Release pipeline: {ACTIONS_URL}")
247
- print(f" GitHub Release will appear at: {TAG_URL.format(version=version)}")
268
+ print(f"\n✅ Tag {tag} pushed. The release pipeline now runs:")
269
+ print(f" {ACTIONS_URL}")
270
+ print(" build → TestPyPI → verify → PyPI → GitHub Release →")
271
+ print(
272
+ " clone of the tag + release + wheels into arielmission-space/ExoSim2-public"
273
+ )
274
+ print(f"\n GitHub Release: {TAG_URL.format(version=version)}")
275
+ print(f" PyPI: https://pypi.org/project/exosim/{version}/")
248
276
 
249
277
 
250
278
  # --------------------------------------------------------------------------- #
@@ -271,8 +299,9 @@ def main(argv: list[str] | None = None) -> int:
271
299
  current = latest_stable()
272
300
  part = choose_part(args.part)
273
301
  version = next_version(current, part)
302
+ title = choose_title()
274
303
  cur_str = ".".join(str(x) for x in current)
275
- print(f"\nRelease: {cur_str} -> {version} ({part})")
304
+ print(f"\nRelease: {cur_str} -> {version} ({part}) — {title!r}")
276
305
  if not confirm("Proceed?"):
277
306
  raise ReleaseError("cancelled")
278
307
 
@@ -282,10 +311,10 @@ def main(argv: list[str] | None = None) -> int:
282
311
  quality_gate()
283
312
 
284
313
  today = dt.date.today().isoformat()
285
- collect_changelog(version)
314
+ collect_changelog(version, title)
286
315
  update_citation(version, today)
287
316
  update_codemeta(version, today)
288
- commit_and_tag(version)
317
+ commit_and_tag(version, title)
289
318
  except ReleaseError as exc:
290
319
  print(f"\n✗ {exc}", file=sys.stderr)
291
320
  return 1
@@ -165,7 +165,10 @@ def _get_git_info() -> tuple[str | None, str | None]:
165
165
  tuple: (branch_name, commit_hash) or (None, None) if not in git repo
166
166
  """
167
167
  try:
168
- base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
168
+ # __file__ is <root>/src/exosim/__about__.py -> walk up to <root>
169
+ base_dir = os.path.dirname(
170
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
171
+ )
169
172
  except NameError:
170
173
  return None, None
171
174
 
@@ -295,13 +295,19 @@ class Channel(log.Logger):
295
295
  target = [
296
296
  source
297
297
  for source, param in self.sources.items()
298
- if "source_target" in param.metadata["parsed_parameters"]
299
- and param.metadata["parsed_parameters"]["source_target"]
298
+ if param.metadata.get("parsed_parameters", {}).get("source_target")
300
299
  ]
300
+ if not target:
301
+ self.error(
302
+ "Multiple sources are defined but none is flagged as the science "
303
+ "target. Set 'source_target' on one source in the input file."
304
+ )
305
+ raise KeyError("no source is flagged as 'source_target'")
301
306
  if len(target) > 1:
302
307
  self.error(
303
308
  "More than one target source found. Please check your input file."
304
309
  )
310
+ raise KeyError("more than one source is flagged as 'source_target'")
305
311
  self.debug(f"Target source is {target[0]}")
306
312
  return target[0]
307
313
 
@@ -484,15 +484,15 @@ class Signal(log.Logger):
484
484
  ) -> tuple[int, int]:
485
485
  """This function finds a data slice given a time interval."""
486
486
  if isinstance(start_time, u.Quantity):
487
- start_time = start_time.to(u.hr)
487
+ start_time = start_time.to_value(u.hr)
488
488
  else:
489
489
  self.debug("start time assumed to be expressed in hr")
490
490
  if isinstance(end_time, u.Quantity):
491
- end_time = end_time.to(u.hr)
491
+ end_time = end_time.to_value(u.hr)
492
492
  else:
493
- self.debug("start time assumed to be expressed in hr")
494
- start = np.argmin(np.abs(self.time - start_time.value))
495
- stop = np.argmin(np.abs(self.time - end_time.value))
493
+ self.debug("end time assumed to be expressed in hr")
494
+ start = np.argmin(np.abs(self.time - start_time))
495
+ stop = np.argmin(np.abs(self.time - end_time))
496
496
  return start, stop
497
497
 
498
498
  def get_slice(self, start_time: ValueType, end_time: ValueType) -> np.ndarray:
@@ -71,7 +71,7 @@ class FocalPlanePlotter(log.Logger):
71
71
  if isinstance(self.input, str)
72
72
  else "channels"
73
73
  )
74
- channels = f[channels_path]["channels"] if channels_path else f["channels"]
74
+ channels = f[channels_path] if channels_path else f["channels"]
75
75
  ch_list = list(channels.keys())
76
76
  ch_list.sort()
77
77
  widths = []
@@ -109,18 +109,26 @@ class CreateFocalPlane(TimedClass, log.Logger):
109
109
  wl_max = self.mainConfig["wl_grid"]["wl_max"]
110
110
  logbin_resolution = self.mainConfig["wl_grid"]["logbin_resolution"]
111
111
  else:
112
- # Fallback: infer from channel config or use sensible defaults
112
+ # Fallback: infer the passband from the channel configuration, or
113
+ # fall back to sensible defaults.
113
114
  wl_min, wl_max, logbin_resolution = 1.0 * u.um, 2.0 * u.um, 100
114
- try:
115
- channels_cfg = self.mainConfig.get(
116
- "channels"
117
- ) or self.payloadConfig.get("channels")
118
- if isinstance(channels_cfg, dict) and len(channels_cfg) > 0:
119
- first_ch = next(iter(channels_cfg.values()))
120
- wl_min = first_ch.get("wl_min", wl_min)
121
- wl_max = first_ch.get("wl_max", wl_max)
122
- except Exception:
123
- pass
115
+ self.warning(
116
+ "no 'wl_grid' in the configuration; inferring the wavelength "
117
+ "grid from the channel passbands"
118
+ )
119
+ channel_cfg = self.payloadConfig.get("channel")
120
+ if isinstance(channel_cfg, OrderedDict):
121
+ channels = list(channel_cfg.values())
122
+ elif isinstance(channel_cfg, dict):
123
+ channels = [channel_cfg]
124
+ else:
125
+ channels = []
126
+ mins = [ch["wl_min"] for ch in channels if "wl_min" in ch]
127
+ maxs = [ch["wl_max"] for ch in channels if "wl_max" in ch]
128
+ if mins:
129
+ wl_min = min(mins)
130
+ if maxs:
131
+ wl_max = max(maxs)
124
132
  self.wl_grid = utils.grids.wl_grid(wl_min, wl_max, logbin_resolution)
125
133
  self.debug(
126
134
  f"Wavelength grid initialized: {len(self.wl_grid)} bins from "