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

Tickets/DM-40213 #137

Merged
merged 2 commits into from
Oct 24, 2023
Merged

Tickets/DM-40213 #137

merged 2 commits into from
Oct 24, 2023

Conversation

gmegh
Copy link
Contributor

@gmegh gmegh commented Sep 12, 2023

No description provided.

@gmegh gmegh force-pushed the tickets/DM-40213 branch 3 times, most recently from 63291fb to 9b17217 Compare September 13, 2023 18:23
@gmegh gmegh requested a review from tribeiro September 13, 2023 18:23
Copy link
Member

@tribeiro tribeiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is an initial round of comments.

doc/version_history.rst Outdated Show resolved Hide resolved
M2_B20 = 49


class ClosedLoop(salobj.BaseScript, metaclass=abc.ABCMeta):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this is an appropriate name for the script. I think usually we want to reflect an action performed by the control system. Maybe drop the "d" and call it CloseLoop? However, I think the intention here is to create a base script to perform close loop with both ComCam and LSSTCam. If this is the case, I'd call this BaseCloseLoop.

I see that you prepared the class to do it (by adding the metaclass), however there's nothing in the implementation on that regard. I think you want to split this one in 2 scripts; a base and a ComCam implementation, leaving the LSSTCam implementation for later. Or maybe even doing the LSSTCam already after we merge the PR in ts_observatory_control.

FAM = 1


class DOFComponent(enum.IntEnum):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to move this enum to ts_observatory_control as we spoke?

from lsst.ts import salobj
from lsst.ts.observatory.control import RemoteGroup

# TO-DO: Remove this import once the new LSSTCam is available
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are making a generic implementation here, then, you don't want to import these here now. See my comment about splitting this into 2 scripts.

descr=descr,
)

self.mtaos = RemoteGroup(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MTAOS is part of MTCS


# The following attributes are set via the configuration:
self.filter = None
self.grating = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Main Telescope instruments have no grating

description: De-focus to apply when acquiring the intra/extra focal images (mm).
type: number
default: 0.8
extra_focal_offset:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the MT has need of this

the change in magnification from moving the secondary mirror.
type: number
default: 0.0011
threshold:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead if having a parameter for threshold and one for selecting the DoF, you could merge these two and make the DoF be a "dictionary" where users can specify the threshold. something like this:

dof:
    type: object
    description: >-
        Defines the DoF to use and their associated properties.
        Set the threshold to zero to disable a dof.
    properties:
        M2_dz:
            type: object
            properties:
                threshold:
                    type: number
        M2_dx:
            type: object
            properties:
                threshold:
                    type: number
        M2_dy:
            type: object
            properties:
                threshold:
                    type: number
        M2_rx:
            type: object
            properties:
                threshold:
                    type: number
        M2_ry:
            type: object
            properties:
                threshold:
                    type: number
        .
        .
        .

Now, there's a question about how to handle defaults. Maybe we should talk about this in a video conf.

await self.checkpoint(f"[{i + 1}/{self.max_iter}]: Taking image...")

# Setting visit_id's to none so run_cwfs will take a new dataset.
if self.mode == ClosedLoopMode.CWFS:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having each option in a if/else statement like this, I'd suggest you extract each "mode" into a function, then make a dictionary with the mode as key and the function as value. Then here you just do:

    await self.operation_model_handlers[self.mode]()

where you would define self.operation_model_handlers in the `init method as something like:

self.operation_model_handlers = {
    ClosedLoopMode.CWFS: self.handle_cwfs_mode,
    ClosedLoopMode.FAM: self.handle_fam_mode,
}

Then self.handle_cwfs_mode and self.handle_fam_mode will have the appropriate code extracted from here.

self.mtaos.rem.mtaos.cmd_runWEP.set_start(
visitId=intra_visit_id, extraId=extra_visit_id
)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, extract the following code into methods that explain what each step does.

Copy link
Member

@tribeiro tribeiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here some additional comments.

@@ -6,7 +6,10 @@
Version History
===============

.. towncrier release notes start
v1.26.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are now using towncrier to manage the release notes. See: https://github.com/lsst-ts/ts_standardscripts/blob/develop/doc/news/README.rst

In short, you should not update this file anymore. Instead create a news fragment in the doc/news directory following the format suggested in the README above.

type: string
enum: ["cwfs", "fam"]
default: cwfs
rotation_angle:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just remove the rotator from the configuration. The software that needs to know about the rotator angle will get it from the correct source inside the MTAOS.

f"[{i + 1}/{self.max_iter}]: Closed Loop converged."
)

self.log.info("Closed Loop completed successfully!\n")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove new line at the end

python/lsst/ts/standardscripts/base_close_loop.py Outdated Show resolved Hide resolved

# If we haven't converged and apply_corrections is true,
# then we apply the corrections
elif self.apply_corrections:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you always want to apply the correction, even if you already converged. This way we don't trow away any iteration. I'd move this check to be before the convergence criteria check and make them independent if statements, e.g.;

if self.apply_corrections:
    ...

if all(abs(dof_offset) < self.threshold):
    ...

you don't need the else step.

Also, if we are not applying the correction, do we want to continue taking data? we might want to break or return in that case, e.g.;

if self.apply_corrections:
    ...
else:
    return

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we do want to keep taking data if we are not applying the correction. The reason being that in that way we can see how the closed loop corrections evolve without applying them. We might be repositioning the telescope or checking the LUT while doing this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that's a good point. In this case, please, add a note in the max_iter description explaining that, if apply_corrections is False it will take this many pairs of images.

self.log.debug("Camera already defined, skipping.")

async def assert_mode_compatibility(self) -> None:
"""Assert that the mode is compatible with ComCam."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, document that is raises an exception.


@property
@abc.abstractmethod
def camera(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add type annotation to these method? I notice below that you did ComCam | None and LSSTCam | None. Type-checking wise this will make our life harder. We currently don't have type checking on in this repo but looking forward I would like to add support for it.

I'd recommend you only return the camera type and raise an exception if the camera is None. Something like this:

@property
@abc.abstractmethod
def camera(self) -> BaseCamera:
    raise NotImplementedError()

Then for ComCam and LSSTCam you would do something like this:

@property
def camera(self) -> BaseCamera:
    if self._comcam is not None:
        return self._comcam
    else:
        raise RuntimeError("Camera not defined.")

You can either use BaseCamera or ComCam as the return type in this case.

self._comcam = None

@property
def camera(self) -> ComCam | None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment above about the type and how to handle this

self._lsstcam = None

@property
def camera(self) -> LSSTCam | None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment above about the type

import numpy as np
from lsst.ts import standardscripts
from lsst.ts.observatory.control.utils.enums import ClosedLoopMode
from lsst.ts.standardscripts import BaseCloseLoop
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseCloseLoop is an abstract class, I don't think you can unit test it. You will have to test the concrete implementations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, do I need to add two unit tests? lsstcam and comcam are basically the same, is it okay if I just have an lsstcam unit test?

Copy link
Member

@tribeiro tribeiro Oct 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only one is fine. However, you may want to check the executable for both.

@gmegh gmegh requested a review from tribeiro October 7, 2023 03:29
This script is used to perform measurements of the wavefront error, then
propose dof offsets based on ts_ofc. The offsets are not applied
automatically and must be turned on by the user through
apply_corrections attribute.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, add a note here that if apply_corrections is off, the script will take a series of intra/extra focal data instead, and the number of pairs is the number of maximum iterations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, make sure you reset all changes to this file.

Copy link
Member

@tribeiro tribeiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here some additional comments. I think once you address those we can probably close this one.

you may want to run

git checkout develop -- doc/version_history.rst

to make sure all your changes to this file are reset. Also, please, expand a bit the news fragment.

@@ -0,0 +1,3 @@
Add new base_close_loop.py script, and executable.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, explaining what this script does.

- type: array
items:
type: string
enum: [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could dynamically generate this by doing something like:

enum: {[dof_name.name for dof_name in DOFName]}

and covert this to an f-string, by adding a "f" at the top, e.g.:

schema_yaml = f"""
...          


assert all(self.state_0 == np.zeros(50))

async def test_executable(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, split this test in 2, e.g.;

    async def test_executable_close_loop_lsstcam(self):
        ...

    async def test_executable_close_loop_comcam(self):
        ...

@gmegh gmegh force-pushed the tickets/DM-40213 branch 2 times, most recently from bfe0495 to 04be8cb Compare October 9, 2023 00:07
Copy link
Member

@tribeiro tribeiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. Here some additional comments!

@@ -0,0 +1,4 @@
Add new base_close_loop.py script, and executable. This script allows to run the closed loop,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, make sure every phrase is in a single line, e.g. merge lines 1 and 2.

"""Handle creating MTCS object and waiting for remote to start."""
if self.mtcs is None:
self.log.debug("Creating MTCS.")
mtcs_usage = None if self.add_remotes else MTCSUsages.DryTest
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can remove the mtcs_usage part here.

of pairs is the number of maximum iterations.
"""

def __init__(self, index=1, add_remotes=True, descr="") -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can remove the add_remotes parameter.

maxItems: 50
default: {[0.004]*50}
max_iter:
description: Maximum number of iterations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think multiline description should be like:

description: >-
    Maximum number of iterations.
    Note, if apply_corrections is False, the script
    will take [max_iter] pairs of images.

See, for instance, how it is done in program below.

"""Handle creating Camera object and waiting for remote to start."""
if self.camera is None:
self.log.debug("Creating Camera.")
comcam_usage = None if self.add_remotes else ComCamUsages.StateTransition
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my previous message about removing add_remotes

"""Handle creating Camera object and waiting for remote to start."""
if self.camera is None:
self.log.debug("Creating Camera.")
camera_usage = None if self.add_remotes else LSSTCamUsages.StateTransition
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my previous comment about removing add_remotes

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this module can be inside maintel since it is maintel specific.

standardscripts.BaseScriptTestCase, unittest.IsolatedAsyncioTestCase
):
async def basic_make_script(self, index):
self.script = CloseLoopLSSTCam(index=index, add_remotes=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using add_remotes, set the value of the camera and TCS classes here with a DryTest usage, e.g.;

self.script = CloseLoopLSSTCam(index=index)
self.script.mtcs = MTCS(
                domain=self.script.domain,
                intended_usage=MTCSUsages.DryTest,
                log=self.script.log,
            )
self.script._lsstcam = LSSTCam(
                domain=self.script.domain,
                intended_usage=LSSTCam.DryTest,
                log=self.script.log,
            )

@tribeiro tribeiro mentioned this pull request Oct 17, 2023
@gmegh gmegh requested a review from tribeiro October 18, 2023 04:53
@gmegh gmegh force-pushed the tickets/DM-40213 branch 4 times, most recently from 002baff to f7f7209 Compare October 23, 2023 04:08
Copy link
Member

@tribeiro tribeiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright! this is looking pretty good. I left one inline comment in the news fragment.

One general comment, please, make sure all your command executions have a timeout, e.g. check that all start and set_start calls pass a timeout=<something>. Otherwise they may lock for ever if there is a communication issue. I think your STD_TIMEOUT might be short for some commands. I suggest you create a CMD_TIMEOUT=60 and use in all commands.. however the runWEP command might require something larger, like 120.

@@ -0,0 +1,3 @@
Add new base_close_loop.py script, and executable. This script allows to run the closed loop, that is, taking images, processing them, and apply ts_ofc corrections.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure each phrase takes one line.
e.g.;

Add new base_close_loop.py script, and executable.
This script allows to run the closed loop, that is, taking images, processing them, and apply ts_ofc corrections.

I think you may also want to add an empty line between each "feature", e.g.;

Add new base_close_loop.py script, and executable.
This script allows to run the closed loop, that is, taking images, processing them, and apply ts_ofc corrections.

Add new maintel/close_loop_comcam.py script, unit test, and executable.

Add new maintel/close_loop_lsstcam.py script, unit test, and executable.

Copy link
Member

@tribeiro tribeiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Thanks!

@gmegh gmegh merged commit 56636be into develop Oct 24, 2023
2 checks passed
@gmegh gmegh deleted the tickets/DM-40213 branch October 24, 2023 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants