Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow scheduling Hartmanns during auto-pilot #22

Merged
merged 6 commits into from
May 30, 2024
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Next version

### ✨ Improved

* [#3](https://github.com/sdss/HAL/issues/3) Added `--add-hartmann` and `--remove-hartmann` flags to `auto-pilot`.


## 1.3.0 - May 29th, 2024

### 🚀 New
Expand Down
25 changes: 25 additions & 0 deletions src/hal/actor/commands/auto_pilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import click

from hal.macros.auto_pilot import AutoPilotMacro
from hal.macros.expose import ExposeMacro

from . import hal_command_parser
Expand Down Expand Up @@ -65,6 +66,18 @@
default=None,
help="Preload the next design this many seconds before the exposure completes.",
)
@click.option(
"--add-hartmann",
is_flag=True,
default=False,
help="Take a Hartmann during the next goto-field (will not repeat Hartmanns).",
)
@click.option(
"--remove-hartmann",
is_flag=True,
default=False,
help="Removes a previously scheduled Hartmann.",
)
async def auto_pilot(
command: HALCommandType,
stop: bool = False,
Expand All @@ -74,6 +87,8 @@ async def auto_pilot(
resume: bool = False,
count: int = 1,
preload_ahead: float | None = None,
add_hartmann: bool = False,
remove_hartmann: bool = False,
):
"""Starts the auto-pilot mode."""

Expand All @@ -83,12 +98,20 @@ async def auto_pilot(
assert isinstance(expose_macro, ExposeMacro)

macro = command.actor.helpers.macros["auto_pilot"]
assert isinstance(macro, AutoPilotMacro)

if (stop or modify or pause or resume) and not macro.running:
return command.fail(
"I'm afraid I cannot do that Dave. The auto pilot mode is not running."
)

if macro.running and (add_hartmann or remove_hartmann):
macro.hartmann = False if remove_hartmann else True
if macro.hartmann:
return command.finish("Scheduled a Hartmann for the next goto-field.")
else:
return command.finish("Removed any previously scheduled Hartmanns.")

if pause and resume:
return command.fail("--pause and --resume are incompatible Dave.")

Expand Down Expand Up @@ -131,6 +154,8 @@ async def auto_pilot(
)

macro.reset(command, count=count, preload_ahead_time=preload_ahead)
if add_hartmann:
macro.hartmann = True

result: bool = True
while True:
Expand Down
1 change: 1 addition & 0 deletions src/hal/etc/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
},
"expose_is_paused": { "type": "boolean" },
"auto_pilot_message": { "type": "string" },
"auto_pilot_hartmann": { "type": "boolean" },
"additionalProperties": false
}
}
2 changes: 1 addition & 1 deletion src/hal/helpers/jaeger.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def get_goto_field_stages(self):
else:
stages = goto_auto_mode_stages["new_field_stages"][observatory]

return stages
return list(stages)


class JaegerHelper(HALHelper):
Expand Down
27 changes: 26 additions & 1 deletion src/hal/macros/auto_pilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,24 @@ def __init__(self):

self.system_state = SystemState(self)

self._hartmann: bool = False
self._preload_task: asyncio.Task | None = None

@property
def hartmann(self):
"""Returns whether a Hartmann is scheduled."""

return self._hartmann

@hartmann.setter
def hartmann(self, value: bool):
"""Sets whether a Hartmann is to be scheduled."""

self._hartmann = value

if self.command:
self.command.debug(auto_pilot_hartmann=value)

async def prepare(self):
"""Prepares the auto pilot."""

Expand Down Expand Up @@ -184,9 +200,18 @@ async def goto_field(self):
self._auto_pilot_message("Skipping goto-field")
return

if self.hartmann and "hartmann" not in stages:
stages.append("hartmann")

self._auto_pilot_message("Running goto-field")
self.helpers.macros["goto_field"].reset(self.command, stages)
if not await self.helpers.macros["goto_field"].run():

result = await self.helpers.macros["goto_field"].run()

if self.hartmann and "hartmann" in stages:
self.hartmann = False

if not result:
raise MacroError("Goto-field failed during auto pilot mode.")

async def expose(self):
Expand Down
180 changes: 180 additions & 0 deletions tests/actor/test_command_auto_pilot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# @Author: José Sánchez-Gallego ([email protected])
# @Date: 2024-05-30
# @Filename: test_command_auto_pilot.py
# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause)

from __future__ import annotations

import asyncio

from typing import TYPE_CHECKING

import pytest

from hal.helpers.jaeger import Configuration
from hal.macros.auto_pilot import AutoPilotMacro


if TYPE_CHECKING:
from pytest_mock import MockerFixture

from hal.actor import HALActor


def _auto_pilot_run_once(macro: AutoPilotMacro):
async def run_once():
result = await AutoPilotMacro.run(macro)
macro.cancelled = True
return result

return run_once


@pytest.fixture()
def mock_auto_pilot(actor: HALActor, mocker: MockerFixture):
macro = actor.helpers.macros["auto_pilot"]
assert isinstance(macro, AutoPilotMacro)

mocker.patch.object(actor.helpers.apogee, "get_exposure_state", return_value="idle")
mocker.patch.object(actor.helpers.boss, "get_exposure_state", return_value="idle")
mocker.patch.object(macro.system_state, "exposure_time_remaining", 0.0)

mocker.patch.object(
actor.helpers.jaeger,
"configuration",
Configuration(
actor=actor,
design_id=1,
configuration_id=1,
loaded=True,
),
)

goto_field_macro = actor.helpers.macros["goto_field"]
mocker.patch.object(goto_field_macro, "run")

mocker.patch.object(actor.helpers.cherno, "is_guiding", return_value=True)
mocker.patch.object(actor.helpers.cherno, "guiding_at_rms", return_value=True)

expose_macro = actor.helpers.macros["expose"]
mocker.patch.object(expose_macro, "run")

mocker.patch.object(macro, "run", side_effect=_auto_pilot_run_once(macro))

yield

macro._preload_task = None


async def test_command_auto_pilot(actor: HALActor, mock_auto_pilot):
cmd = await actor.invoke_mock_command("auto-pilot")
assert cmd.status.did_succeed


async def test_command_auto_pilot_add_hartmann(
actor: HALActor,
mock_auto_pilot,
mocker: MockerFixture,
):
mock_goto_reset = mocker.patch.object(actor.helpers.macros["goto_field"], "reset")

cmd = await actor.invoke_mock_command("auto-pilot --add-hartmann")
assert cmd.status.did_succeed

auto_pilot = actor.helpers.macros["auto_pilot"]
assert isinstance(auto_pilot, AutoPilotMacro)

assert not auto_pilot.hartmann
mock_goto_reset.assert_called_with(
cmd,
[
"slew",
"reconfigure",
"fvc",
"reslew",
"lamps",
"boss_arcs",
"acquire",
"guide",
"hartmann",
],
)

auto_pilot_hartmann = []
for reply in cmd.replies:
if "auto_pilot_hartmann" in reply.message:
auto_pilot_hartmann.append(reply.message["auto_pilot_hartmann"])
assert auto_pilot_hartmann == [True, False]


async def test_command_auto_pilot_add_hartmann_while_running(
actor: HALActor,
mock_auto_pilot,
mocker: MockerFixture,
):
mock_goto_reset = mocker.patch.object(actor.helpers.macros["goto_field"], "reset")

cmd = actor.invoke_mock_command("auto-pilot")

await asyncio.sleep(0.01)
await actor.invoke_mock_command("auto-pilot --add-hartmann")

await cmd
assert cmd.status.did_succeed

auto_pilot = actor.helpers.macros["auto_pilot"]
assert isinstance(auto_pilot, AutoPilotMacro)

assert not auto_pilot.hartmann
mock_goto_reset.assert_called_with(
mocker.ANY,
[
"slew",
"reconfigure",
"fvc",
"reslew",
"lamps",
"boss_arcs",
"acquire",
"guide",
"hartmann",
],
)


async def test_command_auto_pilot_add_then_removehartmann_while_running(
actor: HALActor,
mock_auto_pilot,
mocker: MockerFixture,
):
mock_goto_reset = mocker.patch.object(actor.helpers.macros["goto_field"], "reset")

cmd = actor.invoke_mock_command("auto-pilot")

await asyncio.sleep(0.5)
await actor.invoke_mock_command("auto-pilot --add-hartmann")
await actor.invoke_mock_command("auto-pilot --remove-hartmann")

await cmd
assert cmd.status.did_succeed

auto_pilot = actor.helpers.macros["auto_pilot"]
assert isinstance(auto_pilot, AutoPilotMacro)

assert not auto_pilot.hartmann
mock_goto_reset.assert_called_with(
mocker.ANY,
[
"slew",
"reconfigure",
"fvc",
"reslew",
"lamps",
"boss_arcs",
"acquire",
"guide",
],
)