Developing Moore

Whether you want to edit the C++ algorithms used in Moore or Moore’s Python configuration, it’s generally simplest to use the lb-stack-setup utility.

The README provided on the project homepage will guide you through steps that will give you full check-outs of Moore and all of it’s LHCb and Gaudi dependencies (such as the Phys and Rec projects). You are then free to edit any file in any project, re-build, and re-run Moore to see what changes!

When editing files that live in Moore, there are a few conventions to follow which are listed on this page, but before then it’s important to check the edits don’t break the tests!

Running tests

A good way to start developing is to run the tests, so-called ‘QM tests’, which are run automatically in the nightly build system. This has the big advantage that you start from an options file which is guaranteed to work and you can test yourself that you are not introducing any unwanted changes.

You can find out which tests are available by running:

make Moore/test ARGS='-N'

The specification of most tests is done in files with a .qmt extension, which are located in directories like Hlt/Hlt2Conf/tests/qmtest. The name of a test is given by the directory it is in and the filename. Inside the .qmt file you will find the options which are executed by the test and what the requirements for the test are to pass.

To run a test, the following options are available:

# Show all available tests.
make Moore/test ARGS='-N'
# Run all tests
make Moore/test
# Run all tests with 4 parallel jobs
make Moore/test ARGS='-j 4'
# Run test(s) matching a regular expression
make Moore/test ARGS='-R hlt1_example$'
# Verbose output showing test (failure) details
make Moore/test ARGS='-R hlt1_example -V'
# Run test(s) N to M
make Moore/test ARGS='-I N,M'
# Run failed tests again
make fast/Moore/test ARGS='--rerun-failed'
# Reconfigure Moore after adding additional tests
make Moore/configure

You can also use make fast/Moore/test if you have only changed code in the Moore project. This skips checking upstream projects for changes.

The log files of a test can be found in the directory Moore/build.<platform>/html/<name of test>.

The options of a test, without executing the validators, can also be run by hand with gaudirun.py:

./Moore/run gaudirun.py Moore/<Hat>/<Package>/tests/qmtest/<test name>.qmt

With the introduction of lbexec and qmtexec commands any test can be run using the qmtexec command:

./Moore/run qmtexec Moore/<Hat>/<Package>/tests/qmtest/<test name>.qmt

A more detailed description can be found on the Gaudi testing infrastructure TWiki page (which is outdated in some places).

Authoring tests

Often you will need to modify an existing QM test or add a new one. To add a new test, just create a new file in the tests/qmtest directory of the package that will host it. Make it look like the example shown below, re-configure with make Moore/configure to make your new test known to the build system, and run using the instructions above.

You can find a detailed specification of the available features on the Gaudi testing infrastructure TWiki page (which is outdated in some places), but you can also look at the Gaudi or LHCb testing infrastructure code.

Let’s dissect the test definition of the hlt1_reco_baseline.qmt test in Hlt/RecoConf as a typical example:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE extension  PUBLIC '-//QM/2.3/Extension//EN'  'http://www.codesourcery.com/qm/dtds/2.3/-//qm/2.3/extension//en.dtd'>
<!-- (c) Copyright ... -->
<!--
Make sure HLT1 baseline reconstruction does not change.
-->
<extension class="GaudiTest.GaudiExeTest" kind="test">
<argument name="program"><text>gaudirun.py</text></argument>
<argument name="args"><set>
  <text>$MOOREROOT/tests/options/mdf_input_and_conds.py</text>
  <text>$MOOREROOT/tests/options/download_mdf_input.py</text>
  <text>$RECOCONFROOT/options/hlt1_reco_baseline.py</text>
</set></argument>
<argument name="use_temp_dir"><enumeral>true</enumeral></argument>
<argument name="reference"><text>../refs/hlt1_reco_baseline.ref</text></argument>
<argument name="validator"><text>

from Moore.qmtest.exclusions import ref_preprocessor
validateWithReference(preproc=ref_preprocessor)

from Moore.qmtest.exclusions import remove_known_warnings
countErrorLines({"FATAL": 0, "ERROR": 0, "WARNING": 0},
                stdout=remove_known_warnings(stdout))

</text></argument>
</extension>

You can see some XML boilerplate at the top, the copyright and a useful description of the intent of the test. The program that is run is most often gaudirun.py or sometimes python (e.g. for GaudiPython scripts). The arguments follow after that, where each of them must be put in a separate <text> tag (spaces would be interpreted as part of the argument, the same as if you quote them in the shell). We set the use_temp_dir flag so that tests output in a temporary directory in the build directory (e.g. build.x86_64-centos7-gcc9-opt/Hlt/RecoConf/tests_tmp for this test).

The most important part of the test definition is the validator, which is the logic that defines the condition for success (and failure) of the test. In most tests you will write a validator explicitly, and for that you use Python code embedded in the XML as shown above. There are a couple of frequently used validation patterns.

First, every test should check that no warning, error or fatal messages appear in the standard output. This is done with the code:

from Moore.qmtest.exclusions import remove_known_warnings
countErrorLines({"FATAL": 0, "ERROR": 0, "WARNING": 0},
                stdout=remove_known_warnings(stdout))

You can notice that the standard output is preprocessed with the callable remove_known_warnings, defined in exclusions.py. The purpose of this central list of exclusions is to temporarily mask warnings that do not affect the correctness of running the application. It is important that a reasonable attempt is made to fix the source of the warning before extending the central list, and if the list is extended, it should point to an issue where the resolution of the underlying problem is tracked. Warnings that are expected only in a particular test, but would be problematic if any other test produces them, should not be added to exclusions.py. Error and fatal messages should never be ignored but they should be addressed at their source.

Second, many tests will validate correctness by comparing to a reference. This is done with:

from Moore.qmtest.exclusions import ref_preprocessor
validateWithReference(preproc=ref_preprocessor)

Here ref_preprocessor (from exclusions.py) is used to filter the “noise” from the standard output, which consists of parts that will vary between runs, such as the timing table, the host on which the test runs, etc. In Moore ref_preprocessor also removes everything before the finalization of the job, which is where the most interesting parts reside (counters, PrChecker tables, etc.) From what is left, counters, histogram and TTree summaries are extracted and compared to those in the reference (../refs/hlt1_reco_baseline.ref), and everything else is compared line by line.

You might need to debug the test interactively, especially if you define a more complex validator. Since the test runner (CTest) captures the standard output, if you try to use pdb or similar tools, the test will seemingly hang when you run it with make Moore/test ... waiting for input. The way to overcome this problem is to run the test manually, by extracting the relevant command using make Moore/test ARGS='-R ... -V -N':

make fast/Moore/test ARGS='-R hlt1_reco_baseline$ -V -N'
# ...
# 20: Test command: /cvmfs/lhcb.cern.ch/lib/var/lib/LbEnv/529/stable/x86_64-centos7/bin/xenv "--xml" "/home/rmatev/stack/Moore/build.x86_64-centos7-gcc9-opt/config/Moore-build.xenv" "python" "-m" "GaudiTesting.Run" "--skip-return-code" "77" "--report" "ctest" "--common-tmpdir" "/home/rmatev/stack/Moore/build.x86_64-centos7-gcc9-opt/Hlt/RecoConf/tests_tmp" "--workdir" "/home/rmatev/stack/Moore/Hlt/RecoConf/tests/qmtest" "/home/rmatev/stack/Moore/Hlt/RecoConf/tests/qmtest/hlt1_reco_baseline.qmt"
# ...
Moore/run "python" "-m" "GaudiTesting.Run" "--skip-return-code" "77" "--report" "ctest" "--common-tmpdir" "/home/rmatev/stack/Moore/build.x86_64-centos7-gcc9-opt/Hlt/RecoConf/tests_tmp" "--workdir" "/home/rmatev/stack/Moore/Hlt/RecoConf/tests/qmtest" "/home/rmatev/stack/Moore/Hlt/RecoConf/tests/qmtest/hlt1_reco_baseline.qmt"

Coding conventions

When writing Python in Moore, you should try to follow the PEP8 guidelines and other general good practices:

  • Include comments and documentation when the intent of a line, function, class, or module is not obvious.

  • Factor code into functions when there is repetition and/or for clarity.

  • When modifying an existing file, following the conventions of the surrounding code.

For examples of how to do things, just have a look around what already exists in Moore.

Docstrings

Moore follows Google’s docstring conventions. There is a nice example of usage in the Sphinx documentation 1 and lots within Moore itself.

Standard Python conventions, e.g. PEP8. Small, readable functions. Google-style docstrings.

Continuous integration

Whenever a new commit is made to the Moore repository, a CI pipeline runs that performs some style and syntax checks:

  • The LHCb copyright statement should be present at the top of every source file.

  • The LHCb Python formatting rules must be adhered to.

  • The Python code must have valid syntax and not raise any flake8 error codes.

Timing, throughput and profiling

An important question comes up sooner or later when developing an algorithm or making changes to an existing one: “How fast is this thing?”. In many scenarios, the timing table that is printed at the end of a job can give a sufficient estimate. The timing table prints the total and average time per event for every algorithm, and it is advisable to compare the timing changes of your algorithm relative to some algorithm earlier in the reconstruction chain, like the Velo tracking. If you need more details and for example want to create a flamegraph, we recommend following the instructions in the PRConfig repository. If you want to use valgrind tools (e.g. callgrind) to profile your code, see the docstring of the PyConf.application.make_callgrind_profile function and follow the instructions in the “Job Options Driven Profiling” section of the CodeAnalysisTools Twiki page.

Footnotes

1

Sphinx is the program used to generate the documentation web site you’re currrently reading!