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.
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
.
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:
location_from_astropy(observatory) -> EarthLocation
: this task takes one parameter, the name of the observatory, and returns an Astropy class calledEarthLocation
. That class contains the information of the observatory – such as longitude, latitude, height – retrieved from the Astropy database.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.location_from_fits(file_name) -> EarthLocation
: takes the file name returned byobservatory_file()
, opens the file and returns an AstropyEarthLocation
class containing the information of the observatory retrieved from the FITS file.tune_location(a, b) -> EarthLocation
: takes twoEarthLocation
objectsa
andb
and checks if they refer to the same location. If they do, it computes the average values of longitude, latitude, and height, and returns anEarthLocation
containing these average values.geodetic_info(EarthLocation) -> dict
: takes anEarthLocation
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.