Skip to content

Commit

Permalink
lvmguider version 0.5
Browse files Browse the repository at this point in the history
* Update deps

* Migrate to using pyarrow dtypes

* Add RA, Dec, and FWHM to the datamodel for AG frames

* Populate ISFSWEEP header keyword in focus sweep

* Fix setting header with header_keywords

* Ingest each AG frame to DB after exposure

* Add raise_on_error parameter to dataframe_to_database

* Use weigth in the spline fit

* Add Focuser.require_best_to_be_in_range option

* Change default fit_method to spline in Focuser

* Improve focus based on temperature and relative adjustment

* Add fit_method to best_focus message

* Make sure actor config is a Configuration instance

* Add fwhm to ReferenceFocus

* Fix telemetry actor name

* Rounding and keywords

* Record if the exposure is a focus sweep in the frame data and DB

* Ensure ISFSWEEP is a boolean

* Improve log messages

* Tweak adjust_focus messages

* Fix focus adjustment when using a reference focus

* Tweak message

* Add focus-info command

* Add option to sleep between exposures

* Prevent NaNs in headers

* If guider fails, set status to IDLE | FAILED

* Update changelog
  • Loading branch information
albireox authored Feb 7, 2024
1 parent 5c9478e commit d743c0f
Show file tree
Hide file tree
Showing 14 changed files with 743 additions and 371 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## Next version

### 🚀 New

* Added `RA`, `DEC`, `FWHM`, `ISFSWEEP` to the `PROC` headers, `FrameData`, and `lvmops.agcam_frame`. All AG frames are now loaded to the database, not only those associated with an exposure. ([#16](https://github.com/sdss/lvmguider/pr/16))
* `Focuser` now uses a temperature model to estimate the initial best focus.
* Added `adjust-focus` and `focus-info` commands. If a focus sweep has been previously executed, `adjust-focus` can be used to adjust the focuser position based on the delta temperature between the bench temperature during the focus sweep and the current temperature.

### ✨ Improved

* Use inverse standard deviation as weights for the focus spline fit.
* `Focuser` now has an option to require the best resulting focus to be in the range of focuser positions tested or it will automatically repeat the focus sweep with a larger step size. ([#16](https://github.com/sdss/lvmguider/pr/16))


## 0.4.2 - January 31, 2024

### ✨ Improved
Expand Down
358 changes: 187 additions & 171 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ fast = true

[tool.ruff]
line-length = 88
target-version = 'py311'
target-version = 'py312'
select = ["E", "F", "I"]
unfixable = ["F841"]

Expand Down
7 changes: 6 additions & 1 deletion src/lvmguider/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# encoding: utf-8

import pathlib
import warnings

import pandas

from sdsstools import Configuration, get_logger, get_package_version

Expand All @@ -17,3 +18,7 @@
config = Configuration(pathlib.Path(__file__).parent / "etc/lvmguider.yml")

log = get_logger(NAME, use_rich_handler=True)


pandas.options.future.infer_string = True # type: ignore
pandas.options.mode.copy_on_write = "warn"
22 changes: 19 additions & 3 deletions src/lvmguider/actor/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import asyncio

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, NamedTuple

from clu.actor import AMQPActor

Expand All @@ -26,6 +26,21 @@
__all__ = ["LVMGuiderActor"]


class ReferenceFocus(NamedTuple):
"""A named tuple to store the reference focus."""

focus: float
fwhm: float
temperature: float
timestamp: float

def __str__(self):
return (
f"focus={self.focus:.2f}, fwhm={self.fwhm: .2f}, "
"temperature={self.temperature:.2f}"
)


class LVMGuiderActor(AMQPActor):
"""The ``lvmguider`` actor."""

Expand All @@ -42,13 +57,13 @@ def __init__(self, *args, **kwargs):

# Update package config.
config._BASE = dict(config)
config.update(aconfig)
config.load(aconfig)

name = aconfig["actor"]["name"]
self.telescope: str = aconfig.get("telescope", name.split(".")[1])

# Update the config that will be set in the actor instance.
kwargs["config"] = dict(config)
kwargs["config"] = config

super().__init__(*args, log=log, version=__version__, **kwargs)

Expand All @@ -58,6 +73,7 @@ def __init__(self, *args, **kwargs):
self.cameras = Cameras(self.telescope)

self._status = GuiderStatus.IDLE
self._reference_focus: ReferenceFocus | None = None

self.guider: Guider | None = None
self.guide_task: asyncio.Task | None = None
Expand Down
116 changes: 114 additions & 2 deletions src/lvmguider/actor/commands/focus.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@

from __future__ import annotations

from datetime import datetime, timezone
from time import time

from typing import TYPE_CHECKING

import click

from lvmguider.actor import lvmguider_parser
from lvmguider.actor.actor import ReferenceFocus
from lvmguider.focus import Focuser
from lvmguider.tools import wait_until_cameras_are_idle

Expand All @@ -36,13 +40,14 @@
"-g",
"--guess",
type=float,
help="Initial focus guess, in DT. If not provied, uses current focuser.",
help="Initial focus guess, in DT. If not provied, "
"uses a temperature-based estimate.",
)
@click.option(
"-s",
"--step-size",
type=float,
default=0.5,
default=0.2,
help="Step size, in DT.",
)
@click.option(
Expand Down Expand Up @@ -89,3 +94,110 @@ async def focus(
return command.fail(err)

return command.finish()


@lvmguider_parser.command("adjust-focus")
@click.argument("FOCUS_VALUE", type=float, required=False)
@click.option(
"--relative",
is_flag=True,
help="Adjusts the focus relative to the current value.",
)
@click.option(
"--reference",
is_flag=True,
help="Set as reference focus position.",
)
async def adjust_focus(
command: GuiderCommand,
focus_value: float | None = None,
relative: bool = False,
reference: bool = False,
):
"""Adjusts the focus to a specific value.
If FOCUS_VALUE is not provided, the focus will be adjusted to the
temperature-estimated best focus.
"""

focuser = Focuser(command.actor.telescope)

c_temp = await focuser.get_bench_temperature(command)
c_focus = await focuser.get_focus_position(command)

ref_focus = command.actor._reference_focus

if focus_value is None:
if ref_focus is None:
focus_value = await focuser.get_from_temperature(command, c_temp)
reference = True
if relative:
command.warning("No reference focus found. Using bench temperature.")
relative = False
else:
delta_t = c_temp - ref_focus.temperature
focus_model_a: float = command.actor.config["focus.model.a"]
focus_value = ref_focus.focus + delta_t * focus_model_a
relative = False # We always calculate an absolute focus here.

command.debug(
f"Reference temperature: {c_temp:.2f} C. "
f"Delta temperature: {delta_t:.2f} C."
)

delta_focus = round(focus_value - c_focus, 2)
if abs(delta_focus) > 0.01:
command.debug(f"Focus will be adjusted by {delta_focus:.2f} DT.")
else:
return command.finish(
f"Delta focus {delta_focus:.2f} DT is too small. "
"Focus was not adjusted."
)

if relative:
focus_value = c_focus + focus_value

await focuser.goto_focus_position(command, focus_value)
if reference:
command.actor._reference_focus = ReferenceFocus(
focus_value,
-999.0,
c_temp,
time(),
)

return command.finish(f"New focuser position: {focus_value:.2f} DT.")


@lvmguider_parser.command(name="focus-info")
async def focus_info(command: GuiderCommand):
"""Returns the current and reference focus position."""

focuser = Focuser(command.actor.telescope)

current_temperature = await focuser.get_bench_temperature(command)
current_focus = await focuser.get_focus_position(command)

ref = command.actor._reference_focus
timestamp = datetime.fromtimestamp(ref.timestamp).isoformat() if ref else None

command.info(
reference_focus={
"focus": ref.focus if ref else None,
"fwhm": ref.fwhm if ref else None,
"temperature": ref.temperature if ref else None,
"timestamp": timestamp,
}
)

command.info(
current_focus={
"focus": current_focus,
"temperature": current_temperature,
"delta_temperature": current_temperature - ref.temperature if ref else None,
"timestamp": datetime.now(timezone.utc).isoformat(),
}
)

return command.finish()
11 changes: 10 additions & 1 deletion src/lvmguider/actor/commands/guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ def is_stopping(command: GuiderCommand):
is_flag=True,
help="Do one single iteration and exit.",
)
@click.option(
"--sleep",
type=float,
help="Sleep this number of seconds before taking another exposure.",
)
async def guide(
command: GuiderCommand,
ra: float,
Expand All @@ -75,6 +80,7 @@ async def guide(
guide_tolerance: float | None = None,
apply_corrections: bool = True,
one: bool = False,
sleep: float | None = None,
):
"""Starts the guide loop."""

Expand Down Expand Up @@ -107,7 +113,7 @@ async def guide(
)
await actor.guide_task
except CriticalGuiderError as err:
command.actor.status |= GuiderStatus.FAILED
command.actor.status = GuiderStatus.IDLE | GuiderStatus.FAILED
return command.fail(f"Stopping the guide loop due to critical error: {err}")
except asyncio.CancelledError:
# This means that the stop command was issued. All good.
Expand All @@ -126,6 +132,9 @@ async def guide(
if one:
break

if sleep:
await asyncio.sleep(sleep)

actor.status = GuiderStatus.IDLE
command.actor.guider = None

Expand Down
30 changes: 27 additions & 3 deletions src/lvmguider/cameras.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@
import pathlib
import re

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

import numpy
import pandas
from astropy.io import fits

from sdsstools.time import get_sjd

from lvmguider import __version__
from lvmguider import __version__, config
from lvmguider.dataclasses import CameraSolution
from lvmguider.extraction import extract_sources as extract_sources_func
from lvmguider.maskbits import GuiderStatus
from lvmguider.tools import (
dataframe_to_database,
elapsed_time,
header_from_model,
run_in_executor,
Expand Down Expand Up @@ -63,6 +65,7 @@ async def expose(
flavour: str = "object",
extract_sources: bool = False,
nretries: int = 3,
header_keywords: dict[str, Any] = {},
) -> tuple[list[pathlib.Path], int, list[pandas.DataFrame] | None]:
"""Exposes the cameras and returns the filenames."""

Expand Down Expand Up @@ -148,6 +151,9 @@ async def expose(
header["GUIDERV"] = __version__
header["WCSMODE"] = "none"

for key, value in header_keywords.items():
header[key] = value

headers[fn] = header

sources: list[pandas.DataFrame] = []
Expand All @@ -170,6 +176,8 @@ async def expose(
all_sources = pandas.concat(sources)
valid = all_sources.loc[all_sources.valid == 1]
fwhm = numpy.percentile(valid["fwhm"], 25) if len(valid) > 0 else None
for fn in filenames:
headers[fn]["FWHM"] = fwhm
else:
valid = []
fwhm = None
Expand All @@ -180,7 +188,7 @@ async def expose(
"filenames": [str(fn) for fn in filenames],
"flavour": flavour,
"n_sources": len(valid),
"focus_position": round(focus_position, 1),
"focus_position": round(focus_position, 2),
"fwhm": numpy.round(float(fwhm), 3) if fwhm else -999.0,
}
)
Expand All @@ -191,6 +199,7 @@ async def expose(
if not command.actor.status & GuiderStatus.NON_IDLE:
command.actor.status |= GuiderStatus.IDLE

# Update FITS file PROC extension.
with elapsed_time(command, "updating lvm.agcam file"):
await asyncio.gather(
*[
Expand All @@ -199,6 +208,21 @@ async def expose(
]
)

# Store data to DB.
with elapsed_time(command, "storing lvm.agcam to DB"):
camera_frame_dfs = []
for fn in filenames:
cs = CameraSolution.open(fn)
camera_frame_dfs.append(cs.to_framedata().to_dataframe())

await run_in_executor(
dataframe_to_database,
pandas.concat(camera_frame_dfs, axis=0, ignore_index=True),
config["database"]["agcam_frame_table"],
delete_columns=["frameno", "telescope", "camera"],
raise_on_error=False,
)

return (list(filenames), next_seqno, list(sources) if extract_sources else None)

def reset_seqno(self):
Expand Down
Loading

0 comments on commit d743c0f

Please sign in to comment.