pept API documentation
Description: ![version](https://img.shields.io/badge/version-0.1.4-blue) [![Open In …

# Module `pept`

# PEPT

A Python library that integrates all the tools necessary to perform research using Positron Emission Particle Tracking (PEPT). The library includes algorithms for the location, identification and tracking of particles, in addition to tools for visualisation and analysis, and utilities allowing the realistic simulation of PEPT data.

## Positron Emission Particle Tracking

PEPT is a technique developed at the University of Birmingham which allows the non-invasive, three-dimensional tracking of one or more 'tracer' particles through particulate, fluid or multiphase systems. The technique allows particle or fluid motion to be tracked with sub-millimetre accuracy and sub-millisecond temporal resolution and, due to its use of highly-penetrating 511keV gamma rays, can be used to probe the internal dynamics of even large, dense, optically opaque systems - making it ideal for industrial as well as scientific applications.

## Getting Started

These instructions will help you get started with PEPT data analysis.

### Prerequisites

This package supports Python 3. You also need to have `NumPy` and `Cython` on your system in order to install it.

### Installation

You can install `pept` from PyPI:

    pip install pept

Or you can install the latest version from the GitHub repository:

    pip install git+<https://github.com/uob-positron-imaging-centre/pept>

### Example usage

You can download data samples from the UoB Positron Imaging Centre's Repository:

    $> git clone <https://github.com/uob-positron-imaging-centre/example_data>

A minimal analysis script using the `pept.tracking.peptml` subpackage:

    import pept
    from pept.scanners import ParallelScreens
    from pept.tracking import peptml
    from pept.visualisation import PlotlyGrapher

    lors = ParallelScreens('example_data/sample_2p_42rpm.csv', skiprows = 16)

    max_distance = 0.1
    cutpoints = peptml.Cutpoints(lors, max_distance)

    clusterer = peptml.HDBSCANClusterer(min_sample_size = 30)
    centres, clustered_cutpoints = clusterer.fit_cutpoints(cutpoints)

    fig = PlotlyGrapher().create_figure()
    fig.add_trace(centres.points_trace())
    fig.show()

A more in-depth tutorial is available on Google Colab.

Full documentation is available here.

## Performance

Significant effort has been put into making the algorithms in this package as fast as possible. The most compute-intensive parts have been implemented in `C` and parallelised, where possible, using `joblib`. For example, using the `peptml` subpackage, analysing 1,000,000 LoRs on the author's machine (mid 2012 MacBook Pro) takes ~26 s (with another 12 s to read in the data). This efficiency is largely due to the availabiliy of a great high-performance implementation of the HDBSCAN clustering algorithm.

## Help and Support

We recommend you check out our tutorials. If your issue is not suitably resolved there, please check the issues page on our GitHub. Finally, if no solution is available there, feel free to open an issue; the authors will attempt to respond in a reasonably timely fashion.

## Contributing

We welcome contributions in any form! Assistance with documentation, particularly expanding tutorials, is always welcome. To contribute please fork the project, make your changes and submit a pull request. We will do our best to work through any issues with you and get your code merged into the main branch.

## Citing

If you used this codebase or any software making use of it in a scientific publication, you must cite the following paper:

> Nicuşan AL, Windows-Yule CR. Positron emission particle tracking using machine learning. Review of Scientific Instruments. 2020 Jan 1;91(1):013329.

> https://doi.org/10.1063/1.5129251

## Licensing

The `pept` package is GNU v3.0 licensed. Copyright (C) 2020 Andrei Leonard Nicusan.

Source code

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-

    # File   : __init__.py
    # License: License: GNU v3.0
    # Author : Andrei Leonard Nicusan <a.l.nicusan@bham.ac.uk>
    # Date   : 19.08.2019

    '''

    ![version](https://img.shields.io/badge/version-0.1.4-blue)
    [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1G8XHP9zWMMDVu23PXzANLCOKNP_RjBEO)
    [![](https://img.shields.io/badge/-docs-success)](https://uob-positron-imaging-centre.github.io)

    # PEPT

    A Python library that integrates all the tools necessary to
    perform research using Positron Emission Particle Tracking (PEPT). The library
    includes algorithms for the location, identification and tracking of particles,
    in addition to tools for visualisation and analysis, and utilities allowing the
    realistic simulation of PEPT data.

    ## Positron Emission Particle Tracking
    PEPT is a technique developed at the University of Birmingham which allows the
    non-invasive, three-dimensional tracking of one or more 'tracer' particles through
    particulate, fluid or multiphase systems. The technique allows particle or fluid
    motion to be tracked with sub-millimetre accuracy and sub-millisecond temporal
    resolution and, due to its use of highly-penetrating 511keV gamma rays, can be
    used to probe the internal dynamics of even large, dense, optically opaque
    systems - making it ideal for industrial as well as scientific applications.

    ## Getting Started

    These instructions will help you get started with PEPT data analysis.

    ### Prerequisites

    This package supports Python 3. You also need to have `NumPy` and `Cython`
    on your system in order to install it.

    ### Installation

    You can install `pept` from PyPI:

    ```
    pip install pept
    ```

    Or you can install the latest version from the GitHub repository:

    ```
    pip install git+https://github.com/uob-positron-imaging-centre/pept
    ```

    ### Example usage

    You can download data samples from the [UoB Positron Imaging Centre's
    Repository](https://github.com/uob-positron-imaging-centre/example_data):

    ```
    $> git clone https://github.com/uob-positron-imaging-centre/example_data
    ```

    A minimal analysis script using the `pept.tracking.peptml` subpackage:

    ```
    import pept
    from pept.scanners import ParallelScreens
    from pept.tracking import peptml
    from pept.visualisation import PlotlyGrapher

    lors = ParallelScreens('example_data/sample_2p_42rpm.csv', skiprows = 16)

    max_distance = 0.1
    cutpoints = peptml.Cutpoints(lors, max_distance)

    clusterer = peptml.HDBSCANClusterer(min_sample_size = 30)
    centres, clustered_cutpoints = clusterer.fit_cutpoints(cutpoints)

    fig = PlotlyGrapher().create_figure()
    fig.add_trace(centres.points_trace())
    fig.show()
    ```

    A more in-depth tutorial is available on [Google
    Colab](https://colab.research.google.com/drive/1G8XHP9zWMMDVu23PXzANLCOKNP_RjBEO).

    Full documentation is available [here](https://uob-positron-imaging-centre.github.io).

    ## Performance

    Significant effort has been put into making the algorithms in this package as
    fast as possible. The most compute-intensive parts have been implemented in
    `C` and parallelised, where possible, using `joblib`. For example, using the `peptml`
    subpackage, analysing 1,000,000 LoRs on the author's machine (mid 2012 MacBook Pro)
    takes ~26 s (with another 12 s to read in the data). This efficiency is largely
    due to the availabiliy of a great high-performance [implementation of the
    HDBSCAN](https://github.com/scikit-learn-contrib/hdbscan) clustering algorithm.

    ## Help and Support

    We recommend you check out [our tutorials](https://colab.research.google.com/drive/1G8XHP9zWMMDVu23PXzANLCOKNP_RjBEO). If your issue is not suitably resolved there, please
    check the [issues](https://github.com/uob-positron-imaging-centre/pept/issues)
    page on our GitHub. Finally, if no solution is available there, feel free to
    [open an
    issue](https://github.com/uob-positron-imaging-centre/pept/issues/new); the
    authors will attempt to respond in a reasonably timely fashion.

    ## Contributing

    We welcome contributions in any form! Assistance with documentation, particularly
    expanding tutorials, is always welcome. To contribute please fork the project, make
    your changes and submit a pull request. We will do our best to work through any
    issues with you and get your code merged into the main branch.

    ## Citing

    If you used this codebase or any software making use of it in a scientific
    publication, you must cite the following paper:

    > Nicuşan AL, Windows-Yule CR. Positron emission particle tracking using machine learning. Review of Scientific Instruments. 2020 Jan 1;91(1):013329.

    > https://doi.org/10.1063/1.5129251

    ## Licensing

    The `pept` package is GNU v3.0 licensed.
    Copyright (C) 2020 Andrei Leonard Nicusan.

    '''

    # Import base data structures
    from    .base.line_data     import  LineData
    from    .base.point_data    import  PointData
    from    .base.voxel_data    import  VoxelData

    # Import subpackages
    from    .                   import  scanners
    from    .                   import  simulation
    from    .                   import  diagnostics
    from    .                   import  tracking
    from    .                   import  visualisation
    from    .                   import  utilities

    # Import package version
    from    .__version__        import  __version__

    __all__ = [
        'LineData',
        'PointData',
        'VoxelData',
        'scanners',
        'simulation',
        'diagnostics',
        'tracking',
        'visualisation',
        'utilities'
    ]

    __author__ =        "Andrei Leonard Nicusan"
    __credits__ =       ["Andrei Leonard Nicusan", "Kit Windows-Yule", "Sam Manger"]
    __license__ =       "GNU v3.0"
    __maintainer__ =    "Andrei Leonard Nicusan"
    __email__ =         "a.l.nicusan@bham.ac.uk"
    __status__ =        "Development"

## Sub-modules

`pept.base`
`pept.diagnostics`
`pept.scanners`
`pept.simulation`
`pept.tests`
`pept.tracking`
`pept.utilities`
`pept.visualisation`

## Classes

`class LineData (line_data, sample_size=200, overlap=0, verbose=False)`

  A class for PEPT LoR data iteration, manipulation and visualisation.

  Generally, PEPT LoRs are lines in 3D space, each defined by two points, irrespective of the geometry of the scanner used. This class is used for LoRs (or any lines!) encapsulation. It can yield samples of the `line_data` of an adaptive `sample_size` and `overlap`, without requiring additional storage.

  ## Parameters

  `line_data` : (`N`, `7`) `numpy.ndarray`
    An (N, 7) numpy array that stores the PEPT LoRs (or any generic set of lines) as time and cartesian (3D) coordinates of two points defining each line, in mm. A row is then [time, x1, y1, z1, x2, y2, z2].
  `sample_size` :`int`, optional
    An `int`` that defines the number of lines that should be returned when iterating over`line_data`. A`sample_size` of 0 yields all the data as one single sample. (Default is 200)
  `overlap` :`int`, optional
    An `int` that defines the overlap between two consecutive samples that are returned when iterating over `line_data`. An overlap of 0 means consecutive samples, while an overlap of (`sample_size` - 1) means incrementing the samples by one. A negative overlap means skipping values between samples. An error is raised if `overlap` is larger than or equal to `sample_size`. (Default is 0)
  `verbose` :`bool`, optional
    An option that enables printing the time taken for the initialisation of an instance of the class. Useful when reading large files (10gb files for PEPT data is not unheard of). (Default is True)

  ## Attributes

  `line_data` : (`N`, `7`) `numpy.ndarray`
    An (N, 7) numpy array that stores the PEPT LoRs as time and cartesian (3D) coordinates of two points defining a line, in mm. Each row is then `[time, x1, y1, z1, x2, y2, z2]`.
  `sample_size` :`int`
    An `int` that defines the number of lines that should be returned when iterating over `line_data`. (Default is 200)
  `overlap` :`int`
    An `int` that defines the overlap between two consecutive samples that are returned when iterating over `line_data`. An overlap of 0 means consecutive samples, while an overlap of (`sample_size` - 1) means incrementing the samples by one. A negative overlap means skipping values between samples. It is required to be smaller than `sample_size`. (Default is 0)
  `number_of_lines` :`int`
    An `int` that corresponds to len(`line_data`), or the number of LoRs stored by `line_data`.
  `number_of_samples` :`int`
    An `int` that corresponds to the number of samples that can be accessed from the class. It takes `overlap` into consideration.

  ## Raises

  `ValueError`
    If `overlap` >= `sample_size` unless `sample_size` is 0. Overlap has to be smaller than `sample_size`. Note that it can also be negative.
  `ValueError`
    If `line_data` does not have (N, 7) shape.

  ## Notes

  The class saves `line_data` as a contiguous numpy array for efficient access in C functions. It should not be changed after instantiating the class.

  Source code

      class LineData:
          '''A class for PEPT LoR data iteration, manipulation and visualisation.

          Generally, PEPT LoRs are lines in 3D space, each defined by two points,
          irrespective of the geometry of the scanner used. This class is used
          for LoRs (or any lines!) encapsulation. It can yield samples of the
          `line_data` of an adaptive `sample_size` and `overlap`, without requiring
          additional storage.

          Parameters
          ----------
          line_data : (N, 7) numpy.ndarray
              An (N, 7) numpy array that stores the PEPT LoRs (or any generic set of
              lines) as time and cartesian (3D) coordinates of two points defining each
              line, **in mm**. A row is then [time, x1, y1, z1, x2, y2, z2].
          sample_size : int, optional
              An `int`` that defines the number of lines that should be
              returned when iterating over `line_data`. A `sample_size` of 0
              yields all the data as one single sample. (Default is 200)
          overlap : int, optional
              An `int` that defines the overlap between two consecutive
              samples that are returned when iterating over `line_data`.
              An overlap of 0 means consecutive samples, while an overlap
              of (`sample_size` - 1) means incrementing the samples by one.
              A negative overlap means skipping values between samples. An
              error is raised if `overlap` is larger than or equal to
              `sample_size`. (Default is 0)
          verbose : bool, optional
              An option that enables printing the time taken for the
              initialisation of an instance of the class. Useful when
              reading large files (10gb files for PEPT data is not unheard
              of). (Default is True)

          Attributes
          ----------
          line_data : (N, 7) numpy.ndarray
              An (N, 7) numpy array that stores the PEPT LoRs as time and
              cartesian (3D) coordinates of two points defining a line, **in mm**.
              Each row is then `[time, x1, y1, z1, x2, y2, z2]`.
          sample_size : int
              An `int` that defines the number of lines that should be
              returned when iterating over `line_data`. (Default is 200)
          overlap : int
              An `int` that defines the overlap between two consecutive
              samples that are returned when iterating over `line_data`.
              An overlap of 0 means consecutive samples, while an overlap
              of (`sample_size` - 1) means incrementing the samples by one.
              A negative overlap means skipping values between samples. It
              is required to be smaller than `sample_size`. (Default is 0)
          number_of_lines : int
              An `int` that corresponds to len(`line_data`), or the number of
              LoRs stored by `line_data`.
          number_of_samples : int
              An `int` that corresponds to the number of samples that can be
              accessed from the class. It takes `overlap` into consideration.

          Raises
          ------
          ValueError
              If `overlap` >= `sample_size` unless `sample_size` is 0. Overlap
              has to be smaller than `sample_size`. Note that it can also be negative.
          ValueError
              If `line_data` does not have (N, 7) shape.

          Notes
          -----
          The class saves `line_data` as a **contiguous** numpy array for
          efficient access in C functions. It should not be changed after
          instantiating the class.

          '''

          def __init__(
              self,
              line_data,
              sample_size = 200,
              overlap = 0,
              verbose = False
          ):

              if verbose:
                  start = time.time()

              # If sample_size != 0 (in which case the class returns all data in one
              # sample), check the `overlap` is not larger or equal to `sample_size`.
              if sample_size < 0:
                  raise ValueError('\n[ERROR]: sample_size = {} must be positive (>= 0)'.format(sample_size))
              if sample_size != 0 and overlap >= sample_size:
                  raise ValueError('\n[ERROR]: overlap = {} must be smaller than sample_size = {}\n'.format(overlap, sample_size))

              # Initialise the inner parameters of the class
              self._index = 0
              self._sample_size = sample_size
              self._overlap = overlap

              # If `line_data` is not C-contiguous, create a C-contiguous copy
              self._line_data = np.asarray(line_data, order = 'C', dtype = float)

              # Check that line_data has shape (N, 7)
              if self._line_data.ndim != 2 or self._line_data.shape[1] != 7:
                  raise ValueError('\n[ERROR]: line_data should have dimensions (N, 7). Received {}\n'.format(self._line_data.shape))

              self._number_of_lines = len(self._line_data)

              if verbose:
                  end = time.time()
                  print("Initialising the PEPT data took {} seconds\n".format(end - start))

          @property
          def line_data(self):
              '''Get the lines stored in the class.

              Returns
              -------
              (, 7) numpy.ndarray
                  A memory view of the lines stored in `line_data`.

              '''

              return self._line_data

          @property
          def sample_size(self):
              '''Get the number of lines in one sample returned by the class.

              Returns
              -------
              int
                  The sample size (number of lines) in one sample returned by
                  the class.

              '''

              return self._sample_size

          @sample_size.setter
          def sample_size(self, new_sample_size):
              '''Change `sample_size` without instantiating a new object

              It also resets the inner index of the class.

              Parameters
              ----------
              new_sample_size : int
                  The new sample size. It has to be larger than `overlap`,
                  unless it is 0 (in which case all `line_data` will be returned
                  as one sample).

              Raises
              ------
              ValueError
                  If `overlap` >= `new_sample_size`. Overlap has to be
                  smaller than `sample_size`, unless `sample_size` is 0.
                  Note that it can also be negative.

              '''

              if new_sample_size < 0:
                  raise ValueError('\n[ERROR]: sample_size = {} must be positive (>= 0)'.format(new_sample_size))
              if new_sample_size != 0 and self._overlap >= new_sample_size:
                  raise ValueError('\n[ERROR]: overlap = {} must be smaller than new_sample_size = {}\n'.format(self._overlap, new_sample_size))

              self._index = 0
              self._sample_size = new_sample_size

          @property
          def overlap(self):
              '''Get the overlap between every two samples returned by the class.

              Returns
              -------
              int
                  The overlap (number of lines) between every two samples  returned by
                  the class.

              '''

              return self._overlap

          @overlap.setter
          def overlap(self, new_overlap):
              '''Change `overlap` without instantiating a new object

              It also resets the inner index of the class.

              Parameters
              ----------
              new_overlap : int
                  The new overlap. It has to be smaller than `sample_size`, unless
                  `sample_size` is 0 (in which case all `line_data` will be returned
                  as one sample and so overlap does not play any role).

              Raises
              ------
              ValueError
                  If `new_overlap` >= `sample_size`. `new_overlap` has to be
                  smaller than `sample_size`, unless `sample_size` is 0.
                  Note that it can also be negative.

              '''

              if self._sample_size != 0 and new_overlap >= self._sample_size:
                  raise ValueError('\n[ERROR]: new_overlap = {} must be smaller than sample_size = {}\n'.format(new_overlap, self._sample_size))

              self._index = 0
              self._overlap = new_overlap

          @property
          def number_of_samples(self):
              '''Get number of samples, considering overlap.

              If `sample_size == 0`, all data is returned as a single sample,
              and so `number_of_samples` will be 1. Otherwise, it checks the
              number of samples every time it is called, taking `overlap` into
              consideration.

              Returns
              -------
              int
                  The number of samples, taking `overlap` into consideration.

              '''
              # If self.sample_size == 0, all data is returned as a single sample
              if self._sample_size == 0:
                  return 1

              # If self.sample_size != 0, check there is at least one sample
              if self._number_of_lines >= self._sample_size:
                  return (self._number_of_lines - self._sample_size) // (self.sample_size - self.overlap) + 1
              else:
                  return 0

          @property
          def number_of_lines(self):
              '''Get the number of lines stored in the class.

              Returns
              -------
              int
                  The number of lines stored in `line_data`.

              '''
              return self._number_of_lines

          def sample_n(self, n):
              '''Get sample number n (indexed from 1, i.e. `n > 0`)

              Returns the lines from `line_data` included in sample number
              `n`. Samples are numbered starting from 1.

              Parameters
              ----------
              n : int
                  The number of the sample required. Note that `1 <= n <=
                  number_of_samples`.

              Returns
              -------
              (, 7) numpy.ndarray
                  A shallow copy of the lines from `line_data` included in
                  sample number n.

              Raises
              ------
              IndexError
                  If `sample_size == 0`, all data is returned as one single
                  sample. Raised if `n` is not 1.
              IndexError
                  If `n > number_of_samples` or `n <= 0`.

              '''
              if self._sample_size == 0:
                  if n == 1:
                      return self._line_data
                  else:
                      raise IndexError("\n\n[ERROR]: Trying to access a non-existent sample (samples are indexed from 1): asked for sample number {}, when there is only 1 sample (sample_size == 0)\n".format(n))
              elif (n > self.number_of_samples) or n <= 0:
                  raise IndexError("\n\n[ERROR]: Trying to access a non-existent sample (samples are indexed from 1): asked for sample number {}, when there are {} samples\n".format(n, self.number_of_samples))

              start_index = (n - 1) * (self._sample_size - self._overlap)
              return self._line_data[start_index:(start_index + self._sample_size)]

          def to_csv(self, filepath, delimiter = '  ', newline = '\n'):
              '''Write `line_data` to a CSV file

              Write all LoRs stored in the class to a CSV file.

              Parameters
              ----------
                  filepath : filename or file handle
                      If filepath is a path (rather than file handle), it is relative
                      to where python is called.
                  delimiter : str, optional
                      The delimiter between values. The default is two spaces '  ',
                      such that numbers in the format '123,456.78' are well-understood.
                  newline : str, optional
                      The sequence of characters at the end of every line. The default
                      is a new line '\n'

              '''
              np.savetxt(filepath, self._line_data, delimiter = delimiter, newline = newline)

          def plot_all_lines(self, ax = None, color='r', alpha=1.0 ):
              '''Plot all lines using matplotlib

              Given a **mpl_toolkits.mplot3d.Axes3D** axis `ax`, plots all lines on it.

              Parameters
              ----------
              ax : mpl_toolkits.mplot3D.Axes3D object
                  The 3D matplotlib-based axis for plotting.

              color : matplotlib color option (default 'r')

              alpha : matplotlib opacity option (default 1.0)

              Returns
              -------

              fig, ax : matplotlib figure and axes objects

              Note
              ----
              Plotting all lines in the case of large LoR arrays is *very*
              computationally intensive. For large arrays (> 10000), plotting
              individual samples using `plot_lines_sample_n` is recommended.

              '''

              if ax == None:
                  fig = plt.figure()
                  ax  = fig.add_subplot(111, projection='3d')
              else:
                  fig = plt.gcf()

              p1 = self._line_data[:, 1:4]
              p2 = self._line_data[:, 4:7]

              for i in range(0, self._number_of_lines):
                  ax.plot([ p1[i][0], p2[i][0] ],
                          [ p1[i][1], p2[i][1] ],
                          [ p1[i][2], p2[i][2] ],
                          c = color, alpha = alpha)

              return fig, ax

          def plot_all_lines_alt_axes(self, ax, color='r', alpha=1.0):
              '''Plot all lines using matplotlib on PEPT-style axes

              Given a **mpl_toolkits.mplot3d.Axes3D** axis `ax`, plots all lines on
              the PEPT-style convention: **x** is *parallel and horizontal* to the
              screens, **y** is *parallel and vertical* to the screens, **z** is
              *perpendicular* to the screens. The mapping relative to the
              Cartesian coordinates would then be: (x, y, z) -> (z, x, y)

              Parameters
              ----------
              ax : mpl_toolkits.mplot3D.Axes3D object
                  The 3D matplotlib-based axis for plotting.

              color : matplotlib color option (default 'r')

              alpha : matplotlib opacity option (default 1.0)

              Returns
              -------

              fig, ax : matplotlib figure and axes objects

              Note
              ----
              Plotting all lines in the case of large LoR arrays is *very*
              computationally intensive. For large arrays (> 10000), plotting
              individual samples using `plot_lines_sample_n_alt_axes` is recommended.

              '''

              if ax == None:
                  fig = plt.figure()
                  ax  = fig.add_subplot(111, projection='3d')
              else:
                  fig = plt.gcf()

              p1 = self._line_data[:, 1:4]
              p2 = self._line_data[:, 4:7]

              for i in range(0, self._number_of_lines):
                  ax.plot([ p1[i][2], p2[i][2] ],
                          [ p1[i][0], p2[i][0] ],
                          [ p1[i][1], p2[i][1] ],
                          c = color, alpha=alpha)

              return fig, ax

          def plot_lines_sample_n(self, n, ax = None, color = 'r', alpha = 1.0):
              '''Plot lines from sample `n` using matplotlib

              Given a **mpl_toolkits.mplot3d.Axes3D** axis `ax`, plots all lines
              from sample number `n`.

              Parameters
              ----------
              ax : mpl_toolkits.mplot3D.Axes3D object
                  The 3D matplotlib-based axis for plotting.

              sampleN : int
                  The number of the sample to be plotted.

              color : matplotlib color option (default 'r')

              alpha : matplotlib opacity option (default 1.0)

              Returns
              -------

              fig, ax : matplotlib figure and axes objects

              '''

              if ax == None:
                  fig = plt.figure()
                  ax  = fig.add_subplot(111, projection='3d')
              else:
                  fig = plt.gcf()

              sample = self.sample_n(n)
              for i in range(0, len(sample)):
                  ax.plot([ sample[i][1], sample[i][4] ],
                          [ sample[i][2], sample[i][5] ],
                          [ sample[i][3], sample[i][6] ],
                          c = color, alpha = alpha)

              return fig, ax

          def plot_lines_sample_n_alt_axes(self, n, ax=None, color='r', alpha=1.0):
              '''Plot lines from sample `n` using matplotlib on PEPT-style axes

              Given a **mpl_toolkits.mplot3d.Axes3D** axis `ax`, plots all lines from
              sample number sampleN on the PEPT-style coordinates convention:
              **x** is *parallel and horizontal* to the screens, **y** is
              *parallel and vertical* to the screens, **z** is *perpendicular*
              to the screens. The mapping relative to the Cartesian coordinates
              would then be: (x, y, z) -> (z, x, y)

              Parameters
              ----------
              ax : mpl_toolkits.mplot3D.Axes3D object
                  The 3D matplotlib-based axis for plotting.
              n : int
                  The number of the sample to be plotted.

              color : matplotlib color option (default 'r')

              alpha : matplotlib opacity option (default 1.0)

              Returns
              -------

              fig, ax : matplotlib figure and axes objects

              '''

              if ax == None:
                  fig = plt.figure()
                  ax  = fig.add_subplot(111, projection='3d')
              else:
                  fig = plt.gcf()

              sample = self.sample_n(n)
              for i in range(0, len(sample)):
                  ax.plot([ sample[i][3], sample[i][6] ],
                          [ sample[i][1], sample[i][4] ],
                          [ sample[i][2], sample[i][5] ],
                          c = color, alpha = alpha)

              return fig, ax

          def lines_trace(
              self,
              sample_indices = 0,
              width = 2,
              color = None,
              opacity = 0.6,
              colorbar = True,
              colorbar_col = 0,
              colorbar_title = None
          ):
              '''Get a Plotly trace for all the lines in selected samples.

              Creates a `plotly.graph_objects.Scatter3d` object for all the lines
              included in the samples selected by `sample_indices`. `sample_indices`
              can be a single sample index (e.g. 0) or an iterable of indices (e.g.
              [1,5,6]).
              Can then be passed to the `plotly.graph_objects.figure.add_trace`
              function or a `PlotlyGrapher` instance using the `add_trace` method.

              Parameters
              ----------
              sample_indices : int or iterable
                  The index or indices of the samples of LoRs.
              width : float
                  The width of the lines. The default is 2.
              color : str or list-like
                  Can be a single color (e.g. "black", "rgb(122, 15, 241)") or a colorbar list.
                  Is ignored if `colorbar` is set to True. For more information, check the Plotly
                  documentation. The default is None.
              opacity : float
                  The opacity of the lines, where 0 is transparent and 1 is fully
                  opaque. The default is 0.6.
              colorbar : bool
                  If set to True, will color-code the data in the sample column `colorbar_col`.
                  Overrides `color` if set to True. The default is True, so that every line has
                  a different color.
              colorbar_col : int
                  The column in the data samples that will be used to color the points. Only has
                  an effect if `colorbar` is set to True. The default is 0 (the first column - time).
              colorbar_title : str
                  If set, the colorbar will have this title above. The default is None.

              Returns
              -------
              plotly.graph_objs.Scatter3d
                  A Plotly trace of the LoRs.

              '''

              # Check if sample_indices is an iterable collection (list-like)
              # otherwise just "iterate" over the single number
              if not hasattr(sample_indices, "__iter__"):
                  sample_indices = [sample_indices]

              marker = dict(
                  width = width,
                  color = color,
              )

              if colorbar:
                  marker['color'] = []
                  marker.update(colorscale = "Magma")

                  if colorbar_title is not None:
                      marker.update(colorbar = dict(title = colorbar_title))

              coords_x = []
              coords_y = []
              coords_z = []

              # For each selected sample include all the lines' coordinates
              for n in sample_indices:
                  sample = self[n]

                  for line in sample:
                      coords_x.extend([line[1], line[4], None])
                      coords_y.extend([line[2], line[5], None])
                      coords_z.extend([line[3], line[6], None])

                      if colorbar:
                          marker['color'].extend(3 * [line[colorbar_col]])

              trace = go.Scatter3d(
                  x = coords_x,
                  y = coords_y,
                  z = coords_z,
                  mode = 'lines',
                  opacity = opacity,
                  line = marker
              )

              return trace

          def __len__(self):
              # Defined so that len(class_instance) returns the number of samples.

              return self.number_of_samples

          def __str__(self):
              # Shown when calling print(class)
              docstr = ""

              docstr += "number_of_lines =   {}\n\n".format(self.number_of_lines)
              docstr += "sample_size =       {}\n".format(self._sample_size)
              docstr += "overlap =           {}\n".format(self._overlap)
              docstr += "number_of_samples = {}\n\n".format(self.number_of_samples)
              docstr += "line_data = \n"
              docstr += self._line_data.__str__()

              return docstr

          def __repr__(self):
              # Shown when writing the class on a REPR

              docstr = "Class instance that inherits from `pept.LineData`.\n\n" + self.__str__() + "\n\n"
              docstr += "Particular cases:\n"
              docstr += " > If sample_size == 0, all line_data is returned as one single sample.\n"
              docstr += " > If overlap >= sample_size, an error is raised.\n"
              docstr += " > If overlap < 0, lines are skipped between samples.\n"

              return docstr

          def __getitem__(self, key):
              # Defined so that samples can be accessed as class_instance[0]

              if self.number_of_samples == 0:
                  raise IndexError("Tried to access sample {} (indexed from 0), when there are {} samples".format(key, self.number_of_samples))

              if key >= self.number_of_samples:
                  raise IndexError("Tried to access sample {} (indexed from 0), when there are {} samples".format(key, self.number_of_samples))

              while key < 0:
                  key += self.number_of_samples

              return self.sample_n(key + 1)

          def __iter__(self):
              # Defined so the class can be iterated as `for sample in class_instance: ...`
              return self

          def __next__(self):
              # sample_size = 0 => return all data
              if self._sample_size == 0:
                  self._sample_size = -1
                  return self._line_data
              # Use -1 as a flag
              if self._sample_size == -1:
                  self._sample_size = 0
                  raise StopIteration

              # sample_size > 0 => return slices
              if self._index != 0:
                  self._index = self._index + self._sample_size - self.overlap
              else:
                  self._index = self._index + self.sample_size

              if self._index > self.number_of_lines:
                  self._index = 0
                  raise StopIteration

              return self._line_data[(self._index - self._sample_size):self._index]

  ### Subclasses

  - ParallelScreens
  - ModularCamera

  ### Instance variables

  `var line_data`

    Get the lines stored in the class.

    ## Returns

    (, 7) numpy.ndarray A memory view of the lines stored in `line_data`.

    Source code

        @property
        def line_data(self):
            '''Get the lines stored in the class.

            Returns
            -------
            (, 7) numpy.ndarray
                A memory view of the lines stored in `line_data`.

            '''

            return self._line_data

  `var number_of_lines`

    Get the number of lines stored in the class.

    ## Returns

    `int`
      The number of lines stored in `line_data`.
    Source code

        @property
        def number_of_lines(self):
            '''Get the number of lines stored in the class.

            Returns
            -------
            int
                The number of lines stored in `line_data`.

            '''
            return self._number_of_lines

  `var number_of_samples`

    Get number of samples, considering overlap.

    If `sample_size == 0`, all data is returned as a single sample, and so `number_of_samples` will be 1. Otherwise, it checks the number of samples every time it is called, taking `overlap` into consideration.

    ## Returns

    `int`
      The number of samples, taking `overlap` into consideration.
    Source code

        @property
        def number_of_samples(self):
            '''Get number of samples, considering overlap.

            If `sample_size == 0`, all data is returned as a single sample,
            and so `number_of_samples` will be 1. Otherwise, it checks the
            number of samples every time it is called, taking `overlap` into
            consideration.

            Returns
            -------
            int
                The number of samples, taking `overlap` into consideration.

            '''
            # If self.sample_size == 0, all data is returned as a single sample
            if self._sample_size == 0:
                return 1

            # If self.sample_size != 0, check there is at least one sample
            if self._number_of_lines >= self._sample_size:
                return (self._number_of_lines - self._sample_size) // (self.sample_size - self.overlap) + 1
            else:
                return 0

  `var overlap`

    Get the overlap between every two samples returned by the class.

    ## Returns

    `int`
      The overlap (number of lines) between every two samples returned by the class.
    Source code

        @property
        def overlap(self):
            '''Get the overlap between every two samples returned by the class.

            Returns
            -------
            int
                The overlap (number of lines) between every two samples  returned by
                the class.

            '''

            return self._overlap

  `var sample_size`

    Get the number of lines in one sample returned by the class.

    ## Returns

    `int`
      The sample size (number of lines) in one sample returned by the class.
    Source code

        @property
        def sample_size(self):
            '''Get the number of lines in one sample returned by the class.

            Returns
            -------
            int
                The sample size (number of lines) in one sample returned by
                the class.

            '''

            return self._sample_size

  ### Methods

  `def lines_trace(self, sample_indices=0, width=2, color=None, opacity=0.6, colorbar=True, colorbar_col=0, colorbar_title=None)`

    Get a Plotly trace for all the lines in selected samples.

    Creates a `plotly.graph_objects.Scatter3d` object for all the lines included in the samples selected by `sample_indices`. `sample_indices` can be a single sample index (e.g. 0) or an iterable of indices (e.g. [1,5,6]). Can then be passed to the `plotly.graph_objects.figure.add_trace` function or a `PlotlyGrapher` instance using the `add_trace` method.

    ## Parameters

    `sample_indices` :`int` or `iterable`
      The index or indices of the samples of LoRs.
    `width` :`float`
      The width of the lines. The default is 2.
    `color` :`str` or `list`-`like`
      Can be a single color (e.g. "black", "rgb(122, 15, 241)") or a colorbar list. Is ignored if `colorbar` is set to True. For more information, check the Plotly documentation. The default is None.
    `opacity` :`float`
      The opacity of the lines, where 0 is transparent and 1 is fully opaque. The default is 0.6.
    `colorbar` :`bool`
      If set to True, will color-code the data in the sample column `colorbar_col`. Overrides `color` if set to True. The default is True, so that every line has a different color.
    `colorbar_col` :`int`
      The column in the data samples that will be used to color the points. Only has an effect if `colorbar` is set to True. The default is 0 (the first column - time).
    `colorbar_title` :`str`
      If set, the colorbar will have this title above. The default is None.

    ## Returns

    `plotly.graph_objs.Scatter3d`
      A Plotly trace of the LoRs.
    Source code

        def lines_trace(
            self,
            sample_indices = 0,
            width = 2,
            color = None,
            opacity = 0.6,
            colorbar = True,
            colorbar_col = 0,
            colorbar_title = None
        ):
            '''Get a Plotly trace for all the lines in selected samples.

            Creates a `plotly.graph_objects.Scatter3d` object for all the lines
            included in the samples selected by `sample_indices`. `sample_indices`
            can be a single sample index (e.g. 0) or an iterable of indices (e.g.
            [1,5,6]).
            Can then be passed to the `plotly.graph_objects.figure.add_trace`
            function or a `PlotlyGrapher` instance using the `add_trace` method.

            Parameters
            ----------
            sample_indices : int or iterable
                The index or indices of the samples of LoRs.
            width : float
                The width of the lines. The default is 2.
            color : str or list-like
                Can be a single color (e.g. "black", "rgb(122, 15, 241)") or a colorbar list.
                Is ignored if `colorbar` is set to True. For more information, check the Plotly
                documentation. The default is None.
            opacity : float
                The opacity of the lines, where 0 is transparent and 1 is fully
                opaque. The default is 0.6.
            colorbar : bool
                If set to True, will color-code the data in the sample column `colorbar_col`.
                Overrides `color` if set to True. The default is True, so that every line has
                a different color.
            colorbar_col : int
                The column in the data samples that will be used to color the points. Only has
                an effect if `colorbar` is set to True. The default is 0 (the first column - time).
            colorbar_title : str
                If set, the colorbar will have this title above. The default is None.

            Returns
            -------
            plotly.graph_objs.Scatter3d
                A Plotly trace of the LoRs.

            '''

            # Check if sample_indices is an iterable collection (list-like)
            # otherwise just "iterate" over the single number
            if not hasattr(sample_indices, "__iter__"):
                sample_indices = [sample_indices]

            marker = dict(
                width = width,
                color = color,
            )

            if colorbar:
                marker['color'] = []
                marker.update(colorscale = "Magma")

                if colorbar_title is not None:
                    marker.update(colorbar = dict(title = colorbar_title))

            coords_x = []
            coords_y = []
            coords_z = []

            # For each selected sample include all the lines' coordinates
            for n in sample_indices:
                sample = self[n]

                for line in sample:
                    coords_x.extend([line[1], line[4], None])
                    coords_y.extend([line[2], line[5], None])
                    coords_z.extend([line[3], line[6], None])

                    if colorbar:
                        marker['color'].extend(3 * [line[colorbar_col]])

            trace = go.Scatter3d(
                x = coords_x,
                y = coords_y,
                z = coords_z,
                mode = 'lines',
                opacity = opacity,
                line = marker
            )

            return trace

  `def plot_all_lines(self, ax=None, color='r', alpha=1.0)`

    Plot all lines using matplotlib

    Given a mpl_toolkits.mplot3d.Axes3D axis `ax`, plots all lines on it.

    ## Parameters

    `ax` :`mpl_toolkits.mplot3D.Axes3D` `object`
      The 3D matplotlib-based axis for plotting.
    `color` :`matplotlib` `color` `option` (default `'r'`)
    `alpha` :`matplotlib` `opacity` `option` (default `1.0`)

    ## Returns

    `fig`, `ax` :`matplotlib` `figure` `and` `axes` `objects`

    ## Note

    Plotting all lines in the case of large LoR arrays is very computationally intensive. For large arrays (> 10000), plotting individual samples using `plot_lines_sample_n` is recommended.

    Source code

        def plot_all_lines(self, ax = None, color='r', alpha=1.0 ):
            '''Plot all lines using matplotlib

            Given a **mpl_toolkits.mplot3d.Axes3D** axis `ax`, plots all lines on it.

            Parameters
            ----------
            ax : mpl_toolkits.mplot3D.Axes3D object
                The 3D matplotlib-based axis for plotting.

            color : matplotlib color option (default 'r')

            alpha : matplotlib opacity option (default 1.0)

            Returns
            -------

            fig, ax : matplotlib figure and axes objects

            Note
            ----
            Plotting all lines in the case of large LoR arrays is *very*
            computationally intensive. For large arrays (> 10000), plotting
            individual samples using `plot_lines_sample_n` is recommended.

            '''

            if ax == None:
                fig = plt.figure()
                ax  = fig.add_subplot(111, projection='3d')
            else:
                fig = plt.gcf()

            p1 = self._line_data[:, 1:4]
            p2 = self._line_data[:, 4:7]

            for i in range(0, self._number_of_lines):
                ax.plot([ p1[i][0], p2[i][0] ],
                        [ p1[i][1], p2[i][1] ],
                        [ p1[i][2], p2[i][2] ],
                        c = color, alpha = alpha)

            return fig, ax

  `def plot_all_lines_alt_axes(self, ax, color='r', alpha=1.0)`

    Plot all lines using matplotlib on PEPT-style axes

    Given a mpl_toolkits.mplot3d.Axes3D axis `ax`, plots all lines on the PEPT-style convention: x is parallel and horizontal to the screens, y is parallel and vertical to the screens, z is perpendicular to the screens. The mapping relative to the Cartesian coordinates would then be: (x, y, z) -> (z, x, y)

    ## Parameters

    `ax` :`mpl_toolkits.mplot3D.Axes3D` `object`
      The 3D matplotlib-based axis for plotting.
    `color` :`matplotlib` `color` `option` (default `'r'`)
    `alpha` :`matplotlib` `opacity` `option` (default `1.0`)

    ## Returns

    `fig`, `ax` :`matplotlib` `figure` `and` `axes` `objects`

    ## Note

    Plotting all lines in the case of large LoR arrays is very computationally intensive. For large arrays (> 10000), plotting individual samples using `plot_lines_sample_n_alt_axes` is recommended.

    Source code

        def plot_all_lines_alt_axes(self, ax, color='r', alpha=1.0):
            '''Plot all lines using matplotlib on PEPT-style axes

            Given a **mpl_toolkits.mplot3d.Axes3D** axis `ax`, plots all lines on
            the PEPT-style convention: **x** is *parallel and horizontal* to the
            screens, **y** is *parallel and vertical* to the screens, **z** is
            *perpendicular* to the screens. The mapping relative to the
            Cartesian coordinates would then be: (x, y, z) -> (z, x, y)

            Parameters
            ----------
            ax : mpl_toolkits.mplot3D.Axes3D object
                The 3D matplotlib-based axis for plotting.

            color : matplotlib color option (default 'r')

            alpha : matplotlib opacity option (default 1.0)

            Returns
            -------

            fig, ax : matplotlib figure and axes objects

            Note
            ----
            Plotting all lines in the case of large LoR arrays is *very*
            computationally intensive. For large arrays (> 10000), plotting
            individual samples using `plot_lines_sample_n_alt_axes` is recommended.

            '''

            if ax == None:
                fig = plt.figure()
                ax  = fig.add_subplot(111, projection='3d')
            else:
                fig = plt.gcf()

            p1 = self._line_data[:, 1:4]
            p2 = self._line_data[:, 4:7]

            for i in range(0, self._number_of_lines):
                ax.plot([ p1[i][2], p2[i][2] ],
                        [ p1[i][0], p2[i][0] ],
                        [ p1[i][1], p2[i][1] ],
                        c = color, alpha=alpha)

            return fig, ax

  `def plot_lines_sample_n(self, n, ax=None, color='r', alpha=1.0)`

    Plot lines from sample `n` using matplotlib

    Given a mpl_toolkits.mplot3d.Axes3D axis `ax`, plots all lines from sample number `n`.

    ## Parameters

    `ax` :`mpl_toolkits.mplot3D.Axes3D` `object`
      The 3D matplotlib-based axis for plotting.
    `sampleN` :`int`
      The number of the sample to be plotted.
    `color` :`matplotlib` `color` `option` (default `'r'`)
    `alpha` :`matplotlib` `opacity` `option` (default `1.0`)

    ## Returns

    `fig`, `ax` :`matplotlib` `figure` `and` `axes` `objects`
    Source code

        def plot_lines_sample_n(self, n, ax = None, color = 'r', alpha = 1.0):
            '''Plot lines from sample `n` using matplotlib

            Given a **mpl_toolkits.mplot3d.Axes3D** axis `ax`, plots all lines
            from sample number `n`.

            Parameters
            ----------
            ax : mpl_toolkits.mplot3D.Axes3D object
                The 3D matplotlib-based axis for plotting.

            sampleN : int
                The number of the sample to be plotted.

            color : matplotlib color option (default 'r')

            alpha : matplotlib opacity option (default 1.0)

            Returns
            -------

            fig, ax : matplotlib figure and axes objects

            '''

            if ax == None:
                fig = plt.figure()
                ax  = fig.add_subplot(111, projection='3d')
            else:
                fig = plt.gcf()

            sample = self.sample_n(n)
            for i in range(0, len(sample)):
                ax.plot([ sample[i][1], sample[i][4] ],
                        [ sample[i][2], sample[i][5] ],
                        [ sample[i][3], sample[i][6] ],
                        c = color, alpha = alpha)

            return fig, ax

  `def plot_lines_sample_n_alt_axes(self, n, ax=None, color='r', alpha=1.0)`

    Plot lines from sample `n` using matplotlib on PEPT-style axes

    Given a mpl_toolkits.mplot3d.Axes3D axis `ax`, plots all lines from sample number sampleN on the PEPT-style coordinates convention: x is parallel and horizontal to the screens, y is parallel and vertical to the screens, z is perpendicular to the screens. The mapping relative to the Cartesian coordinates would then be: (x, y, z) -> (z, x, y)

    ## Parameters

    `ax` :`mpl_toolkits.mplot3D.Axes3D` `object`
      The 3D matplotlib-based axis for plotting.
    `n` :`int`
      The number of the sample to be plotted.
    `color` :`matplotlib` `color` `option` (default `'r'`)
    `alpha` :`matplotlib` `opacity` `option` (default `1.0`)

    ## Returns

    `fig`, `ax` :`matplotlib` `figure` `and` `axes` `objects`
    Source code

        def plot_lines_sample_n_alt_axes(self, n, ax=None, color='r', alpha=1.0):
            '''Plot lines from sample `n` using matplotlib on PEPT-style axes

            Given a **mpl_toolkits.mplot3d.Axes3D** axis `ax`, plots all lines from
            sample number sampleN on the PEPT-style coordinates convention:
            **x** is *parallel and horizontal* to the screens, **y** is
            *parallel and vertical* to the screens, **z** is *perpendicular*
            to the screens. The mapping relative to the Cartesian coordinates
            would then be: (x, y, z) -> (z, x, y)

            Parameters
            ----------
            ax : mpl_toolkits.mplot3D.Axes3D object
                The 3D matplotlib-based axis for plotting.
            n : int
                The number of the sample to be plotted.

            color : matplotlib color option (default 'r')

            alpha : matplotlib opacity option (default 1.0)

            Returns
            -------

            fig, ax : matplotlib figure and axes objects

            '''

            if ax == None:
                fig = plt.figure()
                ax  = fig.add_subplot(111, projection='3d')
            else:
                fig = plt.gcf()

            sample = self.sample_n(n)
            for i in range(0, len(sample)):
                ax.plot([ sample[i][3], sample[i][6] ],
                        [ sample[i][1], sample[i][4] ],
                        [ sample[i][2], sample[i][5] ],
                        c = color, alpha = alpha)

            return fig, ax

  `def sample_n(self, n)`

    Get sample number n (indexed from 1, i.e. `n > 0`)

    Returns the lines from `line_data` included in sample number `n`. Samples are numbered starting from 1.

    ## Parameters

    `n` :`int`
      The number of the sample required. Note that `1 <= n <= number_of_samples`.

    ## Returns

    (, 7) numpy.ndarray A shallow copy of the lines from `line_data` included in sample number n.

    ## Raises

    `IndexError`
      If `sample_size == 0`, all data is returned as one single sample. Raised if `n` is not 1.
    `IndexError`
      If `n > number_of_samples` or `n <= 0`.
    Source code

        def sample_n(self, n):
            '''Get sample number n (indexed from 1, i.e. `n > 0`)

            Returns the lines from `line_data` included in sample number
            `n`. Samples are numbered starting from 1.

            Parameters
            ----------
            n : int
                The number of the sample required. Note that `1 <= n <=
                number_of_samples`.

            Returns
            -------
            (, 7) numpy.ndarray
                A shallow copy of the lines from `line_data` included in
                sample number n.

            Raises
            ------
            IndexError
                If `sample_size == 0`, all data is returned as one single
                sample. Raised if `n` is not 1.
            IndexError
                If `n > number_of_samples` or `n <= 0`.

            '''
            if self._sample_size == 0:
                if n == 1:
                    return self._line_data
                else:
                    raise IndexError("\n\n[ERROR]: Trying to access a non-existent sample (samples are indexed from 1): asked for sample number {}, when there is only 1 sample (sample_size == 0)\n".format(n))
            elif (n > self.number_of_samples) or n <= 0:
                raise IndexError("\n\n[ERROR]: Trying to access a non-existent sample (samples are indexed from 1): asked for sample number {}, when there are {} samples\n".format(n, self.number_of_samples))

            start_index = (n - 1) * (self._sample_size - self._overlap)
            return self._line_data[start_index:(start_index + self._sample_size)]

  `def to_csv(self, filepath, delimiter=' ', newline='\n')`

    Write `line_data` to a CSV file

            Write all LoRs stored in the class to a CSV file.

            Parameters
            ----------
                filepath : filename or file handle
                    If filepath is a path (rather than file handle), it is relative
                    to where python is called.
                delimiter : str, optional
                    The delimiter between values. The default is two spaces '  ',
                    such that numbers in the format '123,456.78' are well-understood.
                newline : str, optional
                    The sequence of characters at the end of every line. The default
                    is a new line '

    '

    Source code

        def to_csv(self, filepath, delimiter = '  ', newline = '\n'):
            '''Write `line_data` to a CSV file

            Write all LoRs stored in the class to a CSV file.

            Parameters
            ----------
                filepath : filename or file handle
                    If filepath is a path (rather than file handle), it is relative
                    to where python is called.
                delimiter : str, optional
                    The delimiter between values. The default is two spaces '  ',
                    such that numbers in the format '123,456.78' are well-understood.
                newline : str, optional
                    The sequence of characters at the end of every line. The default
                    is a new line '\n'

            '''
            np.savetxt(filepath, self._line_data, delimiter = delimiter, newline = newline)

`class PointData (point_data, sample_size=0, overlap=0, verbose=False)`

  A class for generic PEPT data iteration, manipulation and visualisation.

  This class is used to encapsulate points. Unlike `LineData`, it does not have any restriction on the maximum number of columns it can store. It can yield samples of the `point_data` of an adaptive `sample_size` and `overlap`, without requiring additional storage.

  ## Parameters

  `point_data` : (`N`, `M`) `numpy.ndarray`
    An (N, M >= 4) numpy array that stores points (or any generic 2D set of data). It expects that the first column is time, followed by cartesian (3D) coordinates of points in mm, followed by any extra information the user needs. A row is then [time, x, y, z, etc].
  `sample_size` :`int`, optional
    An `int`` that defines the number of points that should be returned when iterating over`point_data`. A`sample_size` of 0 yields all the data as one single sample. (Default is 200)
  `overlap` :`int`, optional
    An `int` that defines the overlap between two consecutive samples that are returned when iterating over `point_data`. An overlap of 0 means consecutive samples, while an overlap of (`sample_size` - 1) means incrementing the samples by one. A negative overlap means skipping values between samples. An error is raised if `overlap` is larger than or equal to `sample_size`. (Default is 0)
  `verbose` :`bool`, optional
    An option that enables printing the time taken for the initialisation of an instance of the class. Useful when reading large files (10gb files for PEPT data is not unheard of). (Default is True)

  ## Attributes

  `point_data` : (`N`, `M`) `numpy.ndarray`
    An (N, M >= 4) numpy array that stores the points as time, followed by cartesian (3D) coordinates of the point in mm, followed by any extra information. Each row is then `[time, x, y, z, etc]`.
  `sample_size` :`int`
    An `int` that defines the number of lines that should be returned when iterating over `point_data`. (Default is 200)
  `overlap` :`int`
    An `int` that defines the overlap between two consecutive samples that are returned when iterating over `point_data`. An overlap of 0 means consecutive samples, while an overlap of (`sample_size` - 1) means incrementing the samples by one. A negative overlap means skipping values between samples. It is required to be smaller than `sample_size`. (Default is 0)
  `number_of_points` :`int`
    An `int` that corresponds to len(`point_data`), or the number of points stored by `point_data`.
  `number_of_samples` :`int`
    An `int` that corresponds to the number of samples that can be accessed from the class, taking the `overlap` into consideration.

  ## Raises

  `ValueError`
    If `overlap` >= `sample_size`. Overlap is required to be smaller than `sample_size`, unless `sample_size` is 0. Note that it can also be negative.
  `ValueError`
    If `line_data` does not have (N, M) shape, where M >= 4.

  ## Notes

  The class saves `point_data` as a contiguous numpy array for efficient access in C extensions.

  Source code

      class PointData:
          '''A class for generic PEPT data iteration, manipulation and visualisation.

          This class is used to encapsulate points. Unlike `LineData`, it does not have
          any restriction on the maximum number of columns it can store. It can yield
          samples of the `point_data` of an adaptive `sample_size` and `overlap`,
          without requiring additional storage.

          Parameters
          ----------
          point_data : (N, M) numpy.ndarray
              An (N, M >= 4) numpy array that stores points (or any generic 2D set of
              data). It expects that the first column is time, followed by cartesian
              (3D) coordinates of points **in mm**, followed by any extra information
              the user needs. A row is then [time, x, y, z, etc].
          sample_size : int, optional
              An `int`` that defines the number of points that should be
              returned when iterating over `point_data`. A `sample_size` of 0
              yields all the data as one single sample. (Default is 200)
          overlap : int, optional
              An `int` that defines the overlap between two consecutive
              samples that are returned when iterating over `point_data`.
              An overlap of 0 means consecutive samples, while an overlap
              of (`sample_size` - 1) means incrementing the samples by one.
              A negative overlap means skipping values between samples. An
              error is raised if `overlap` is larger than or equal to
              `sample_size`. (Default is 0)
          verbose : bool, optional
              An option that enables printing the time taken for the
              initialisation of an instance of the class. Useful when
              reading large files (10gb files for PEPT data is not unheard
              of). (Default is True)

          Attributes
          ----------
          point_data : (N, M) numpy.ndarray
              An (N, M >= 4) numpy array that stores the points as time, followed by
              cartesian (3D) coordinates of the point **in mm**, followed by any extra
              information. Each row is then `[time, x, y, z, etc]`.
          sample_size : int
              An `int` that defines the number of lines that should be
              returned when iterating over `point_data`. (Default is 200)
          overlap : int
              An `int` that defines the overlap between two consecutive
              samples that are returned when iterating over `point_data`.
              An overlap of 0 means consecutive samples, while an overlap
              of (`sample_size` - 1) means incrementing the samples by one.
              A negative overlap means skipping values between samples. It
              is required to be smaller than `sample_size`. (Default is 0)
          number_of_points : int
              An `int` that corresponds to len(`point_data`), or the number of
              points stored by `point_data`.
          number_of_samples : int
              An `int` that corresponds to the number of samples that can be
              accessed from the class, taking the `overlap` into consideration.

          Raises
          ------
          ValueError
              If `overlap` >= `sample_size`. Overlap is required to be smaller
              than `sample_size`, unless `sample_size` is 0. Note that it can
              also be negative.
          ValueError
              If `line_data` does not have (N, M) shape, where M >= 4.

          Notes
          -----
          The class saves `point_data` as a **contiguous** numpy array for
          efficient access in C extensions.

          '''

          def __init__(
              self,
              point_data,
              sample_size = 0,
              overlap = 0,
              verbose = False
          ):

              if verbose:
                  start = time.time()

              if sample_size < 0:
                  raise ValueError('\n[ERROR]: sample_size = {} must be positive (>= 0)'.format(sample_size))
              if sample_size != 0 and overlap >= sample_size:
                  raise ValueError('\n[ERROR]: overlap = {} must be smaller than sample_size = {}\n'.format(overlap, sample_size))

              self._index = 0
              self._sample_size = sample_size
              self._overlap = overlap

              self._point_data = np.asarray(point_data, order = 'C', dtype = float)

              if self._point_data.ndim != 2 or self._point_data.shape[1] < 4:
                  raise ValueError('\n[ERROR]: point_data should have two dimensions (M, N), where N >= 4. Received {}\n'.format(self._point_data.shape))

              self._number_of_points = len(self._point_data)

              if verbose:
                  end = time.time()
                  print("Initialising the PEPT data took {} seconds\n".format(end - start))

          @property
          def point_data(self):
              '''Get the points stored in the class.

              Returns
              -------
              (M, N) numpy.ndarray
                  A memory view of the points stored in `point_data`.

              '''

              return self._point_data

          @property
          def sample_size(self):
              '''Get the number of points in one sample returned by the class.

              Returns
              -------
              int
                  The sample size (number of lines) in one sample returned by
                  the class.

              '''

              return self._sample_size

          @sample_size.setter
          def sample_size(self, new_sample_size):
              '''Change `sample_size` without instantiating a new object

              It also resets the inner index of the class.

              Parameters
              ----------
              new_sample_size : int
                  The new sample size. It has to be larger than `overlap`,
                  unless it is 0 (in which case all `point_data` will be returned
                  as one sample).

              Raises
              ------
              ValueError
                  If `overlap` >= `new_sample_size`. Overlap has to be
                  smaller than `sample_size`, unless `sample_size` is 0.
                  Note that it can also be negative.

              '''

              if new_sample_size < 0:
                  raise ValueError('\n[ERROR]: sample_size = {} must be positive (>= 0)'.format(new_sample_size))
              if new_sample_size != 0 and self._overlap >= new_sample_size:
                  raise ValueError('\n[ERROR]: overlap = {} must be smaller than new_sample_size = {}\n'.format(self._overlap, new_sample_size))

              self._index = 0
              self._sample_size = new_sample_size

          @property
          def overlap(self):
              '''Get the overlap between every two samples returned by the class.

              Returns
              -------
              int
                  The overlap (number of points) between every two samples  returned by
                  the class.

              '''

              return self._overlap

          @overlap.setter
          def overlap(self, new_overlap):
              '''Change `overlap` without instantiating a new object

              It also resets the inner index of the class.

              Parameters
              ----------
              new_overlap : int
                  The new overlap. It has to be smaller than `sample_size`, unless
                  `sample_size` is 0 (in which case all `point_data` will be returned
                  as one sample and so overlap does not play any role).

              Raises
              ------
              ValueError
                  If `new_overlap` >= `sample_size`. `new_overlap` has to be
                  smaller than `sample_size`, unless `sample_size` is 0.
                  Note that it can also be negative.

              '''

              if self._sample_size != 0 and new_overlap >= self._sample_size:
                  raise ValueError('\n[ERROR]: new_overlap = {} must be smaller than sample_size = {}\n'.format(new_overlap, self._sample_size))

              self._index = 0
              self._overlap = new_overlap

          @property
          def number_of_samples(self):
              '''Get number of samples, considering overlap.

              If `sample_size == 0`, all data is returned as a single sample,
              and so `number_of_samples` will be 1. Otherwise, it checks the
              number of samples every time it is called, taking `overlap` into
              consideration.

              Returns
              -------
              int
                  The number of samples, taking `overlap` into consideration.

              '''
              # If self.sample_size == 0, all data is returned as a single sample
              if self._sample_size == 0:
                  return 1

              # If self.sample_size != 0, check there is at least one sample
              if self._number_of_points >= self._sample_size:
                  return (self._number_of_points - self._sample_size) // (self.sample_size - self.overlap) + 1
              else:
                  return 0

          @property
          def number_of_points(self):
              '''Get the number of points stored in the class.

              Returns
              -------
              int
                  The number of points stored in `point_data`.

              '''
              return self._number_of_points

          def sample_n(self, n):
              '''Get sample number n (indexed from 1, i.e. `n > 0`)

              Returns the lines from `point_data` included in sample number
              `n`. Samples are numbered starting from 1.

              Parameters
              ----------
              n : int
                  The number of the sample required. Note that `1 <= n <=
                  number_of_samples`.

              Returns
              -------
              (M, N) numpy.ndarray
                  A shallow copy of the points from `point_data` included in
                  sample number n.

              Raises
              ------
              IndexError
                  If `sample_size == 0`, all data is returned as one single
                  sample. Raised if `n` is not 1.
              IndexError
                  If `n > number_of_samples` or `n <= 0`.

              '''
              if self._sample_size == 0:
                  if n == 1:
                      return self._point_data
                  else:
                      raise IndexError("\n\n[ERROR]: Trying to access a non-existent sample (samples indexed from 1): asked for sample number {}, when there is only 1 sample (sample_size == 0)\n".format(n))
              elif (n > self.number_of_samples) or n <= 0:
                  raise IndexError("\n\n[ERROR]: Trying to access a non-existent sample (samples are indexed from 1): asked for sample number {}, when there are {} samples\n".format(n, self.number_of_samples))

              start_index = (n - 1) * (self._sample_size - self._overlap)
              return self._point_data[start_index:(start_index + self._sample_size)]

          def to_csv(self, filepath, delimiter = '  ', newline = '\n'):
              '''Write `point_data` to a CSV file

              Write all points (and any extra data) stored in the class to a CSV file.

              Parameters
              ----------
                  filepath : filename or file handle
                      If filepath is a path (rather than file handle), it is relative
                      to where python is called.
                  delimiter : str, optional
                      The delimiter between values. The default is two spaces '  ',
                      such that numbers in the format '123,456.78' are well-understood.
                  newline : str, optional
                      The sequence of characters at the end of every line. The default
                      is a new line '\n'

              '''
              np.savetxt(filepath, self._point_data, delimiter = delimiter, newline = newline)

          def plot_all_points(self, ax = None):
              '''Plot all points using matplotlib

              Given a **mpl_toolkits.mplot3d.Axes3D** axis, plots all points on it.

              Parameters
              ----------
              ax : mpl_toolkits.mplot3D.Axes3D object
                  The 3D matplotlib-based axis for plotting.

              Returns
              -------
              fig, ax : matplotlib figure and axes objects

              Note
              ----
              Plotting all points in the case of large LoR arrays is *very*
              computationally intensive. For large arrays (> 10000), plotting
              individual samples using `plot_points_sample_n` is recommended.

              '''

              if ax == None:
                  fig = plt.figure()
                  ax  = fig.add_subplot(111, projection='3d')
              else:
                  fig = plt.gcf()

              # Scatter x, y, z, [color]

              x = self._point_data[:, 1],
              y = self._point_data[:, 2],
              z = self._point_data[:, 3],

              color = self._point_data[:, -1],

              cmap = plt.cm.magma
              color_array = cmap(colour_data)

              ax.scatter(x,y,z,c=color_array[0])

              return fig, ax

          def plot_all_points_alt_axes(self, ax = None ):
              '''Plot all points using matplotlib on PEPT-style axes

              Given a **mpl_toolkits.mplot3d.Axes3D** axis, plots all points on
              the PEPT-style convention: **x** is *parallel and horizontal* to the
              screens, **y** is *parallel and vertical* to the screens, **z** is
              *perpendicular* to the screens. The mapping relative to the
              Cartesian coordinates would then be: (x, y, z) -> (z, x, y)

              Parameters
              ----------
              ax : mpl_toolkits.mplot3D.Axes3D object
                  The 3D matplotlib-based axis for plotting.

              Returns
              -------
              fig, ax : matplotlib figure and axes objects

              Note
              ----
              Plotting all points in the case of large LoR arrays is *very*
              computationally intensive. For large arrays (> 10000), plotting
              individual samples using `plot_lines_sample_n_alt_axes` is recommended.

              '''

              if ax == None:
                  fig = plt.figure()
                  ax  = fig.add_subplot(111, projection='3d')
              else:
                  fig = plt.gcf()

              # Scatter x, y, z, [color]

              x = self._point_data[:, 1]
              y = self._point_data[:, 2]
              z = self._point_data[:, 3]

              color = self._point_data[:, -1]

              cmap = plt.cm.magma
              color_array = cmap(color)

              ax.scatter(z,x,y,c=color_array[0])

              return fig, ax

          def plot_points_sample_n(self, n, ax=None):
              '''Plot points from sample `n` using matplotlib

              Given a **mpl_toolkits.mplot3d.Axes3D** axis, plots all points
              from sample number `n`.

              Parameters
              ----------
              ax : mpl_toolkits.mplot3D.Axes3D object
                  The 3D matplotlib-based axis for plotting.
              n : int
                  The number of the sample to be plotted.

              Returns
              -------

              fig, ax : matplotlib figure and axes objects

              '''

              if ax == None:
                  fig = plt.figure()
                  ax  = fig.add_subplot(111, projection='3d')
              else:
                  fig = plt.gcf()

              # Scatter x, y, z, [color]

              sample = self.sample_n(n)

              x = sample[:, 1]
              y = sample[:, 2]
              z = sample[:, 3]

              color = sample[:, -1]

              cmap = plt.cm.magma
              color_array = cmap(color)

              ax.scatter(z,x,y,c=color_array[0])

              return fig, ax

          def plot_points_sample_n_alt_axes(self, n, ax=None):
              '''Plot points from sample `n` using matplotlib on PEPT-style axes

              Given a **mpl_toolkits.mplot3d.Axes3D** axis, plots all points from
              sample number sampleN on the PEPT-style coordinates convention:
              **x** is *parallel and horizontal* to the screens, **y** is
              *parallel and vertical* to the screens, **z** is *perpendicular*
              to the screens. The mapping relative to the Cartesian coordinates
              would then be: (x, y, z) -> (z, x, y)

              Parameters
              ----------
              ax : mpl_toolkits.mplot3D.Axes3D object
                  The 3D matplotlib-based axis for plotting.
              n : int
                  The number of the sample to be plotted.

              Returns
              -------

              fig, ax : matplotlib figure and axes objects
              '''

              if ax == None:
                  fig = plt.figure()
                  ax  = fig.add_subplot(111, projection='3d')
              else:
                  fig = plt.gcf()

              # Scatter x, y, z, [color]

              sample = self.sample_n(n)

              x = sample[:, 1]
              y = sample[:, 2]
              z = sample[:, 3]

              color = sample[:, -1]

              cmap = plt.cm.magma
              color_array = cmap(color)

              ax.scatter(z,x,y,c=color_array[0])

              return fig, ax

          def all_points_trace(self, size = 2, color = None):
              '''Get a Plotly trace of all points.

              Creates a `plotly.graph_objects.Scatter3d` object. Can
              then be passed to the `plotly.graph_objects.figure.add_trace`
              function or a `PlotlyGrapher` instance using the `add_trace` method.

              Returns
              -------
              plotly.graph_objects.Scatter3d
                  A `plotly.graph_objects.Scatter3d` trace of all points.

              Note
              ----
              Plotting all points in the case of large LoR arrays is *very*
              computationally intensive. For large arrays (> 10000), plotting
              individual samples using `points_sample_n_traces` is recommended.

              '''

              trace = go.Scatter3d(
                  x = self._point_data[:, 1],
                  y = self._point_data[:, 2],
                  z = self._point_data[:, 3],
                  mode = 'markers',
                  marker = dict(
                      size = size,
                      color = color,
                      opacity = 0.8
                  )
              )

              return trace

          def all_points_trace_colorbar(self, size = 2, colorbar_title = None):
              '''Get a Plotly trace of all points, colour-coding the last column of `point_data`.

              Creates a `plotly.graph_objects.Scatter3d` object. Can
              then be passed to the `plotly.graph_objects.figure.add_trace`
              function or a `PlotlyGrapher` instance using the `add_trace` method.

              Returns
              -------
              plotly.graph_objects.Scatter3d
                  A `plotly.graph_objects.Scatter3d` trace of all points.

              Note
              ----
              Plotting all points in the case of large LoR arrays is *very*
              computationally intensive. For large arrays (> 10000), plotting
              individual samples using `points_sample_n_traces` is recommended.

              '''

              if colorbar_title != None:
                  colorbar = dict(title = colorbar_title)
              else:
                  colorbar = dict()

              trace = go.Scatter3d(
                  x = self._point_data[:, 1],
                  y = self._point_data[:, 2],
                  z = self._point_data[:, 3],
                  mode = 'markers',
                  marker = dict(
                      size = size,
                      color = self._point_data[:, -1],
                      colorscale = 'Magma',
                      colorbar = colorbar,
                      opacity = 0.8
                  )
              )

              return trace

          def points_sample_n_trace(self, n, size = 2, color = None):
              '''Get a Plotly trace for all points in sample `n`.

              Returns a `plotly.graph_objects.Scatter3d` trace containing all points
              included in sample number `n`.
              Can then be passed to the `plotly.graph_objects.figure.add_trace`
              function or a `PlotlyGrapher` instance using the `add_trace` method.

              Parameters
              ----------
              n : int
                  The number of the sample to be plotted.

              Returns
              -------
              plotly.graph_object.Scatter3d
                  A `plotly.graph_objects.Scatter3d` trace of all points in sample `n`.

              '''

              sample = self.sample_n(n)
              trace = go.Scatter3d(
                  x = sample[:, 1],
                  y = sample[:, 2],
                  z = sample[:, 3],
                  mode = 'markers',
                  marker = dict(
                      size = size,
                      color = color,
                      opacity = 0.8
                  )
              )

              return trace

          def points_sample_n_trace_colorbar(self, n, size = 2, colorbar_title = None):
              '''Get a Plotly trace for all points in sample `n`, colour-coding the last column.

              Returns a `plotly.graph_objects.Scatter3d` trace containing all points
              included in sample number `n`.
              Can then be passed to the `plotly.graph_objects.figure.add_trace`
              function or a `PlotlyGrapher` instance using the `add_trace` method.

              Parameters
              ----------
              n : int
                  The number of the sample to be plotted.

              Returns
              -------
              plotly.graph_object.Scatter3d
                  A `plotly.graph_objects.Scatter3d` trace of all points in sample `n`.

              '''

              if colorbar_title != None:
                  colorbar = dict(title = colorbar_title)
              else:
                  colorbar = dict()

              sample = self.sample_n(n)
              trace = go.Scatter3d(
                  x = sample[:, 1],
                  y = sample[:, 2],
                  z = sample[:, 3],
                  mode = 'markers',
                  marker = dict(
                      size = size,
                      color = sample[:, -1],
                      colorscale = 'Magma',
                      colorbar = colorbar,
                      opacity = 0.8
                  )
              )

              return trace

          def points_trace(
              self,
              sample_indices = 0,
              size = 2,
              color = None,
              opacity = 0.8,
              colorbar = False,
              colorbar_col = -1,
              colorbar_title = None
          ):
              '''Get a Plotly trace for all points in selected samples, with possible color-coding.

              Returns a `plotly.graph_objects.Scatter3d` trace containing all points
              included in in the samples selected by `sample_indices`. `sample_indices`
              can be a single sample index (e.g. 0) or an iterable of indices (e.g.
              [1,5,6]).
              Can then be passed to the `plotly.graph_objects.figure.add_trace`
              function or a `PlotlyGrapher` instance using the `add_trace` method.

              Parameters
              ----------
              sample_indices : int or iterable
                  The index or indices of the samples of LoRs. The default is 0 (the first sample).
              size : float
                  The marker size of the points. The default is 2.
              color : str or list-like
                  Can be a single color (e.g. "black", "rgb(122, 15, 241)") or a colorbar list.
                  Is ignored if `colorbar` is set to True. For more information, check the Plotly
                  documentation. The default is None.
              opacity : float
                  The opacity of the lines, where 0 is transparent and 1 is fully
                  opaque. The default is 0.8.
              colorbar : bool
                  If set to True, will color-code the data in the sample column `colorbar_col`.
                  Overrides `color` if set to True. The default is False.
              colorbar_col : int
                  The column in the data samples that will be used to color the points. Only has
                  an effect if `colorbar` is set to True. The default is -1 (the last column).
              colorbar_title : str
                  If set, the colorbar will have this title above. The default is None.

              Returns
              -------
              plotly.graph_objs.Scatter3d
                  A Plotly trace of the points.

              '''

              # Check if sample_indices is an iterable collection (list-like)
              # otherwise just "iterate" over the single number
              if not hasattr(sample_indices, "__iter__"):
                  sample_indices = [sample_indices]

              coords_x = []
              coords_y = []
              coords_z = []

              marker = dict(
                  size = size,
                  color = color,
                  opacity = opacity
              )

              if colorbar:
                  marker['color'] = []
                  marker.update(colorscale = "Magma")

                  if colorbar_title is not None:
                      marker.update(colorbar = dict(title = colorbar_title))

              # For each selected sample include all the needed coordinates
              for n in sample_indices:
                  sample = self[n]

                  coords_x.extend(sample[:, 1])
                  coords_y.extend(sample[:, 2])
                  coords_z.extend(sample[:, 3])

                  if colorbar == True:
                      marker['color'].extend(sample[:, colorbar_col])

              trace = go.Scatter3d(
                  x = coords_x,
                  y = coords_y,
                  z = coords_z,
                  mode = "markers",
                  marker = marker
              )

              return trace

          def __len__(self):
              # Defined so that len(class_instance) returns the number of samples.

              return self.number_of_samples

          def __str__(self):
              # Shown when calling print(class)
              docstr = ""

              docstr += "number_of_points =  {}\n\n".format(self.number_of_points)
              docstr += "sample_size =       {}\n".format(self._sample_size)
              docstr += "overlap =           {}\n".format(self._overlap)
              docstr += "number_of_samples = {}\n\n".format(self.number_of_samples)
              docstr += "point_data = \n"
              docstr += self._point_data.__str__()

              return docstr

          def __repr__(self):
              # Shown when writing the class on a REPR

              docstr = "Class instance that inherits from `pept.PointData`.\n\n" + self.__str__() + "\n\n"
              docstr += "Particular cases:\n"
              docstr += " > If sample_size == 0, all point_data is returned as one single sample.\n"
              docstr += " > If overlap >= sample_size, an error is raised.\n"
              docstr += " > If overlap < 0, points are skipped between samples.\n"

              return docstr

          def __getitem__(self, key):
              # Defined so that samples can be accessed as class_instance[0]

              if self.number_of_samples == 0:
                  raise IndexError("Tried to access sample {} (indexed from 0), when there are {} samples".format(key, self.number_of_samples))

              if key >= self.number_of_samples:
                  raise IndexError("Tried to access sample {} (indexed from 0), when there are {} samples".format(key, self.number_of_samples))

              while key < 0:
                  key += self.number_of_samples

              return self.sample_n(key + 1)

          def __iter__(self):
              # Defined so the class can be iterated as `for sample in class_instance: ...`
              return self

          def __next__(self):
              # sample_size = 0 => return all data
              if self._sample_size == 0:
                  self._sample_size = -1
                  return self._point_data
              # Use -1 as a flag
              if self._sample_size == -1:
                  self._sample_size = 0
                  raise StopIteration

              # sample_size > 0 => return slices
              if self._index != 0:
                  self._index = self._index + self._sample_size - self.overlap
              else:
                  self._index = self._index + self.sample_size

              if self._index > self.number_of_points:
                  self._index = 0
                  raise StopIteration

              return self._point_data[(self._index - self._sample_size):self._index]

  ### Subclasses

  - Cutpoints

  ### Instance variables

  `var number_of_points`

    Get the number of points stored in the class.

    ## Returns

    `int`
      The number of points stored in `point_data`.
    Source code

        @property
        def number_of_points(self):
            '''Get the number of points stored in the class.

            Returns
            -------
            int
                The number of points stored in `point_data`.

            '''
            return self._number_of_points

  `var number_of_samples`

    Get number of samples, considering overlap.

    If `sample_size == 0`, all data is returned as a single sample, and so `number_of_samples` will be 1. Otherwise, it checks the number of samples every time it is called, taking `overlap` into consideration.

    ## Returns

    `int`
      The number of samples, taking `overlap` into consideration.
    Source code

        @property
        def number_of_samples(self):
            '''Get number of samples, considering overlap.

            If `sample_size == 0`, all data is returned as a single sample,
            and so `number_of_samples` will be 1. Otherwise, it checks the
            number of samples every time it is called, taking `overlap` into
            consideration.

            Returns
            -------
            int
                The number of samples, taking `overlap` into consideration.

            '''
            # If self.sample_size == 0, all data is returned as a single sample
            if self._sample_size == 0:
                return 1

            # If self.sample_size != 0, check there is at least one sample
            if self._number_of_points >= self._sample_size:
                return (self._number_of_points - self._sample_size) // (self.sample_size - self.overlap) + 1
            else:
                return 0

  `var overlap`

    Get the overlap between every two samples returned by the class.

    ## Returns

    `int`
      The overlap (number of points) between every two samples returned by the class.
    Source code

        @property
        def overlap(self):
            '''Get the overlap between every two samples returned by the class.

            Returns
            -------
            int
                The overlap (number of points) between every two samples  returned by
                the class.

            '''

            return self._overlap

  `var point_data`

    Get the points stored in the class.

    ## Returns

    (M, N) numpy.ndarray A memory view of the points stored in `point_data`.

    Source code

        @property
        def point_data(self):
            '''Get the points stored in the class.

            Returns
            -------
            (M, N) numpy.ndarray
                A memory view of the points stored in `point_data`.

            '''

            return self._point_data

  `var sample_size`

    Get the number of points in one sample returned by the class.

    ## Returns

    `int`
      The sample size (number of lines) in one sample returned by the class.
    Source code

        @property
        def sample_size(self):
            '''Get the number of points in one sample returned by the class.

            Returns
            -------
            int
                The sample size (number of lines) in one sample returned by
                the class.

            '''

            return self._sample_size

  ### Methods

  `def all_points_trace(self, size=2, color=None)`

    Get a Plotly trace of all points.

    Creates a `plotly.graph_objects.Scatter3d` object. Can then be passed to the `plotly.graph_objects.figure.add_trace` function or a `PlotlyGrapher` instance using the `add_trace` method.

    ## Returns

    `plotly.graph_objects.Scatter3d`
      A `plotly.graph_objects.Scatter3d` trace of all points.

    ## Note

    Plotting all points in the case of large LoR arrays is very computationally intensive. For large arrays (> 10000), plotting individual samples using `points_sample_n_traces` is recommended.

    Source code

        def all_points_trace(self, size = 2, color = None):
            '''Get a Plotly trace of all points.

            Creates a `plotly.graph_objects.Scatter3d` object. Can
            then be passed to the `plotly.graph_objects.figure.add_trace`
            function or a `PlotlyGrapher` instance using the `add_trace` method.

            Returns
            -------
            plotly.graph_objects.Scatter3d
                A `plotly.graph_objects.Scatter3d` trace of all points.

            Note
            ----
            Plotting all points in the case of large LoR arrays is *very*
            computationally intensive. For large arrays (> 10000), plotting
            individual samples using `points_sample_n_traces` is recommended.

            '''

            trace = go.Scatter3d(
                x = self._point_data[:, 1],
                y = self._point_data[:, 2],
                z = self._point_data[:, 3],
                mode = 'markers',
                marker = dict(
                    size = size,
                    color = color,
                    opacity = 0.8
                )
            )

            return trace

  `def all_points_trace_colorbar(self, size=2, colorbar_title=None)`

    Get a Plotly trace of all points, colour-coding the last column of `point_data`.

    Creates a `plotly.graph_objects.Scatter3d` object. Can then be passed to the `plotly.graph_objects.figure.add_trace` function or a `PlotlyGrapher` instance using the `add_trace` method.

    ## Returns

    `plotly.graph_objects.Scatter3d`
      A `plotly.graph_objects.Scatter3d` trace of all points.

    ## Note

    Plotting all points in the case of large LoR arrays is very computationally intensive. For large arrays (> 10000), plotting individual samples using `points_sample_n_traces` is recommended.

    Source code

        def all_points_trace_colorbar(self, size = 2, colorbar_title = None):
            '''Get a Plotly trace of all points, colour-coding the last column of `point_data`.

            Creates a `plotly.graph_objects.Scatter3d` object. Can
            then be passed to the `plotly.graph_objects.figure.add_trace`
            function or a `PlotlyGrapher` instance using the `add_trace` method.

            Returns
            -------
            plotly.graph_objects.Scatter3d
                A `plotly.graph_objects.Scatter3d` trace of all points.

            Note
            ----
            Plotting all points in the case of large LoR arrays is *very*
            computationally intensive. For large arrays (> 10000), plotting
            individual samples using `points_sample_n_traces` is recommended.

            '''

            if colorbar_title != None:
                colorbar = dict(title = colorbar_title)
            else:
                colorbar = dict()

            trace = go.Scatter3d(
                x = self._point_data[:, 1],
                y = self._point_data[:, 2],
                z = self._point_data[:, 3],
                mode = 'markers',
                marker = dict(
                    size = size,
                    color = self._point_data[:, -1],
                    colorscale = 'Magma',
                    colorbar = colorbar,
                    opacity = 0.8
                )
            )

            return trace

  `def plot_all_points(self, ax=None)`

    Plot all points using matplotlib

    Given a mpl_toolkits.mplot3d.Axes3D axis, plots all points on it.

    ## Parameters

    `ax` :`mpl_toolkits.mplot3D.Axes3D` `object`
      The 3D matplotlib-based axis for plotting.

    ## Returns

    `fig`, `ax` :`matplotlib` `figure` `and` `axes` `objects`

    ## Note

    Plotting all points in the case of large LoR arrays is very computationally intensive. For large arrays (> 10000), plotting individual samples using `plot_points_sample_n` is recommended.

    Source code

        def plot_all_points(self, ax = None):
            '''Plot all points using matplotlib

            Given a **mpl_toolkits.mplot3d.Axes3D** axis, plots all points on it.

            Parameters
            ----------
            ax : mpl_toolkits.mplot3D.Axes3D object
                The 3D matplotlib-based axis for plotting.

            Returns
            -------
            fig, ax : matplotlib figure and axes objects

            Note
            ----
            Plotting all points in the case of large LoR arrays is *very*
            computationally intensive. For large arrays (> 10000), plotting
            individual samples using `plot_points_sample_n` is recommended.

            '''

            if ax == None:
                fig = plt.figure()
                ax  = fig.add_subplot(111, projection='3d')
            else:
                fig = plt.gcf()

            # Scatter x, y, z, [color]

            x = self._point_data[:, 1],
            y = self._point_data[:, 2],
            z = self._point_data[:, 3],

            color = self._point_data[:, -1],

            cmap = plt.cm.magma
            color_array = cmap(colour_data)

            ax.scatter(x,y,z,c=color_array[0])

            return fig, ax

  `def plot_all_points_alt_axes(self, ax=None)`

    Plot all points using matplotlib on PEPT-style axes

    Given a mpl_toolkits.mplot3d.Axes3D axis, plots all points on the PEPT-style convention: x is parallel and horizontal to the screens, y is parallel and vertical to the screens, z is perpendicular to the screens. The mapping relative to the Cartesian coordinates would then be: (x, y, z) -> (z, x, y)

    ## Parameters

    `ax` :`mpl_toolkits.mplot3D.Axes3D` `object`
      The 3D matplotlib-based axis for plotting.

    ## Returns

    `fig`, `ax` :`matplotlib` `figure` `and` `axes` `objects`

    ## Note

    Plotting all points in the case of large LoR arrays is very computationally intensive. For large arrays (> 10000), plotting individual samples using `plot_lines_sample_n_alt_axes` is recommended.

    Source code

        def plot_all_points_alt_axes(self, ax = None ):
            '''Plot all points using matplotlib on PEPT-style axes

            Given a **mpl_toolkits.mplot3d.Axes3D** axis, plots all points on
            the PEPT-style convention: **x** is *parallel and horizontal* to the
            screens, **y** is *parallel and vertical* to the screens, **z** is
            *perpendicular* to the screens. The mapping relative to the
            Cartesian coordinates would then be: (x, y, z) -> (z, x, y)

            Parameters
            ----------
            ax : mpl_toolkits.mplot3D.Axes3D object
                The 3D matplotlib-based axis for plotting.

            Returns
            -------
            fig, ax : matplotlib figure and axes objects

            Note
            ----
            Plotting all points in the case of large LoR arrays is *very*
            computationally intensive. For large arrays (> 10000), plotting
            individual samples using `plot_lines_sample_n_alt_axes` is recommended.

            '''

            if ax == None:
                fig = plt.figure()
                ax  = fig.add_subplot(111, projection='3d')
            else:
                fig = plt.gcf()

            # Scatter x, y, z, [color]

            x = self._point_data[:, 1]
            y = self._point_data[:, 2]
            z = self._point_data[:, 3]

            color = self._point_data[:, -1]

            cmap = plt.cm.magma
            color_array = cmap(color)

            ax.scatter(z,x,y,c=color_array[0])

            return fig, ax

  `def plot_points_sample_n(self, n, ax=None)`

    Plot points from sample `n` using matplotlib

    Given a mpl_toolkits.mplot3d.Axes3D axis, plots all points from sample number `n`.

    ## Parameters

    `ax` :`mpl_toolkits.mplot3D.Axes3D` `object`
      The 3D matplotlib-based axis for plotting.
    `n` :`int`
      The number of the sample to be plotted.

    ## Returns

    `fig`, `ax` :`matplotlib` `figure` `and` `axes` `objects`
    Source code

        def plot_points_sample_n(self, n, ax=None):
            '''Plot points from sample `n` using matplotlib

            Given a **mpl_toolkits.mplot3d.Axes3D** axis, plots all points
            from sample number `n`.

            Parameters
            ----------
            ax : mpl_toolkits.mplot3D.Axes3D object
                The 3D matplotlib-based axis for plotting.
            n : int
                The number of the sample to be plotted.

            Returns
            -------

            fig, ax : matplotlib figure and axes objects

            '''

            if ax == None:
                fig = plt.figure()
                ax  = fig.add_subplot(111, projection='3d')
            else:
                fig = plt.gcf()

            # Scatter x, y, z, [color]

            sample = self.sample_n(n)

            x = sample[:, 1]
            y = sample[:, 2]
            z = sample[:, 3]

            color = sample[:, -1]

            cmap = plt.cm.magma
            color_array = cmap(color)

            ax.scatter(z,x,y,c=color_array[0])

            return fig, ax

  `def plot_points_sample_n_alt_axes(self, n, ax=None)`

    Plot points from sample `n` using matplotlib on PEPT-style axes

    Given a mpl_toolkits.mplot3d.Axes3D axis, plots all points from sample number sampleN on the PEPT-style coordinates convention: x is parallel and horizontal to the screens, y is parallel and vertical to the screens, z is perpendicular to the screens. The mapping relative to the Cartesian coordinates would then be: (x, y, z) -> (z, x, y)

    ## Parameters

    `ax` :`mpl_toolkits.mplot3D.Axes3D` `object`
      The 3D matplotlib-based axis for plotting.
    `n` :`int`
      The number of the sample to be plotted.

    ## Returns

    `fig`, `ax` :`matplotlib` `figure` `and` `axes` `objects`
    Source code

        def plot_points_sample_n_alt_axes(self, n, ax=None):
            '''Plot points from sample `n` using matplotlib on PEPT-style axes

            Given a **mpl_toolkits.mplot3d.Axes3D** axis, plots all points from
            sample number sampleN on the PEPT-style coordinates convention:
            **x** is *parallel and horizontal* to the screens, **y** is
            *parallel and vertical* to the screens, **z** is *perpendicular*
            to the screens. The mapping relative to the Cartesian coordinates
            would then be: (x, y, z) -> (z, x, y)

            Parameters
            ----------
            ax : mpl_toolkits.mplot3D.Axes3D object
                The 3D matplotlib-based axis for plotting.
            n : int
                The number of the sample to be plotted.

            Returns
            -------

            fig, ax : matplotlib figure and axes objects
            '''

            if ax == None:
                fig = plt.figure()
                ax  = fig.add_subplot(111, projection='3d')
            else:
                fig = plt.gcf()

            # Scatter x, y, z, [color]

            sample = self.sample_n(n)

            x = sample[:, 1]
            y = sample[:, 2]
            z = sample[:, 3]

            color = sample[:, -1]

            cmap = plt.cm.magma
            color_array = cmap(color)

            ax.scatter(z,x,y,c=color_array[0])

            return fig, ax

  `def points_sample_n_trace(self, n, size=2, color=None)`

    Get a Plotly trace for all points in sample `n`.

    Returns a `plotly.graph_objects.Scatter3d` trace containing all points included in sample number `n`. Can then be passed to the `plotly.graph_objects.figure.add_trace` function or a `PlotlyGrapher` instance using the `add_trace` method.

    ## Parameters

    `n` :`int`
      The number of the sample to be plotted.

    ## Returns

    `plotly.graph_object.Scatter3d`
      A `plotly.graph_objects.Scatter3d` trace of all points in sample `n`.
    Source code

        def points_sample_n_trace(self, n, size = 2, color = None):
            '''Get a Plotly trace for all points in sample `n`.

            Returns a `plotly.graph_objects.Scatter3d` trace containing all points
            included in sample number `n`.
            Can then be passed to the `plotly.graph_objects.figure.add_trace`
            function or a `PlotlyGrapher` instance using the `add_trace` method.

            Parameters
            ----------
            n : int
                The number of the sample to be plotted.

            Returns
            -------
            plotly.graph_object.Scatter3d
                A `plotly.graph_objects.Scatter3d` trace of all points in sample `n`.

            '''

            sample = self.sample_n(n)
            trace = go.Scatter3d(
                x = sample[:, 1],
                y = sample[:, 2],
                z = sample[:, 3],
                mode = 'markers',
                marker = dict(
                    size = size,
                    color = color,
                    opacity = 0.8
                )
            )

            return trace

  `def points_sample_n_trace_colorbar(self, n, size=2, colorbar_title=None)`

    Get a Plotly trace for all points in sample `n`, colour-coding the last column.

    Returns a `plotly.graph_objects.Scatter3d` trace containing all points included in sample number `n`. Can then be passed to the `plotly.graph_objects.figure.add_trace` function or a `PlotlyGrapher` instance using the `add_trace` method.

    ## Parameters

    `n` :`int`
      The number of the sample to be plotted.

    ## Returns

    `plotly.graph_object.Scatter3d`
      A `plotly.graph_objects.Scatter3d` trace of all points in sample `n`.
    Source code

        def points_sample_n_trace_colorbar(self, n, size = 2, colorbar_title = None):
            '''Get a Plotly trace for all points in sample `n`, colour-coding the last column.

            Returns a `plotly.graph_objects.Scatter3d` trace containing all points
            included in sample number `n`.
            Can then be passed to the `plotly.graph_objects.figure.add_trace`
            function or a `PlotlyGrapher` instance using the `add_trace` method.

            Parameters
            ----------
            n : int
                The number of the sample to be plotted.

            Returns
            -------
            plotly.graph_object.Scatter3d
                A `plotly.graph_objects.Scatter3d` trace of all points in sample `n`.

            '''

            if colorbar_title != None:
                colorbar = dict(title = colorbar_title)
            else:
                colorbar = dict()

            sample = self.sample_n(n)
            trace = go.Scatter3d(
                x = sample[:, 1],
                y = sample[:, 2],
                z = sample[:, 3],
                mode = 'markers',
                marker = dict(
                    size = size,
                    color = sample[:, -1],
                    colorscale = 'Magma',
                    colorbar = colorbar,
                    opacity = 0.8
                )
            )

            return trace

  `def points_trace(self, sample_indices=0, size=2, color=None, opacity=0.8, colorbar=False, colorbar_col=-1, colorbar_title=None)`

    Get a Plotly trace for all points in selected samples, with possible color-coding.

    Returns a `plotly.graph_objects.Scatter3d` trace containing all points included in in the samples selected by `sample_indices`. `sample_indices` can be a single sample index (e.g. 0) or an iterable of indices (e.g. [1,5,6]). Can then be passed to the `plotly.graph_objects.figure.add_trace` function or a `PlotlyGrapher` instance using the `add_trace` method.

    ## Parameters

    `sample_indices` :`int` or `iterable`
      The index or indices of the samples of LoRs. The default is 0 (the first sample).
    `size` :`float`
      The marker size of the points. The default is 2.
    `color` :`str` or `list`-`like`
      Can be a single color (e.g. "black", "rgb(122, 15, 241)") or a colorbar list. Is ignored if `colorbar` is set to True. For more information, check the Plotly documentation. The default is None.
    `opacity` :`float`
      The opacity of the lines, where 0 is transparent and 1 is fully opaque. The default is 0.8.
    `colorbar` :`bool`
      If set to True, will color-code the data in the sample column `colorbar_col`. Overrides `color` if set to True. The default is False.
    `colorbar_col` :`int`
      The column in the data samples that will be used to color the points. Only has an effect if `colorbar` is set to True. The default is -1 (the last column).
    `colorbar_title` :`str`
      If set, the colorbar will have this title above. The default is None.

    ## Returns

    `plotly.graph_objs.Scatter3d`
      A Plotly trace of the points.
    Source code

        def points_trace(
            self,
            sample_indices = 0,
            size = 2,
            color = None,
            opacity = 0.8,
            colorbar = False,
            colorbar_col = -1,
            colorbar_title = None
        ):
            '''Get a Plotly trace for all points in selected samples, with possible color-coding.

            Returns a `plotly.graph_objects.Scatter3d` trace containing all points
            included in in the samples selected by `sample_indices`. `sample_indices`
            can be a single sample index (e.g. 0) or an iterable of indices (e.g.
            [1,5,6]).
            Can then be passed to the `plotly.graph_objects.figure.add_trace`
            function or a `PlotlyGrapher` instance using the `add_trace` method.

            Parameters
            ----------
            sample_indices : int or iterable
                The index or indices of the samples of LoRs. The default is 0 (the first sample).
            size : float
                The marker size of the points. The default is 2.
            color : str or list-like
                Can be a single color (e.g. "black", "rgb(122, 15, 241)") or a colorbar list.
                Is ignored if `colorbar` is set to True. For more information, check the Plotly
                documentation. The default is None.
            opacity : float
                The opacity of the lines, where 0 is transparent and 1 is fully
                opaque. The default is 0.8.
            colorbar : bool
                If set to True, will color-code the data in the sample column `colorbar_col`.
                Overrides `color` if set to True. The default is False.
            colorbar_col : int
                The column in the data samples that will be used to color the points. Only has
                an effect if `colorbar` is set to True. The default is -1 (the last column).
            colorbar_title : str
                If set, the colorbar will have this title above. The default is None.

            Returns
            -------
            plotly.graph_objs.Scatter3d
                A Plotly trace of the points.

            '''

            # Check if sample_indices is an iterable collection (list-like)
            # otherwise just "iterate" over the single number
            if not hasattr(sample_indices, "__iter__"):
                sample_indices = [sample_indices]

            coords_x = []
            coords_y = []
            coords_z = []

            marker = dict(
                size = size,
                color = color,
                opacity = opacity
            )

            if colorbar:
                marker['color'] = []
                marker.update(colorscale = "Magma")

                if colorbar_title is not None:
                    marker.update(colorbar = dict(title = colorbar_title))

            # For each selected sample include all the needed coordinates
            for n in sample_indices:
                sample = self[n]

                coords_x.extend(sample[:, 1])
                coords_y.extend(sample[:, 2])
                coords_z.extend(sample[:, 3])

                if colorbar == True:
                    marker['color'].extend(sample[:, colorbar_col])

            trace = go.Scatter3d(
                x = coords_x,
                y = coords_y,
                z = coords_z,
                mode = "markers",
                marker = marker
            )

            return trace

  `def sample_n(self, n)`

    Get sample number n (indexed from 1, i.e. `n > 0`)

    Returns the lines from `point_data` included in sample number `n`. Samples are numbered starting from 1.

    ## Parameters

    `n` :`int`
      The number of the sample required. Note that `1 <= n <= number_of_samples`.

    ## Returns

    (M, N) numpy.ndarray A shallow copy of the points from `point_data` included in sample number n.

    ## Raises

    `IndexError`
      If `sample_size == 0`, all data is returned as one single sample. Raised if `n` is not 1.
    `IndexError`
      If `n > number_of_samples` or `n <= 0`.
    Source code

        def sample_n(self, n):
            '''Get sample number n (indexed from 1, i.e. `n > 0`)

            Returns the lines from `point_data` included in sample number
            `n`. Samples are numbered starting from 1.

            Parameters
            ----------
            n : int
                The number of the sample required. Note that `1 <= n <=
                number_of_samples`.

            Returns
            -------
            (M, N) numpy.ndarray
                A shallow copy of the points from `point_data` included in
                sample number n.

            Raises
            ------
            IndexError
                If `sample_size == 0`, all data is returned as one single
                sample. Raised if `n` is not 1.
            IndexError
                If `n > number_of_samples` or `n <= 0`.

            '''
            if self._sample_size == 0:
                if n == 1:
                    return self._point_data
                else:
                    raise IndexError("\n\n[ERROR]: Trying to access a non-existent sample (samples indexed from 1): asked for sample number {}, when there is only 1 sample (sample_size == 0)\n".format(n))
            elif (n > self.number_of_samples) or n <= 0:
                raise IndexError("\n\n[ERROR]: Trying to access a non-existent sample (samples are indexed from 1): asked for sample number {}, when there are {} samples\n".format(n, self.number_of_samples))

            start_index = (n - 1) * (self._sample_size - self._overlap)
            return self._point_data[start_index:(start_index + self._sample_size)]

  `def to_csv(self, filepath, delimiter=' ', newline='\n')`

    Write `point_data` to a CSV file

            Write all points (and any extra data) stored in the class to a CSV file.

            Parameters
            ----------
                filepath : filename or file handle
                    If filepath is a path (rather than file handle), it is relative
                    to where python is called.
                delimiter : str, optional
                    The delimiter between values. The default is two spaces '  ',
                    such that numbers in the format '123,456.78' are well-understood.
                newline : str, optional
                    The sequence of characters at the end of every line. The default
                    is a new line '

    '

    Source code

        def to_csv(self, filepath, delimiter = '  ', newline = '\n'):
            '''Write `point_data` to a CSV file

            Write all points (and any extra data) stored in the class to a CSV file.

            Parameters
            ----------
                filepath : filename or file handle
                    If filepath is a path (rather than file handle), it is relative
                    to where python is called.
                delimiter : str, optional
                    The delimiter between values. The default is two spaces '  ',
                    such that numbers in the format '123,456.78' are well-understood.
                newline : str, optional
                    The sequence of characters at the end of every line. The default
                    is a new line '\n'

            '''
            np.savetxt(filepath, self._point_data, delimiter = delimiter, newline = newline)

`class VoxelData (line_data, volume_limits=[500.0, 500.0, 500.0], number_of_voxels=[10, 10, 10], traverse=True, verbose=False)`
  Source code

      class VoxelData:

          def __init__(
              self,
              line_data,
              volume_limits = [500., 500., 500.],
              number_of_voxels = [10, 10, 10],
              traverse = True,
              verbose = False
          ):

              if verbose:
                  start = time.time()

              # If `line_data` is not C-contiguous, create a C-contiguous copy
              self._line_data = np.asarray(line_data, order = 'C', dtype = float)
              # Check that line_data has shape (N, 7)
              if self._line_data.ndim != 2 or self._line_data.shape[1] != 7:
                  raise ValueError('\n[ERROR]: line_data should have dimensions (N, 7). Received {}\n'.format(self._line_data.shape))

              self._number_of_lines = len(self._line_data)

              # If `volume_limits` is not C-contiguous, create a C-contiguous copy
              self._volume_limits = np.asarray(volume_limits, dtype = float, order = "C")
              # Check that volume_limits has shape (3,)
              if self._volume_limits.ndim != 1 or self._volume_limits.shape[0] != 3:
                  raise ValueError("\n[ERROR]: volume_limits should have dimensions (3,). Received {}\n".format(self._volume_limits.shape))

              # If `number_of_voxels` is not C-contiguous, create a C-contiguous copy
              self._number_of_voxels = np.asarray(number_of_voxels, dtype = int, order = "C")
              # Check that number_of_voxels has shape(3,)
              if self._number_of_voxels.ndim != 1 or self._number_of_voxels.shape[0] != 3:
                  raise ValueError("\n[ERROR]: number_of_voxels should have dimensions (3,). Received {}\n".format(self._number_of_voxels.shape))

              self._voxel_sizes = self._volume_limits / self._number_of_voxels

              # If, for dimension x, there are 5 voxels between coordinates 0
              # and 5, then the delimiting grid is [0, 1, 2, 3, 4, 5].
              self._voxel_grid = [np.linspace(0, self._volume_limits[i], self._number_of_voxels[i] + 1) for i in range(3)]

              # All access to voxel_positions will be done directly through the inner
              # class _VoxelPositions, so no need for a private property here
              self.voxel_positions = self._VoxelPositions(self._volume_limits, self._number_of_voxels)
              self._voxel_data = np.zeros(self._number_of_voxels, dtype = int)

              if traverse:
                  if verbose:
                      start_traverse = time.time()

                  if traverse == True:
                      self.traverse()
                  else:
                      self.traverse(traverse)

                  if verbose:
                      end_traverse = time.time()

              if verbose:
                  end = time.time()
                  print("Initialising the instance of VoxelData took {} seconds.\n".format(end - start))
                  if traverse:
                      print("Traversing all voxels took {} seconds.\n".format(end_traverse - start_traverse))

          class _VoxelPositions:

              def __init__(self, volume_limits, number_of_voxels):

                  self.volume_limits = np.asarray(volume_limits, dtype = float, order = "C")
                  self.number_of_voxels = np.asarray(number_of_voxels, dtype = int, order = "C")
                  self.voxel_sizes = self.volume_limits / self.number_of_voxels

                  self._index = 0

              def at(self, ix, iy, iz):
                  # Evaluate the position of the voxel (the centre of it) at indices
                  # [ix, iy, iz]

                  indices = np.array([ix, iy, iz], dtype = int)

                  if (indices >= self.number_of_voxels).any() or (indices < 0).any():
                      raise IndexError("[ERROR]: Each of the [ix, iy, iz] indices must be between 0 and the corresponding `number_of_voxels`.")

                  return self._at(indices)

              def _at(self, indices):
                  # Unchecked!
                  return self.voxel_sizes * (0.5 + indices)

              def at_corner(self, ix, iy, iz):
                  # Evaluate the position of the voxel (the corner of it) at indices
                  # [ix, iy, iz]

                  indices = np.array([ix, iy, iz], dtype = int)

                  if (indices >= self.number_of_voxels).any() or (indices < 0).any():
                      raise IndexError("[ERROR]: Each of the [ix, iy, iz] indices must be between 0 and the corresponding `number_of_voxels`.")

                  return self._at_corner(indices)

              def _at_corner(self, indices):
                  # Unchecked!
                  return self.voxel_sizes * indices

              def all(self):

                  positions = []
                  for i in range(self.number_of_voxels[0]):
                      for j in range(self.number_of_voxels[1]):
                          for k in range(self.number_of_voxels[2]):
                              positions.append(self._at(np.array([i, j, k])))

                  return np.array(positions)

              def __len__(self):
                  return self.number_of_voxels[0]

              def __getitem__(self, key):

                  if not isinstance(key, tuple):
                      key = (key,)

                  if len(key) > 3:
                      raise ValueError("[ERROR]: The accessor takes maximum 3 indices, {} were given.".format(len(key)))

                  # Calculate the starting and ending indices and the step for the
                  # [x, y, z] coordinates of all the elements that are accessed.
                  # The default (:, :, :) is the whole range.
                  start = [0, 0, 0]
                  stop = list(self.number_of_voxels)
                  step = [1, 1, 1]

                  # The ranges of data selection for each dimension. Default is a
                  # range, but can be an explicit list too (e.g. select elements
                  # [1,2,5]).
                  xyz_ranges = [range(stop[i]) for i in range(3)]

                  # Handles negative indices for each of the 3 dimensions.
                  def make_positive(index, dimension):
                      while index < 0:
                          index += self.number_of_voxels[dimension]
                      return index

                  # Interpret each key
                  for i in range(len(key)):
                      # If key[i] is an int, only access the elements at that index,
                      # equivalent to range(key[i], key[i] + 1, 1).
                      if isinstance(key[i], (int, np.integer)):
                          if key[i] >= self.number_of_voxels[i]:
                              raise IndexError("[ERROR]: Tried to access voxel number {} (indexed from 0), when there are {} voxels for dimension {}.".format(key[i], self.number_of_voxels[i], i))

                          index = make_positive(key[i], i)
                          start[i] = index
                          stop[i] = index + 1

                          xyz_ranges[i] = range(start[i], stop[i], step[i])

                      # Interpret the possible slices (1:5, ::-1, etc.).
                      elif isinstance(key[i], slice):
                          # First interpret the step for the ::-1 corner case.
                          if key[i].step is not None:
                              if not isinstance(key[i].step, (int, np.integer)):
                                  raise TypeError("Slice step must be an int. Received {}.".format(type(key[i].step)))
                              if key[i].step == 0:
                                  raise ValueError("Slice step cannot be zero.")
                              elif key[i].step < 0:
                                  # If the step is negative, the default start and
                                  # stop become (max_index - 1) and -1, such that
                                  # ::-1 works.
                                  start[i] = self.number_of_voxels[i] - 1
                                  stop[i] = -1
                                  step[i] = key[i].step
                              else:
                                  step[i] = key[i].step

                          if key[i].start is not None:
                              if not isinstance(key[i].start, (int, np.integer)):
                                  raise TypeError("Slice start must be an int. Received {}.".format(type(key[i].start)))
                              # Corner case: x = [1,2,3] => x[5:10] == []
                              start[i] = min(make_positive(key[i].start, i), self.number_of_voxels[i])

                          if key[i].stop is not None:
                              if not isinstance(key[i].stop, (int, np.integer)):
                                  raise TypeError("Slice stop must be an int. Received {}.".format(type(key[i].stop)))
                              # Corner case: x = [1,2,3] => x[5:10] == []
                              stop[i] = min(make_positive(key[i].stop, i), self.number_of_voxels[i])

                          xyz_ranges[i] = range(start[i], stop[i], step[i])

                      # Interpret iterable sequence of selected elements
                      elif hasattr(key[i], "__iter__"):
                          xyz_ranges[i] = np.asarray(key[i], dtype = int)

                      else:
                          raise TypeError("Indices must be either `int`, `slice` or iterable of `int`s. Received {}.".format(type(key[i])))

                  positions = []
                  # Iterate through all the elements that need to be accessed
                  for x in xyz_ranges[0]:
                      for y in xyz_ranges[1]:
                          for z in xyz_ranges[2]:
                              positions.append(self._at(np.array([x, y, z])))

                  if len(positions) == 1:
                      return positions[0]
                  else:
                      return np.array(positions)

              def __iter__(self):
                  return self

              def __next__(self):
                  if self._index >= len(self):
                      self._index = 0
                      raise StopIteration

                  self._index += 1
                  return self[self._index - 1]

          @property
          def line_data(self):
              return self._line_data

          @property
          def number_of_lines(self):
              return self._number_of_lines

          @property
          def volume_limits(self):
              return self._volume_limits

          @volume_limits.setter
          def volume_limits(self, volume_limits):
              # If `volume_limits` is not C-contiguous, create a C-contiguous copy
              self._volume_limits = np.asarray(volume_limits, dtype = float, order = "C")
              # Check that volume_limits has shape (3,)
              if self._volume_limits.ndim != 1 or self._volume_limits.shape[0] != 3:
                  raise ValueError("\n[ERROR]: volume_limits should have dimensions (3,). Received {}\n".format(self._volume_limits.shape))

              self._voxel_sizes = self._volume_limits / self._number_of_voxels

              # If, for dimension x, there are 5 voxels between coordinates 0
              # and 5, then the delimiting grid is [0, 1, 2, 3, 4, 5].
              self._voxel_grid = [np.linspace(0, self._volume_limits[i], self._number_of_voxels[i] + 1) for i in range(3)]

              # All access to voxel_positions will be done directly through the inner
              # class _VoxelPositions, so no need for a private property here
              self.voxel_positions = self._VoxelPositions(self._volume_limits, self._number_of_voxels)
              self._voxel_data = np.zeros(self._number_of_voxels, dtype = int)

          @property
          def number_of_voxels(self):
              return self._number_of_voxels

          @number_of_voxels.setter
          def number_of_voxels(self, number_of_voxels):
              # If `number_of_voxels` is not C-contiguous, create a C-contiguous copy
              self._number_of_voxels = np.asarray(number_of_voxels, dtype = int, order = "C")
              # Check that number_of_voxels has shape(3,)
              if self._number_of_voxels.ndim != 1 or self._number_of_voxels.shape[0] != 3:
                  raise ValueError("\n[ERROR]: number_of_voxels should have dimensions (3,). Received {}\n".format(self._number_of_voxels.shape))

              self._voxel_sizes = self._volume_limits / self._number_of_voxels

              # If, for dimension x, there are 5 voxels between coordinates 0
              # and 5, then the delimiting grid is [0, 1, 2, 3, 4, 5].
              self._voxel_grid = [np.linspace(0, self._volume_limits[i], self._number_of_voxels[i] + 1) for i in range(3)]

              # All access to voxel_positions will be done directly through the inner
              # class _VoxelPositions, so no need for a private property here
              self.voxel_positions = self._VoxelPositions(self._volume_limits, self._number_of_voxels)
              self._voxel_data = np.zeros(self._number_of_voxels, dtype = int)

          @property
          def voxel_sizes(self):
              return self._voxel_sizes

          @property
          def voxel_grid(self):
              return self._voxel_grid

          @property
          def voxel_data(self):
              return self._voxel_data

          def traverse_python(self, lor_indices = None):
              # Adapted from "A Fast Voxel Traversal Algorithm for Ray Tracing" by
              # John Amanatides and Andrew Woo.

              # Traverse voxels for all LoRs by default
              if lor_indices is None:
                  lor_indices = range(self._number_of_lines)

              if not hasattr(lor_indices, "__iter__"):
                  raise TypeError("[ERROR]: The `lor_indices` parameter must be iterable.")

              # The adapted grid traversal algorithm
              for li in lor_indices:
                  # Define a line as L(t) = U + t V
                  # If an LoR is defined as two points P1 and P2, then
                  # U = P1 and V = P2 - P1
                  p1 = self._line_data[li, 1:4]
                  p2 = self._line_data[li, 4:7]
                  u = p1
                  v = p2 - p1

                  ##############################################################
                  # Initialisation stage

                  # The step [sx, sy, sz] defines the sense of the LoR.
                  # If V[0] is positive, then sx = 1
                  # If V[0] is negative, then sx = -1
                  step = np.array([1, 1, 1], dtype = int)
                  for i, c in enumerate(v):
                      if c < 0:
                          step[i] = -1

                  # The current voxel indices [ix, iy, iz] that the line passes
                  # through.
                  voxel_index = np.array([0, 0, 0], dtype = int)

                  # The value of t at which the line passes through to the next
                  # voxel, for each dimension.
                  t_next = np.array([0., 0., 0.], dtype = float)

                  # Find the initial voxel that the line starts from, for each
                  # dimension.
                  for i in range(len(voxel_index)):
                      # If, for dimension x, there are 5 voxels between coordinates 0
                      # and 5, then the delimiting grid is [0, 1, 2, 3, 4, 5].
                      # If the line starts at 1.5, then it is part of the voxel at
                      # index 1.
                      voxel_index[i] = np.searchsorted(self._voxel_grid[i], u[i], side = "right") - 1

                      # If the line is going "up", the next voxel is the next one
                      if v[i] >= 0:
                          offset = 1
                      # If the line is going "down", the next voxel is the current one
                      else:
                          offset = 0
                      t_next[i] = (self._voxel_grid[i][voxel_index[i] + offset] - u[i]) / v[i]

                  # delta_t indicates how far along the ray we must move (in units of
                  # t) for each component to be equal to the size of the voxel in
                  # that dimension.
                  delta_t = np.abs(self._voxel_sizes / v)

                  ###############################################################
                  # Incremental traversal stage

                  # Loop until we reach the last voxel in space
                  while (voxel_index < self._number_of_voxels).all() and (voxel_index >= 0).all():

                      self._voxel_data[tuple(voxel_index)] += 1

                      # If p2 is fully bounded by the voxel, stop the algorithm
                      if ((self.voxel_positions._at_corner(voxel_index) < p2).all() and
                          (self.voxel_positions._at_corner(voxel_index + 1) > p2).all()):
                          break

                      # The dimension of the minimum t that makes the line pass
                      # through to the next voxel
                      min_i = t_next.argmin()
                      t_next[min_i] += delta_t[min_i]
                      voxel_index[min_i] += step[min_i]

          def traverse(self, lor_indices = None):
              # Traverse all intersecting voxels for selected LoRs.

              # Traverse voxels for all LoRs by default
              if lor_indices is None:
                  lor_indices = range(self._number_of_lines)

              if not hasattr(lor_indices, "__iter__"):
                  raise TypeError("[ERROR]: The `lor_indices` parameter must be iterable.")

              traverse3d(
                  self._voxel_data,
                  self._line_data[lor_indices],
                  self._voxel_grid[0],
                  self._voxel_grid[1],
                  self._voxel_grid[2]
              )

          def indices(self, coords):
              # Find the voxel indices for a point at `coords`
              coords = np.asarray(coords, dtype = float)
              if coords.ndim != 1 or coords.shape[0] != 3:
                  raise ValueError("The `coords` parameter must have shape (3,). Received {}.".format(coords))

              indices = np.array([0, 0, 0], dtype = int)
              for i in range(3):
                  indices[i] = np.searchsorted(self._voxel_grid[i], coords[i], side = "right") - 1

              return indices

          def cube_trace(self, index, opacity = 0.4, color = None, colorscale = False):
              # For a small number of cubes

              index = np.asarray(index, dtype = int)
              xyz = self.voxel_positions.at_corner(*index)

              x = np.array([0, 0, 1, 1, 0, 0, 1, 1]) * self._voxel_sizes[0]
              y = np.array([0, 1, 1, 0, 0, 1, 1, 0]) * self._voxel_sizes[1]
              z = np.array([0, 0, 0, 0, 1, 1, 1, 1]) * self._voxel_sizes[2]
              i = np.array([7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2])
              j = np.array([3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3])
              k = np.array([0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6])

              cube = dict(
                  x =  x + xyz[0],
                  y =  y + xyz[1],
                  z =  z + xyz[2],
                  i =  i,
                  j =  j,
                  k =  k,
                  opacity = opacity,
                  color = color
              )

              if colorscale:
                  cmap = matplotlib.cm.get_cmap("magma")
                  c = cmap(self._voxel_data[tuple(index)] / (self._voxel_data.max() or 1))
                  cube.update(
                      color = "rgb({},{},{})".format(c[0], c[1], c[2])
                  )

              return go.Mesh3d(cube)

          def cubes_traces(
              self,
              condition = lambda voxel_data: voxel_data > 0,
              opacity = 0.4,
              color = None,
              colorscale = False
          ):
              # For a small number of cubes

              indices = np.argwhere(condition(self._voxel_data))
              traces = [self.cube_trace(i, opacity = opacity, color = color, colorscale = colorscale) for i in indices]

              return traces

          def voxels_trace(
              self,
              condition = lambda voxel_data: voxel_data > 0,
              size = 4,
              opacity = 0.4,
              color = None,
              colorscale = False
          ):
              # For a large number of cubes

              filtered_indices = np.argwhere(condition(self._voxel_data))
              positions = self.voxel_positions._at(filtered_indices)

              marker = dict(
                  size = size,
                  color = color,
                  symbol = "square"
              )

              if colorscale:
                  cvalues = [self._voxel_data[tuple(t)] for t in filtered_indices]
                  marker.update(colorscale = "Magma", color = cvalues)

              voxels = dict(
                  x = positions[:, 0],
                  y = positions[:, 1],
                  z = positions[:, 2],
                  opacity = opacity,
                  mode = "markers",
                  marker = marker
              )

              return go.Scatter3d(voxels)

          def heatmap_trace(
              self,
              ix = None,
              iy = None,
              iz = None,
              width = 0
          ):

              if ix is not None:
                  x = self._voxel_grid[1]
                  y = self._voxel_grid[2]
                  z = self._voxel_data[ix, :, :]

                  for i in range(1, width + 1):
                      z = z + self._voxel_data[ix + i, :, :]
                      z = z + self._voxel_data[ix - i, :, :]

              elif iy is not None:
                  x = self._voxel_grid[0]
                  y = self._voxel_grid[2]
                  z = self._voxel_data[:, iy, :]

                  for i in range(1, width + 1):
                      z = z + self._voxel_data[:, iy + i, :]
                      z = z + self._voxel_data[:, iy - i, :]

              elif iz is not None:
                  x = self._voxel_grid[0]
                  y = self._voxel_grid[1]
                  z = self._voxel_data[:, :, iz]

                  for i in range(1, width + 1):
                      z = z + self._voxel_data[:, :, iz + i]
                      z = z + self._voxel_data[:, :, iz - i]

              else:
                  raise ValueError("[ERROR]: One of the `ix`, `iy`, `iz` slice indices must be provided.")

              heatmap = dict(
                  x = x,
                  y = y,
                  z = z,
                  colorscale = "Magma",
                  transpose = True
              )

              return go.Heatmap(heatmap)

          def __str__(self):
              # Shown when calling print(class)
              docstr = ""

              docstr += "number_of_lines =   {}\n\n".format(self.number_of_lines)
              docstr += "volume_limits =     {}\n".format(self.volume_limits)
              docstr += "number_of_voxels =  {}\n".format(self.number_of_voxels)
              docstr += "voxel_sizes =       {}\n\n".format(self.voxel_sizes)

              docstr += "line_data = \n"
              docstr += self._line_data.__str__()

              docstr += "\n\nvoxel_data = \n"
              docstr += self._voxel_data.__str__()

              return docstr

          def __repr__(self):
              # Shown when writing the class on a REPR

              docstr = "Class instance that inherits from `pept.VoxelData`.\n\n" + self.__str__() + "\n\n"

              return docstr

  ### Instance variables

  `var line_data`
    Source code

        @property
        def line_data(self):
            return self._line_data

  `var number_of_lines`
    Source code

        @property
        def number_of_lines(self):
            return self._number_of_lines

  `var number_of_voxels`
    Source code

        @property
        def number_of_voxels(self):
            return self._number_of_voxels

  `var volume_limits`
    Source code

        @property
        def volume_limits(self):
            return self._volume_limits

  `var voxel_data`
    Source code

        @property
        def voxel_data(self):
            return self._voxel_data

  `var voxel_grid`
    Source code

        @property
        def voxel_grid(self):
            return self._voxel_grid

  `var voxel_sizes`
    Source code

        @property
        def voxel_sizes(self):
            return self._voxel_sizes

  ### Methods

  `def cube_trace(self, index, opacity=0.4, color=None, colorscale=False)`
    Source code

        def cube_trace(self, index, opacity = 0.4, color = None, colorscale = False):
            # For a small number of cubes

            index = np.asarray(index, dtype = int)
            xyz = self.voxel_positions.at_corner(*index)

            x = np.array([0, 0, 1, 1, 0, 0, 1, 1]) * self._voxel_sizes[0]
            y = np.array([0, 1, 1, 0, 0, 1, 1, 0]) * self._voxel_sizes[1]
            z = np.array([0, 0, 0, 0, 1, 1, 1, 1]) * self._voxel_sizes[2]
            i = np.array([7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2])
            j = np.array([3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3])
            k = np.array([0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6])

            cube = dict(
                x =  x + xyz[0],
                y =  y + xyz[1],
                z =  z + xyz[2],
                i =  i,
                j =  j,
                k =  k,
                opacity = opacity,
                color = color
            )

            if colorscale:
                cmap = matplotlib.cm.get_cmap("magma")
                c = cmap(self._voxel_data[tuple(index)] / (self._voxel_data.max() or 1))
                cube.update(
                    color = "rgb({},{},{})".format(c[0], c[1], c[2])
                )

            return go.Mesh3d(cube)

  `def cubes_traces(self, condition= at 0x11815e560>, opacity=0.4, color=None, colorscale=False)`
    Source code

        def cubes_traces(
            self,
            condition = lambda voxel_data: voxel_data > 0,
            opacity = 0.4,
            color = None,
            colorscale = False
        ):
            # For a small number of cubes

            indices = np.argwhere(condition(self._voxel_data))
            traces = [self.cube_trace(i, opacity = opacity, color = color, colorscale = colorscale) for i in indices]

            return traces

  `def heatmap_trace(self, ix=None, iy=None, iz=None, width=0)`
    Source code

        def heatmap_trace(
            self,
            ix = None,
            iy = None,
            iz = None,
            width = 0
        ):

            if ix is not None:
                x = self._voxel_grid[1]
                y = self._voxel_grid[2]
                z = self._voxel_data[ix, :, :]

                for i in range(1, width + 1):
                    z = z + self._voxel_data[ix + i, :, :]
                    z = z + self._voxel_data[ix - i, :, :]

            elif iy is not None:
                x = self._voxel_grid[0]
                y = self._voxel_grid[2]
                z = self._voxel_data[:, iy, :]

                for i in range(1, width + 1):
                    z = z + self._voxel_data[:, iy + i, :]
                    z = z + self._voxel_data[:, iy - i, :]

            elif iz is not None:
                x = self._voxel_grid[0]
                y = self._voxel_grid[1]
                z = self._voxel_data[:, :, iz]

                for i in range(1, width + 1):
                    z = z + self._voxel_data[:, :, iz + i]
                    z = z + self._voxel_data[:, :, iz - i]

            else:
                raise ValueError("[ERROR]: One of the `ix`, `iy`, `iz` slice indices must be provided.")

            heatmap = dict(
                x = x,
                y = y,
                z = z,
                colorscale = "Magma",
                transpose = True
            )

            return go.Heatmap(heatmap)

  `def indices(self, coords)`
    Source code

        def indices(self, coords):
            # Find the voxel indices for a point at `coords`
            coords = np.asarray(coords, dtype = float)
            if coords.ndim != 1 or coords.shape[0] != 3:
                raise ValueError("The `coords` parameter must have shape (3,). Received {}.".format(coords))

            indices = np.array([0, 0, 0], dtype = int)
            for i in range(3):
                indices[i] = np.searchsorted(self._voxel_grid[i], coords[i], side = "right") - 1

            return indices

  `def traverse(self, lor_indices=None)`
    Source code

        def traverse(self, lor_indices = None):
            # Traverse all intersecting voxels for selected LoRs.

            # Traverse voxels for all LoRs by default
            if lor_indices is None:
                lor_indices = range(self._number_of_lines)

            if not hasattr(lor_indices, "__iter__"):
                raise TypeError("[ERROR]: The `lor_indices` parameter must be iterable.")

            traverse3d(
                self._voxel_data,
                self._line_data[lor_indices],
                self._voxel_grid[0],
                self._voxel_grid[1],
                self._voxel_grid[2]
            )

  `def traverse_python(self, lor_indices=None)`
    Source code

        def traverse_python(self, lor_indices = None):
            # Adapted from "A Fast Voxel Traversal Algorithm for Ray Tracing" by
            # John Amanatides and Andrew Woo.

            # Traverse voxels for all LoRs by default
            if lor_indices is None:
                lor_indices = range(self._number_of_lines)

            if not hasattr(lor_indices, "__iter__"):
                raise TypeError("[ERROR]: The `lor_indices` parameter must be iterable.")

            # The adapted grid traversal algorithm
            for li in lor_indices:
                # Define a line as L(t) = U + t V
                # If an LoR is defined as two points P1 and P2, then
                # U = P1 and V = P2 - P1
                p1 = self._line_data[li, 1:4]
                p2 = self._line_data[li, 4:7]
                u = p1
                v = p2 - p1

                ##############################################################
                # Initialisation stage

                # The step [sx, sy, sz] defines the sense of the LoR.
                # If V[0] is positive, then sx = 1
                # If V[0] is negative, then sx = -1
                step = np.array([1, 1, 1], dtype = int)
                for i, c in enumerate(v):
                    if c < 0:
                        step[i] = -1

                # The current voxel indices [ix, iy, iz] that the line passes
                # through.
                voxel_index = np.array([0, 0, 0], dtype = int)

                # The value of t at which the line passes through to the next
                # voxel, for each dimension.
                t_next = np.array([0., 0., 0.], dtype = float)

                # Find the initial voxel that the line starts from, for each
                # dimension.
                for i in range(len(voxel_index)):
                    # If, for dimension x, there are 5 voxels between coordinates 0
                    # and 5, then the delimiting grid is [0, 1, 2, 3, 4, 5].
                    # If the line starts at 1.5, then it is part of the voxel at
                    # index 1.
                    voxel_index[i] = np.searchsorted(self._voxel_grid[i], u[i], side = "right") - 1

                    # If the line is going "up", the next voxel is the next one
                    if v[i] >= 0:
                        offset = 1
                    # If the line is going "down", the next voxel is the current one
                    else:
                        offset = 0
                    t_next[i] = (self._voxel_grid[i][voxel_index[i] + offset] - u[i]) / v[i]

                # delta_t indicates how far along the ray we must move (in units of
                # t) for each component to be equal to the size of the voxel in
                # that dimension.
                delta_t = np.abs(self._voxel_sizes / v)

                ###############################################################
                # Incremental traversal stage

                # Loop until we reach the last voxel in space
                while (voxel_index < self._number_of_voxels).all() and (voxel_index >= 0).all():

                    self._voxel_data[tuple(voxel_index)] += 1

                    # If p2 is fully bounded by the voxel, stop the algorithm
                    if ((self.voxel_positions._at_corner(voxel_index) < p2).all() and
                        (self.voxel_positions._at_corner(voxel_index + 1) > p2).all()):
                        break

                    # The dimension of the minimum t that makes the line pass
                    # through to the next voxel
                    min_i = t_next.argmin()
                    t_next[min_i] += delta_t[min_i]
                    voxel_index[min_i] += step[min_i]

  `def voxels_trace(self, condition= at 0x11815e680>, size=4, opacity=0.4, color=None, colorscale=False)`
    Source code

        def voxels_trace(
            self,
            condition = lambda voxel_data: voxel_data > 0,
            size = 4,
            opacity = 0.4,
            color = None,
            colorscale = False
        ):
            # For a large number of cubes

            filtered_indices = np.argwhere(condition(self._voxel_data))
            positions = self.voxel_positions._at(filtered_indices)

            marker = dict(
                size = size,
                color = color,
                symbol = "square"
            )

            if colorscale:
                cvalues = [self._voxel_data[tuple(t)] for t in filtered_indices]
                marker.update(colorscale = "Magma", color = cvalues)

            voxels = dict(
                x = positions[:, 0],
                y = positions[:, 1],
                z = positions[:, 2],
                opacity = opacity,
                mode = "markers",
                marker = marker
            )

            return go.Scatter3d(voxels)