Developers’ Guide#

CIL is an Object Orientated software. It has evolved during the years and it currently does not fully adhere to the following conventions. New additions must comply with the conventions and documentation guidelines described in this section.

Building CIL from source code#

Getting the code#

In case of local development and testing it is useful to be able to build the software directly. You should first clone this repository as:

git clone git@github.com:TomographicImaging/CIL

The parameter --depth 1 can be added to create a shallow clone with a history truncated to the specified number of commits reducing the size of the clone. See git documentation

Building with pip#

Install Dependencies#

We suggest creating a conda environment with all the dependencies for building CIL using the appropriate command for your operating system:

OS

Command

Status

Linux

conda env create -f ./scripts/cil_development.yml

Tested

Windows

conda env create -f ./scripts/cil_development.yml

Tested

MacOS (ARM)

conda env create -f ./scripts/cil_development_osx.yml

Experimental

Note

Currently only Linux and Windows are tested and supported. The support on MacOS (ARM) is experimental and certain features are not available and not working, such as FFT filtering for FDK.

This will create an environment called cil_dev.

Activate the environment with:

conda activate cil_dev

Build CIL#

A C++ compiler is required to build the source code. Let’s suppose that the user is in the source directory, then the following commands should work:

pip install -e .

Note

You need to have a working compiler on your system, such as Visual Studio on Windows, GCC on Linux and XCode on MacOS.

If not installing inside a conda environment, then the user might need to set the locations of optional libraries:

pip install -e . -Ccmake.define.IPP_ROOT="<path_to_ipp>" -Ccmake.define.OpenMP_ROOT="<path_to_openmp>" -Ccmake.define.CMAKE_BUILD_TYPE=RelWithDebInfo

Notes for Windows users#

One option for development on Windows is using Windows Subsystem for Linux (WSL) Launch WSL and install build-essential using:

apt install build-essential

This will enable you to then follow the linux instructions for creating the environment and building CIL.

To build for windows you can install Visual Studio Community (or higher) and select the Desktop development with C++ workload.

If you are developing on Windows with conda, you need to have access to both the Visual Studio compiler and have created the conda environment using the command for Windows above.

You can achieve this in two ways:

  1. by opening “x64 Native Tools Command Prompt for VS” and activating the conda environment from there. This requires you to know the path to the conda.bat file, which is typically located in the condabin subdirectory of your conda installation. Once located you need to run <path_to>\conda.bat activate <env_name> to activate the conda environment, and then you can run the build command from there.

  2. by opening the conda prompt and running the vcvarsall.bat x64 file from the Visual Studio installation (with x64 argument), and then running the build command. This requires you to know the path to the vcvarsall.bat file, which is typically located in the VC/Auxiliary/Build subdirectory of your Visual Studio installation.

Note: we tested these instructions with Visual Studio 2026 version 18.1.1

Building with Docker#

In the repository root, simply update submodules and run docker build:

git submodule update --init --recursive
docker build . -t ghcr.io/tomographicimaging/cil

Testing#

Once installed, CIL functionality can be tested using the following command:

export TESTS_FORCE_GPU=1  # optional, makes GPU test failures noisy
python -m unittest discover -v ./Wrappers/Python/test

Conventions on new CIL objects#

For each class there are essential, and non-essential parameters. The non-essential can be further be divided in often configured and advanced parameters:

  • essential

  • non-essential

    • often-configured

    • advanced

The definition of what are the essential, often-configured and advanced parameters depends on the actual class.

To create an instance of a class, the creator of a class should require the essential and often-configured parameters as named parameters.

It should not accept positional arguments *args or key-worded arguments **kwargs so that the user can clearly understand what parameters are necessary to create the instance.

Use of property is favoured instead of class members to store parameters so that the parameters can be protected.

The class should provide setter methods to change all the parameters at any time. Setter methods to set multiple parameters at the same time is also accepted. Setter methods should be named set_<parameter>. The use of set_ helps IDEs and user to find what they should change in an instance of a class.

Methods that are not meant to be used by the user should have a _ (underscore) at the beginning of the name. All methods should follow the convention of small caps underscore separated words.

Logging and warning#

We follow pythons convention on logging (see e.g. https://docs.python.org/3/howto/logging.html). In particular:

Logging and warning guidelines#

Task you want to perform

The best tool for the task

Display console output for ordinary usage of a command line script or program

print()

Report events that occur during normal operation of a program (e.g. for status monitoring or fault investigation)

A logger’s info() (or debug() method for very detailed output for diagnostic purposes)

Issue a warning regarding a particular runtime event

warnings.warn() if the issue is avoidable and the user’s code should be modified to eliminate the warning.

A logger’s warning() method if there is nothing the user can do about the situation, but the event should still be noted

Report an error regarding a particular runtime event

Raise an exception

Documentation#

The Core Imaging Library (CIL) follows the NumpyDoc style with the PyData Sphinx HTML theme. When contributing your code please refer to this link for docstring formatting and this rendered example.

Example from cil#

The following provides an example of the docstring format used within cil, and the rendered documentation generated from it.

Source#

FBP.run method from cil.io.recon.FBP#
    def run(self, out=None, verbose=1):
        """
        Runs the configured FBP recon and returns the reconstruction

        Parameters
        ----------
        out : ImageData, optional
           Fills the referenced ImageData with the reconstructed volume and suppresses the return

        verbose : int, default=1
           Controls the verbosity of the reconstructor. 0: No output is logged, 1: Full configuration is logged

        Returns
        -------
        ImageData
            The reconstructed volume. Suppressed if `out` is passed
        """
        if verbose:
            print(self)

        if self.slices_per_chunk:
            if AcquisitionType.DIM2 & self.acquisition_geometry.dimension:
                raise ValueError("Only 3D datasets can be processed in chunks with `set_split_processing`")
            elif self.acquisition_geometry.system_description != 'simple' and self.acquisition_geometry.system_description != 'offset':
                raise ValueError("Only simple and offset geometries can be processed in chunks with `set_split_processing`")
            elif self.acquisition_geometry.get_ImageGeometry() != self.image_geometry:
                raise ValueError("Only default image geometries can be processed in chunks `set_split_processing`")

            if out is None:
                ret = self.image_geometry.allocate()
            else:
                ret = out

            if self.filter_inplace:
                self._pre_filtering(self.input)

            tot_slices = self.acquisition_geometry.pixel_num_v
            remainder = tot_slices % self.slices_per_chunk
            num_chunks = int(np.ceil(self.image_geometry.shape[0] / self._slices_per_chunk))

            if verbose:
                pbar = tqdm(total=num_chunks)

            #process dataset by requested chunk size
            self._setup_PO_for_chunks(self.slices_per_chunk)
            for i in range(0, tot_slices-remainder, self.slices_per_chunk):

                if 'bottom' in self.acquisition_geometry.config.panel.origin:
                    start = i
                    end = i + self.slices_per_chunk
                else:
                    start = tot_slices -i - self.slices_per_chunk
                    end = tot_slices - i

                ret.array[start:end,:,:] = self._process_chunk(i, self.slices_per_chunk)

                if verbose:
                    pbar.update(1)

            #process excess rows
            if remainder:
                self._setup_PO_for_chunks(remainder)

                if 'bottom' in self.acquisition_geometry.config.panel.origin:
                    start = tot_slices-remainder
                    end = tot_slices
                else:
                    start = 0
                    end = remainder

                ret.array[start:end,:,:] = self._process_chunk(i, remainder)

                if verbose:
                    pbar.update(1)

            if verbose:
                pbar.close()

            if out is None:
                return ret

        else:

            if self.filter_inplace is False:
                proj_filtered = self.input.copy()
            else:
                proj_filtered = self.input

            self._pre_filtering(proj_filtered)

            operator = self._PO_class(self.image_geometry,self.acquisition_geometry)

            if out is None:
                return operator.adjoint(proj_filtered)
            else:
                operator.adjoint(proj_filtered, out = out)

Rendered#

FBP.run(out=None, verbose=1)[source]#

Runs the configured FBP recon and returns the reconstruction

Parameters:
  • out (ImageData, optional) – Fills the referenced ImageData with the reconstructed volume and suppresses the return

  • verbose (int, default=1) – Controls the verbosity of the reconstructor. 0: No output is logged, 1: Full configuration is logged

Returns:

The reconstructed volume. Suppressed if out is passed

Return type:

ImageData

Building documentation locally#

The easiest way to test documentation changes is to open a pull request and download the rendered documentation from the CI.

Alternatively, to build the docs locally, you will need a working cil installation. For development of the documentation embedded within the source code itself (e.g. docstrings), you should build cil from source.

The following steps can be used to create an environment that is suitable for building cil and its documentation, and to start a HTTP server to view the documentation.

  1. Clone cil repo

  2. Update conda with conda update -n base -c defaults conda

  3. Follow the instructions here to create a conda environment and build cil from source

  4. Go to docs folder

  5. Install packages from docs_environment.yml

  6. Install Ruby version 3.2

  7. Install the web dependencies with make web-deps

  8. Build the documentation with make dirhtml web

  9. Start an HTTP server with make serve to access the docs via localhost:8000.

Example:

git clone git@github.com:TomographicImaging/CIL
cd CIL
conda env create -f ./scripts/cil_development.yml
conda activate ENVIRONMENT_NAME
pip install -e .
cd docs
conda update -n base -c defaults conda
conda env update -f docs_environment.yml # with the name field set to ENVIRONMENT_NAME
make web-deps  # install dependencies (requires ruby 3.2)
make dirhtml web serve

Testing#

Methods are tested using Python’s unittest library. Please see the documentation here for information and usage examples. The unittest-parametrize library is used to generate parametrized test cases.

The @parametrize decorator allows you to define multiple sets of input parameters for a single test method, treating each combination as a separate test case. This helps to test various scenarios without duplicating code.

Example: Parameterized Test for ProjectionOperator

The parameters passed to the test are: The device (string), which is the device name passed to the ProjectionOperator, raise_error (bool), which specifies if an error is expected during initialisation, and the expected err_type.

In this example, the test instantiates a ProjectionOperator with a device name, checks if any errors should be raised, and ensures they are the expected type. There are 3 sets of parameters:

  • param('cpu', False, None, id="cpu_NoError") - Test using the device name ‘cpu’ (lowercase), and expects no error.

  • param('CPU', False, None, id="CPU_NoError") - Test using the device name ‘CPU’ (uppercase), and expects no error.

  • param('InvalidInput', True, ValueError, id="InvalidInput_ValueError") - Test using an invalid string, and expects the ValueError to be raised.

Each parameter set has a unique id which can also be customised for easier identification in test outputs (e.g., cpu_NoError, InvalidInput_ValueError)

When running the test, each parameterized case is shown as a distinct result:

If you open a pull request, continuous integration & deployment (CI/CD) via GitHub Actions (GHA) will run automatically. The CI will run the tests and build the documentation. Note that the test-cuda job uses unittest -k flags to detect GPU test names.

For more information please see the GHA README.

Contributions guidelines#

Make sure that each contributed file contains the following text enclosed in the appropriate comment syntax for the file format. Please replace [yyyy] and [name of copyright owner] with your own identifying information. Optionally you may add author name and email.

Copyright [yyyy] United Kingdom Research and Innovation
Copyright [yyyy] The University of Manchester
Copyright [yyyy] [name of copyright owner]
Author(s): [Author name, Author email (optional)]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.