Perform antenna characterization

GitHub repository -> here.

How to install Perform

Perform is supported for Python >= 3.10. As a first step, install Poetry, a Python package and dependency manager. The installation procedure is explained on the Poetry website. You also need the GitHub client, or alternatively Git. To install the former one, follow the instruction available here.

Now that Poetry and the GitHub client are installed, clone the Perform repository:

$ gh repo clone discos/perform

Install all requirements:

$ cd perform/
$ poetry shell
$ poetry install

The command poetry shell creates a .venv virtual environment inside the current directory. The command poetry install installs all dependencies into the virtual environment.

User Guide

Tuned Geodetic Information - example

Before explaining how to use this procedure, we take a few minutes to illustrate the rationale. Let’s start looking at the EarthLocation class of the Astropy library. It gives information about an observatory, as you can see here:

>>> from astropy.coordinates import EarthLocation
>>> srt = EarthLocation.of_site("SRT")
>>> srt.info.name
'Sardinia Radio Telescope'
>>> srt.height
<Quantity 671.6665 m>

The height of SRT, according to Astropy, is 671.6665 meters. That height is not the same as the one saved on the FITS files generated from DISCOS:

>>> from astropy.io import fits
>>> hdul = fits.open('tests/data/xyz.fits')
>>> hdul[0].header["ANTENNA"]
'SRT'
>>> hdul[0].header["SiteHeight"]
650.0

That is why we implemented the example procedure. In fact example computes and returns the average geodetic information of a given observatory. That information is computed taking in account the data from the Astropy library and from a FITS file produced at the observatory. To see how it works, execute the command perform:

$ perform
Procedure name:

The prompt is asking us to enter the name of the procedure. We type example:

$ perform
Procedure name: example
Tuned Geodetic Info of:

The prompt is now asking to enter the observatory name. We give the name SRT and finally get the average geodetic information of SRT:

$ perform
Procedure name: example
Tuned Geodetic Info of: SRT
   ...
Geodetic_info: {
    "observatory": "Sardinia Radio Telescope",
    "latitude": 39.493056194999994,
    "longitude": 9.245155620000004,
    "height": 660.8332500009775
}

Note

The user documentation of this procedure is just an example, use it as a reference template.

Development Guide

Workflow

By the word procedure we mean a software operation that measures an antenna characteristic, such as beam shape or gain. This section explains the guidelines to be followed in order to write a procedure.

Share the idea

When you want to implement a new procedure, as a first step you share your idea by writing the proposal as a GitHub issue, by opening a new issue on the GitHub repository. The proposal description should be as clear as possible, all contributors (astronomers, software developers, people in charge of the documentation) must be able to understand what the procedure is supposed to do and the motivation.

Once you wrote the GitHub issue, the contributors evaluate the proposal and start a discussion by commenting the proposal on the issue’s page. The proposal can be improved and finally approved, or even discarded. It is adviced to set a short deadline for the decision to be taken. Once the proposal is approved, the procedure has to be properly documented before the implementation starts.

Documentation

We open a GitHub issue related to the documentation that we are supposed to write, then we write the documentation describing the procedure from the user point of view. It means that we should indicate how the user runs the procedure, the meaning of the parameters, the description of the result, some examples of execution, and so on. This documentation has to be written in the User Guide of this manual (file user.rst), as you will see later.

Design

Once the user documentation of the procedure is completed, we analize the procedure in order to split it in small and independend N tasks. We hopefully have N contributors working in parallel, one contributor taking care of one single task. Each task must have its own GitHub issue that describes what the task is supposed to do. Eventually we have N+1 open issues, one for the procedure, and one for every task composing the procedure. Finally, for every task we decide how to automatically test it.

Create a branch for the procedure

Go to the GitHub issue page of the procedure and create a new branch for the issue (right side of the page, under the section Development). If the procedure is called foo, give the branch the name foo-procedure. Finally, select Checkout locally and create the branch. A pop-up windows appears showing you the commands to execute in order to fetch and checkout the branch locally.

Implementation

The contributor in charge of one task writes the implementation and the tests related to that task. You will see later, in section Practical example, how to properly implement a procedure.

Run tox

Before pushing the code to the procedure’s branch, check your code with tox. Details are explained in section Run tox to check the example procedure.

Push the code and open a pull request

Push the code to the procedure’s branch and open a pull request asking for the code to be merged to the main branch.

Practical example

Let’s suppose we want to implement a new procedure called Tuned Geodetic Information, identified by the short name example.

Share the idea

We open a GitHub issue where we describe the proposal. Please have a look.

Write the user documentation

We open a GitHub issue related to the documentation. We write the user documentation in the user.rst text file, that you find on the docs directory. The markup language used to format the text file is reStructuredText.

After writing the documentation in the user.rst file, you should generate the HTML and check if there are any errors. To generate the HTML move to the docs directory and run make html (as usual, the virtual environment has to be previously activated):

$ poetry shell  # The venv should be active
$ cd docs
$ make html
Running Sphinx v6.2.1
    ...
The HTML pages are in _build/html.

The last line of the output refers the location of the generated documentation. To see the result, open the index.html file with your browser. For instance:

$ firefox _build/html/index.html

Once the user documentation is completed, close the related GitHub issue.

Note

To have a look at the user documentation of this example, look at here. As you can see, before implementing the procedure we should already have a clear idea of the user interface.

Design

As described in the GitHub issue, the procedure takes the name of an observatory (for instance SRT), and returns the average geodetic information (observatory name, latitude, longitude, height) of that observatory. That average information is computed taking in account the values from Astropy and from a FITS file produced at that observatory.

The procedure can be splitted in 5 independent tasks:

  1. location_from_astropy(observatory) -> EarthLocation: this task takes one parameter, the name of the observatory, and returns an Astropy class called EarthLocation. That class contains the information of the observatory – such as longitude, latitude, height – retrieved from the Astropy database.

  2. observatory_file(observatory) -> file_name: takes the name of the observatory, looks for a FITS file produced at that observatory, and eventually returns the name of the first file that has been found.

  3. location_from_fits(file_name) -> EarthLocation: takes the file name returned by observatory_file(), opens the file and returns an Astropy EarthLocation class containing the information of the observatory retrieved from the FITS file.

  4. tune_location(a, b) -> EarthLocation: takes two EarthLocation objects a and b and checks if they refer to the same location. If they do, it computes the average values of longitude, latitude, and height, and returns an EarthLocation containing these average values.

  5. geodetic_info(EarthLocation) -> dict: takes an EarthLocation and reads from it the observatory name, latitude, longitude, and height. It returns a Python dictionary containing this information.

By splitting a procedure in independent tasks we can assign each task to a different collaborator. In this way the development of the tasks can progress in parallel. In addition, the tasks can be executed concurrently, speeding up the execution time of the proceedure.

Finally, we open 5 GitHub issues, one for each task.

Create a branch for the procedure

We go to the GitHub page of the procedure and we choose to open a new branch. The short name of the procedure is example, so we give the branch the name example-procedure. We select Checkout locally and finally create the branch. At this point we get the branch locally by executing the following commands (from the package directory):

$ git fetch origin
$ git checkout example-procedure

Implementation

To implement the procedures we use a workflow manager called Prefect. Basically, Prefect makes easy splitting the procedure in small tasks. It has several features, like concurrency, waiting for task X to terminate before executing task Y, scheduling, and so on.

The name given by Prefect to what we have been called procedure is flow. If you have a look at the file perform/example.py you see how the example procedure has been implemented. Basically, every task is decorated with the task decorator, the procedure is decorated with the flow decorator. To have an idea, here is the procedure:

 1@flow(description="Tuned geodetic information of {observatory}")
 2def tuned_geodetic_info(observatory: str) -> dict[str, Any]:
 3    logger = get_run_logger()
 4    logger.setLevel(logging.WARNING)
 5    loc_from_astropy = location_from_astropy.submit(observatory)
 6    file_name = observatory_file.submit(observatory)
 7    loc_from_file = location_from_fits.submit(file_name)
 8    tuned_location = tune_location(
 9        loc_from_astropy,
10        loc_from_file,
11    )
12    return geodetic_info(tuned_location)

As you can see, the procedure is not defined by the name example. Nevertheless, the user runs the procedure by giving the name example, as indicated in the user documentation. That’s because example is the name of the module. So, the name of the module gives the name to the procedure to be executed by command line.

The five tasks are disposed sequentially in the code but Perfect, under the hood, executes them concurrently. Actually, concurrency happens only when we submit the tasks to the task runner, as in the following case:

1    loc_from_astropy = location_from_astropy.submit(observatory)
2    file_name = observatory_file.submit(observatory)

In fact, when we use submit() the task runner creates a future and executes the tasks concurrently.

To understand what happens, we import time and add a time.sleep(5) both inside the task location_from_astropy() and observatory_file(). We also add the following two lines at the end of the file:

if __name__ == "__main__":
    tuned_geodetic_info("SRT")

Now we run example.py throught the Unix time command:

$ time python perform/example.py
    ...
real 0m7,615s
user 0m2,979s
sys  0m1,540s

The real execution time is less than ten seconds because the two tasks with time.sleep(5) have been executed concurrently. Now we remove the .submit():

loc_from_astropy = location_from_astropy(observatory)
file_name = observatory_file(observatory)

As you will see, the tasks are going to be executed sequentially:

$ time python perform/example.py
    ...
real 0m12,629s
user 0m2,980s
sys  0m1,494s

Note

The cli() function defined at the end of the module wraps the command line entry point, in order for the prompt to not stop the automatic execution of the tests. By the way, to see the tests of the example procedure have a look at tests/test_example.py. You can use these tests as a template for testing your procedure.

Run tox to check the example procedure

Before pushing the code to the procedure’s branch, run tox:

$ tox
    ...
py310: OK
py311: OK
tests: OK
docs: OK
congratulations :)

Tox is configured in order to run the tests with Python 3.10 and 3.11, and of course it expects that they all pass. It also checks the coverage, expecting 100%. It eventually builds the documentation, and verifies that all files – code, configuration files and documentation – are properly formatted.

Push the code and open a pull request

If tox is happy, you can commit your code and push to the repository. In the commit message insert the issue ID of your task. For instance, if the issue ID of your task is 77, write:

$ git commit -m "fix issue 77"

Now push the code to the repository:

$ git push origin example-procedure

We pushed to the example-procedure branch because example is the name of that procedure. If your procedure is called foo, push to the branch foo-procedure. Finally, open a pull request asking for the code to be merged to the main branch.