Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pifinder_update.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#! /usr/bin/bash
git checkout release
git pull --no-recurse-submodules
git pull
source /home/pifinder/PiFinder/pifinder_post_update.sh

echo "PiFinder software update complete, please restart the Pi"
Expand Down
12 changes: 7 additions & 5 deletions python/PiFinder/sqm/sqm.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ def _measure_star_flux_with_local_background(

# Check for saturation in aperture
aperture_pixels = image_patch[aperture_mask]
max_aperture_pixel = np.max(aperture_pixels) if len(aperture_pixels) > 0 else 0
max_aperture_pixel = (
np.max(aperture_pixels) if len(aperture_pixels) > 0 else 0
)

if max_aperture_pixel >= saturation_threshold:
# Mark saturated star with flux=-1 to be excluded from mzero calculation
Expand Down Expand Up @@ -263,9 +265,7 @@ def _calculate_mzero(
# Flux-weighted mean: brighter stars contribute more
valid_mzeros_arr = np.array(valid_mzeros)
valid_fluxes_arr = np.array(valid_fluxes)
weighted_mzero = float(
np.average(valid_mzeros_arr, weights=valid_fluxes_arr)
)
weighted_mzero = float(np.average(valid_mzeros_arr, weights=valid_fluxes_arr))

return weighted_mzero, mzeros

Expand Down Expand Up @@ -540,7 +540,9 @@ def calculate(
# Following ASTAP: zenith is reference point where extinction = 0
# Only ADDITIONAL extinction below zenith is added: 0.28 * (airmass - 1)
# This allows comparing measurements at different altitudes
extinction_for_altitude = self._atmospheric_extinction(altitude_deg) # 0.28*(airmass-1)
extinction_for_altitude = self._atmospheric_extinction(
altitude_deg
) # 0.28*(airmass-1)

# Main SQM value: no extinction correction (raw measurement)
sqm_final = sqm_uncorrected
Expand Down
163 changes: 158 additions & 5 deletions python/PiFinder/ui/object_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

"""

from pydeepskylog.exceptions import InvalidParameterError

from PiFinder import cat_images
from PiFinder.ui.marking_menus import MarkingMenuOption, MarkingMenu
from PiFinder.obj_types import OBJ_TYPES
Expand All @@ -25,13 +27,15 @@
from PiFinder.db.observations_db import ObservationsDatabase
import numpy as np
import time
import pydeepskylog as pds


# Constants for display modes
DM_DESC = 0 # Display mode for description
DM_LOCATE = 1 # Display mode for LOCATE
DM_POSS = 2 # Display mode for POSS
DM_SDSS = 3 # Display mode for SDSS
DM_CONTRAST = 4 # Display mode for Contrast Reserve explanation


class UIObjectDetails(UIModule):
Expand All @@ -46,6 +50,7 @@ class UIObjectDetails(UIModule):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.contrast = None
self.screen_direction = self.config_object.get_option("screen_direction")
self.mount_type = self.config_object.get_option("mount_type")
self.object = self.item_definition["object"]
Expand All @@ -68,7 +73,7 @@ def __init__(self, *args, **kwargs):
),
)

# Used for displaying obsevation counts
# Used for displaying observation counts
self.observations_db = ObservationsDatabase()

self.simpleTextLayout = functools.partial(
Expand Down Expand Up @@ -117,8 +122,15 @@ def _layout_designator(self):
designator_color = 255
if not self.object.last_filtered_result:
designator_color = 128

# layout the name - contrast reserve line
space_calculator = SpaceCalculatorFixed(14)

_, typeconst = space_calculator.calculate_spaces(
self.object.display_name, self.contrast
)
return self.simpleTextLayout(
self.object.display_name,
typeconst,
font=self.fonts.large,
color=self.colors.get(designator_color),
)
Expand Down Expand Up @@ -197,6 +209,85 @@ def update_object_info(self):
scrollspeed=self._get_scrollspeed_config(),
)

# Get the SQM from the shared state
sqm = self.shared_state.get_sky_brightness()

# Check if a telescope and eyepiece are set
if (
self.config_object.equipment.active_eyepiece is None
or self.config_object.equipment.active_eyepiece is None
):
self.contrast = ""
else:
# Calculate contrast reserve. The object diameters are given in arc seconds.
magnification = self.config_object.equipment.calc_magnification(
self.config_object.equipment.active_telescope,
self.config_object.equipment.active_eyepiece,
)
if self.object.mag_str == "-":
self.contrast = ""
else:
try:
if self.object.size:
# Check if the size contains 'x'
if "x" in self.object.size:
diameter1, diameter2 = map(
float, self.object.size.split("x")
)
diameter1 = (
diameter1 * 60.0
) # Convert arc seconds to arc minutes
diameter2 = diameter2 * 60.0
elif "'" in self.object.size:
# Convert arc minutes to arc seconds
diameter1 = float(self.object.size.replace("'", "")) * 60.0
diameter2 = diameter1
else:
diameter1 = diameter2 = float(self.object.size) * 60.0
else:
diameter1 = diameter2 = None

self.contrast = pds.contrast_reserve(
sqm=sqm,
telescope_diameter=self.config_object.equipment.active_telescope.aperture_mm,
magnification=magnification,
surf_brightness=None,
magnitude=float(self.object.mag_str),
object_diameter1=diameter1,
object_diameter2=diameter2,
)
except InvalidParameterError as e:
print(f"Error calculating contrast reserve: {e}")
self.contrast = ""
if self.contrast is not None and self.contrast != "":
self.contrast = f"{self.contrast: .1f}"
else:
self.contrast = ""

# Add contrast reserve line to details with interpretation
if self.contrast:
contrast_val = float(self.contrast)
if contrast_val < -0.2:
contrast_str = "Object is not visible"
elif -0.2 <= contrast_val < 0.1:
contrast_str = "Questionable detection"
elif 0.1 <= contrast_val < 0.35:
contrast_str = "Difficult to see"
elif 0.35 <= contrast_val < 0.5:
contrast_str = "Quite difficult to see"
elif 0.5 <= contrast_val < 1.0:
contrast_str = "Easy to see"
elif contrast_val >= 1.0:
contrast_str = "Very easy to see"
else:
contrast_str = ""
self.texts["contrast_reserve"] = self.ScrollTextLayout(
contrast_str,
font=self.fonts.base,
color=self.colors.get(255),
scrollspeed=self._get_scrollspeed_config(),
)

# NGC description....
logs = self.observations_db.get_logs_for_object(self.object)
desc = ""
Expand Down Expand Up @@ -452,6 +543,62 @@ def update(self, force=True):
desc.set_available_lines(desc_available_lines)
desc.draw((0, posy))

elif self.object_display_mode == DM_CONTRAST:
# Display contrast reserve explanation page
y_pos = 20

# Title
self.draw.text(
(0, y_pos),
_("Contrast Reserve"),
font=self.fonts.base.font,
fill=self.colors.get(255),
)
y_pos += 14

# Display the contrast value
contrast = self.texts.get("contrast_reserve")

if self.contrast:
contrast_display = f"CR: {self.contrast}"
self.draw.text(
(0, y_pos),
contrast_display,
font=self.fonts.bold.font,
fill=self.colors.get(255),
)
y_pos += 17

# Display the interpretation
if contrast and contrast.text.strip():
contrast.draw((0, y_pos))
y_pos += 17
else:
self.draw.text(
(0, y_pos),
_("No contrast data"),
font=self.fonts.base.font,
fill=self.colors.get(128),
)
y_pos += 14

# Add explanation about what CR means
explanation_lines = [
_("CR measures object"),
_("visibility based on"),
_("sky brightness,"),
_("telescope, and EP."),
]

for line in explanation_lines:
self.draw.text(
(0, y_pos),
line,
font=self.fonts.base.font,
fill=self.colors.get(200),
)
y_pos += 11

return self.screen_update()

def cycle_display_mode(self):
Expand All @@ -460,9 +607,15 @@ def cycle_display_mode(self):
for a module. Invoked when the square
key is pressed
"""
self.object_display_mode = (
self.object_display_mode + 1 if self.object_display_mode < 2 else 0
)
# Cycle: LOCATE -> POSS -> DESC -> CONTRAST -> LOCATE
if self.object_display_mode == DM_LOCATE:
self.object_display_mode = DM_POSS
elif self.object_display_mode == DM_POSS:
self.object_display_mode = DM_DESC
elif self.object_display_mode == DM_DESC:
self.object_display_mode = DM_CONTRAST
else: # DM_CONTRAST or any other mode
self.object_display_mode = DM_LOCATE
self.update_object_info()
self.update()

Expand Down
4 changes: 3 additions & 1 deletion python/PiFinder/ui/sqm.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,9 @@ def _is_calibrated(self) -> bool:
camera_type = self.shared_state.camera_type()
camera_type_processed = f"{camera_type}_processed"
calibration_file = (
Path.home() / "PiFinder_data" / f"sqm_calibration_{camera_type_processed}.json"
Path.home()
/ "PiFinder_data"
/ f"sqm_calibration_{camera_type_processed}.json"
)
return calibration_file.exists()

Expand Down
4 changes: 3 additions & 1 deletion python/PiFinder/ui/sqm_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,9 @@ def _analyze_calibration(self):
# 2. Compute read noise using temporal variance (not spatial)
# Spatial std includes fixed pattern noise (PRNU), which is wrong.
# Temporal variance at each pixel measures true read noise.
temporal_variance = np.var(bias_stack, axis=0) # variance across frames per pixel
temporal_variance = np.var(
bias_stack, axis=0
) # variance across frames per pixel
self.read_noise = float(np.sqrt(np.mean(temporal_variance)))

# 3. Compute dark current rate
Expand Down
2 changes: 1 addition & 1 deletion python/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ numpy==1.26.4
numpy-quaternion==2023.0.4
pam==0.2.0
pandas==1.5.3
pydeepskylog==1.6
pillow==10.4.0
pydeepskylog==1.3.2
pyjwt==2.8.0
python-libinput==0.3.0a0
pytz==2022.7.1
Expand Down
4 changes: 3 additions & 1 deletion python/tests/test_sqm.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,9 @@ def test_calculate_extinction_applied(self):

# Check extinction values (ASTAP convention: 0 at zenith)
# Pickering airmass at 30° ≈ 1.995, so extinction ≈ 0.28 * 0.995 ≈ 0.279
assert details_zenith["extinction_for_altitude"] == pytest.approx(0.0, abs=0.001)
assert details_zenith["extinction_for_altitude"] == pytest.approx(
0.0, abs=0.001
)
expected_ext_30 = 0.28 * (sqm._pickering_airmass(30.0) - 1)
assert details_30deg["extinction_for_altitude"] == pytest.approx(
expected_ext_30, abs=0.001
Expand Down
Loading