Skip to content

Improve regularisation documentation and add ADMT demo - #427

Open
jacklovell wants to merge 21 commits into
developmentfrom
feature/admt-demo
Open

Improve regularisation documentation and add ADMT demo#427
jacklovell wants to merge 21 commits into
developmentfrom
feature/admt-demo

Conversation

@jacklovell

@jacklovell jacklovell commented Mar 11, 2024

Copy link
Copy Markdown
Member
  • Add a Regularisation section to the tomography documentation, describing the functionality inside the admt_utils module.
  • Fixup the docstrings in admt_utils.
  • Add calculation of "skewed" second derivative operators, which operate along the diagonals of the grid.
  • Add a bolometer diagonstic system to Generomak.
  • Add a new example using the Generomak bolometers and the regularisation operators to perform isotropic and ADMT inversions.
  • Add a regularised NNLS function which uses sparse matrices.
  • Enable the functions in admt_utils to optionally output sparse matrices depending on the input.

Fixes #382 and #380

* Add a Regularisation section to the tomography documentation,
  describing the functionality inside the admt_utils module.
* Fixup the docstrings in admt_utils.
* Add calculation of "skewed" second derivative operators, which
  operate along the diagonals of the grid.
* Add a bolometer diagonstic system to Generomak.
* Add a new example using the Generomak bolometers and the
  regularisation operators to perform isotropic and ADMT inversions.

@vsnever vsnever left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jacklovell, very valuable demo. A similar regularisation is used in ITER divertor bolometry to reconstruct the radiation power density profile. Generomak also receives the first diagnostics, which in itself is great.

I think the changelog should reflect that this demo has been added, and also that the bolometric diagnostic model has been added to Generomak.

I also have some comments and a few suggestions.

Comment thread docs/source/tools/tomography.rst Outdated
Comment thread demos/observers/bolometry/admt_tomographic_inversion.py
Comment thread demos/observers/bolometry/admt_tomographic_inversion.py
Comment thread cherab/generomak/diagnostics/bolometers.py Outdated
Comment thread cherab/generomak/diagnostics/bolometers.py Outdated
Comment thread cherab/generomak/diagnostics/bolometers.py Outdated
Comment thread cherab/generomak/diagnostics/bolometers.py Outdated
@@ -36,15 +36,15 @@ def generate_derivative_operators(voxel_vertices, grid_index_1d_to_2d_map,
Generate the first and second derivative operators for a regular grid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the interface and implementation of the generate_derivative_operators() a bit complex. In the case of a rectangular grid, it is enough to pass to the function arrays of grid nodes along the x and y axes, and a 2D voxel map. Also, this function can be written in a loop-free way. I propose the following implementation, which seems simpler to me:

def generate_derivative_operators(x, y, voxel_map):
    r"""
    Generate the first and second derivative operators for a regular rectangular grid.

    :param ndarray x: 1D array containing coordinates of grid nodes along the X-axis
    :param ndarray y: 1D array containing coordinates of grid nodes along the Y-axis.
    :param ndarray voxel_map: 2D integer array of shape (x.size - 1, y.size - 1) containing
        the indices of the voxels. This array maps spacially arranged cells to the voxels.
        Mapping multiple cells to the same voxel is not currently supported.
        Cells where `voxel_map == -1` are not mapped to any voxel.

    :return: a dictionary containing the derivative operators: Dij for
        i, j ∊ (x, y) and Di for i ∊ (x, y), Dsp and Dsm.

    Currently, all voxels are assumed to have the same width and height.
    If this is not the case, the results will be nonsense.

    The return dict contains all the first and second derivative
    operators:

    .. math::
        D_{xx} \equiv \frac{\partial^2}{\partial x^2}\\
        D_{xy} \equiv \frac{\partial^2}{\partial x \partial y}

    etc. It also produces two additional operators, Dsp and Dsm, for
    second derivatives on the dy/dx = 1 and dy/dx = -1 diagonals
    respectively.

    Note that the standard 2D laplacian (for isotropic regularisation)
    can be trivially calculated as follows:

    .. math::
        L = (1 - \alpha) (D_{xx} + D_{yy}) + (\alpha / 2) (D_{sp} + D_{sm})

    α = 2/3 produces the operator used in Carr et. al. RSI 89, 083506 (2018).
    α = 1/3 produces the operator with optimal isotropy.
    """

    x = np.asarray(x)
    y = np.asarray(y)

    if x.ndim != 1:
        raise ValueError('Attribute x must be a 1D array-like.')
    if y.ndim != 1:
        raise ValueError('Attribute y must be a 1D array-like.')

    voxel_map = np.asarray(voxel_map, dtype=int)

    dx = np.diff(x)
    dy = np.diff(y)

    if np.any(dx <= 0):
        raise ValueError('X-axis grid nodes must be given in ascending order.')
    if np.any(dy <= 0):
        raise ValueError('Y-axis grid nodes must be given in ascending order.')

    grid_shape = (dx.size, dy.size)
    if voxel_map.shape != grid_shape:
        raise ValueError('Shape mismatch: voxel_map shape {} does not match grid shape {}'.format(voxel_map.shape, grid_shape))

    num_cells = voxel_map.max() + 1

    if np.any(np.sort(voxel_map[voxel_map > -1]) != np.arange(num_cells, dtype=int)):
        raise ValueError('Voxel map contains duplicated cell numbers or some cell numbers are missing.')

    # assume regular grid
    # TODO: support non-regular rectangular grids in the future
    dx = dx.min()
    dy = dy.min()

    # add boundary rows and columns to avoid an out-of-bounds error
    voxel_map_ext = np.full((voxel_map.shape[0] + 2, voxel_map.shape[1] + 2), -1, dtype=int)
    voxel_map_ext[1:-1, 1:-1] = voxel_map

    # get cell indices for each voxel
    indx = np.where(voxel_map_ext > -1)
    voxel_indices = voxel_map_ext[indx]
    isort = np.argsort(voxel_indices)
    cell_indx = (indx[0][isort], indx[1][isort])

    # Individual derivative operators
    Dx = np.zeros((num_cells, num_cells))
    Dy = np.zeros((num_cells, num_cells))
    Dxx = np.zeros((num_cells, num_cells))
    Dxy = np.zeros((num_cells, num_cells))
    Dyy = np.zeros((num_cells, num_cells))
    Dsp = np.zeros((num_cells, num_cells))
    Dsm = np.zeros((num_cells, num_cells))

    # neighbouring voxel indices around each voxel
    n_below = voxel_map_ext[cell_indx[0], cell_indx[1] - 1]
    n_above = voxel_map_ext[cell_indx[0], cell_indx[1] + 1]
    n_left = voxel_map_ext[cell_indx[0] - 1, cell_indx[1]]
    n_right = voxel_map_ext[cell_indx[0] + 1, cell_indx[1]]
    n_below_left = voxel_map_ext[cell_indx[0] - 1, cell_indx[1] - 1]
    n_below_right = voxel_map_ext[cell_indx[0] + 1, cell_indx[1] - 1]
    n_above_left = voxel_map_ext[cell_indx[0] - 1, cell_indx[1] + 1]
    n_above_right = voxel_map_ext[cell_indx[0] + 1, cell_indx[1] + 1]

    # special voxel locations
    at_left = (n_left == -1) & (n_right > -1)
    at_right = (n_right == -1) & (n_left > -1)
    at_bottom = (n_below == -1) & (n_above > -1)
    at_top = (n_above == -1) & (n_below > -1)

    at_top_left = at_left & at_top & (n_below_right > -1)
    at_bottom_left = at_left & at_bottom & (n_above_right > -1)
    at_top_right = at_right & at_top & (n_below_left > -1)
    at_bottom_right = at_right & at_bottom & (n_above_left > -1)

    at_left_not_at_corner = at_left & ~(at_top_left | at_bottom_left)
    at_right_not_at_corner = at_right & ~(at_top_right | at_bottom_right)
    at_bottom_not_at_corner = at_bottom & ~(at_bottom_left | at_bottom_right)
    at_top_not_at_corner = at_top & ~(at_top_left | at_top_right)

    # filling derivative operator matrices
    np.fill_diagonal(Dxx, -2)
    np.fill_diagonal(Dyy, -2)

    indx, = np.where(n_below > -1)
    Dy[indx, n_below[indx]] = -1 / 2
    Dyy[indx, n_below[indx]] = 1

    indx, = np.where(n_above > -1)
    Dy[indx, n_above[indx]] = 1 / 2
    Dyy[indx, n_above[indx]] = 1

    indx, = np.where(n_left > -1)
    Dx[indx, n_left[indx]] = -1 / 2
    Dxx[indx, n_left[indx]] = 1

    indx, = np.where(n_right > -1)
    Dx[indx, n_right[indx]] = 1 / 2
    Dxx[indx, n_right[indx]] = 1

    indx, = np.where(n_below_left > -1)
    Dxy[indx, n_below_left[indx]] = 1 / 4

    indx, = np.where(n_above_left > -1)
    Dxy[indx, n_above_left[indx]] = -1 / 4

    indx, = np.where(n_below_right > -1)
    Dxy[indx, n_below_right[indx]] = -1 / 4

    indx, = np.where(n_above_right > -1)
    Dxy[indx, n_above_right[indx]] = 1 / 4

    indx, = np.where(at_left)
    Dx[indx, indx] = -1
    Dx[indx, n_right[indx]] = 1
    Dxx[indx, indx] = -1
    Dxx[indx, n_right[indx]] = 1

    indx, = np.where(at_left_not_at_corner)
    Dxy[indx, n_above_right[indx]] = 1 / 2
    Dxy[indx, n_below[indx]] = 1 / 2
    Dxy[indx, n_above[indx]] = -1 / 2
    Dxy[indx, n_below_right[indx]] = -1 / 2

    indx, = np.where(at_right)
    Dx[indx, n_left[indx]] = -1
    Dx[indx, indx] = 1
    Dxx[indx, n_left[indx]] = -1
    Dxx[indx, indx] = 1

    indx, = np.where(at_right_not_at_corner)
    Dxy[indx, n_above[indx]] = 1 / 2
    Dxy[indx, n_below_left[indx]] = 1 / 2
    Dxy[indx, n_above_left[indx]] = -1 / 2
    Dxy[indx, n_below[indx]] = -1 / 2

    indx, = np.where(at_top)
    Dy[indx, n_below[indx]] = -1
    Dy[indx, indx] = 1
    Dyy[indx, n_below[indx]] = -1
    Dyy[indx, indx] = 1

    indx, = np.where(at_top_not_at_corner)
    Dxy[indx, n_right[indx]] = 1 / 2
    Dxy[indx, n_below_left[indx]] = 1 / 2
    Dxy[indx, n_left[indx]] = -1 / 2
    Dxy[indx, n_below_right[indx]] = -1 / 2

    indx, = np.where(at_bottom)
    Dy[indx, n_above[indx]] = 1
    Dy[indx, indx] = -1
    Dyy[indx, n_above[indx]] = 1
    Dyy[indx, indx] = -1

    indx, = np.where(at_bottom_not_at_corner)
    Dxy[indx, n_above_right[indx]] = 1 / 2
    Dxy[indx, n_left[indx]] = 1 / 2
    Dxy[indx, n_above_left[indx]] = -1 / 2
    Dxy[indx, n_right[indx]] = -1 / 2

    indx, = np.where(at_top_left)
    Dxy[indx, n_below[indx]] = 1
    Dxy[indx, n_right[indx]] = 1
    Dxy[indx, indx] = -1
    Dxy[indx, n_below_right[indx]] = -1

    indx, = np.where(at_top_right)
    Dxy[indx, indx] = 1
    Dxy[indx, n_below_left[indx]] = 1
    Dxy[indx, n_left[indx]] = -1
    Dxy[indx, n_below[indx]] = -1

    indx, = np.where(at_bottom_left)
    Dxy[indx, n_above_right[indx]] = 1
    Dxy[indx, indx] = 1
    Dxy[indx, n_above[indx]] = -1
    Dxy[indx, n_right[indx]] = -1

    indx, = np.where(at_bottom_right)
    Dxy[indx, n_above[indx]] = 1
    Dxy[indx, n_left[indx]] = 1
    Dxy[indx, indx] = -1
    Dxy[indx, n_above_left[indx]] = -1

    indx, = np.where((n_above_left > -1) & (n_below_right > -1))
    Dsm[indx, indx] = -2
    Dsm[indx, n_above_left[indx]] = 1
    Dsm[indx, n_below_right[indx]] = 1
    indx, = np.where((n_above_left == -1) & (n_below_right > -1))
    Dsm[indx, indx] = -1
    Dsm[indx, n_below_right[indx]] = 1
    indx, = np.where((n_above_left > -1) & (n_below_right == -1))
    Dsm[indx, indx] = -1
    Dsm[indx, n_above_left[indx]] = 1

    indx, = np.where((n_above_right > -1) & (n_below_left > -1))
    Dsp[indx, indx] = -2
    Dsp[indx, n_above_right[indx]] = 1
    Dsp[indx, n_below_left[indx]] = 1
    indx, = np.where((n_above_right == -1) & (n_below_left > -1))
    Dsp[indx, indx] = -1
    Dsp[indx, n_below_left[indx]] = 1
    indx, = np.where((n_above_right > -1) & (n_below_left == -1))
    Dsp[indx, indx] = -1
    Dsp[indx, n_above_right[indx]] = 1

    Dx = Dx / dx
    Dy = Dy / dy
    Dxx = Dxx / dx**2
    Dyy = Dyy / dy**2
    Dxy = Dxy / (dx * dy)
    Dsp = Dsp / (dx**2 + dy**2)
    Dsm = Dsm / (dx**2 + dy**2)

    # Package all operators up into a dictionary
    operators = dict(Dx=Dx, Dy=Dy, Dxx=Dxx, Dyy=Dyy, Dxy=Dxy, Dsp=Dsp, Dsm=Dsm)
    return operators

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface you propose is indeed cleaner for the specific case of a regular rectangular grid, but we need to preserve backwards compatibility and also support non-regular grids such as the ToroidalVoxelGrid - which doesn't have to be rectangular - albeit with rectangular cross sections for individual voxels.

I'll look at vectorising the function but in order to support a ragged grid it might not be possible: for a ragged grid it was significantly simpler to just write out the loop. And it's only O(ncells) not O(ncells^2) so the performance hit isn't that bad for an operation which only needs to be done once.

At the very least, the derivative operators can be made sparse which will help with memory usage for large ncells.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to have a simpler easily readable function for regular grids as Vlad suggests and another one with irregular voxels in mind?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that 2 functions will be any easier to read than 1 function.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not only about having two function, I should have explained better. In his comments, Vlad is suggesting that a function assuming regular grid would make the demo simpler and therefore the use on regular grids in general. In my experience, regular grids are by far the most common choice these days.

So the question is, whether having a function with limited scope tailored for the most common use case outweighs the longer source code with two functions, where the second would be more general.

Or a wrapper function that would handle the mapping from regular grid into general could be created to simplify the most common use case.

Alternatively I have a rather longer term idea for consideration. Given that inversions are not the primary aim of Cherab, inversion related parts (all or the more advanced ones) could be relocated to a dedicated package.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've remembered now why I implemented it this way.

We've historically used "masked" voxel grids for doing bolometry inversions in the machine-specific subpackages: examples for JET, MAST-U and AUG. For a voxel grid there is a measurable performance advantage to omitting voxels which aren't within the first wall of the machine, which leads to "ragged" arrays of voxels which roughly follow the contours of the first wall/limiting surface.

Since this formulism is already in widespread use I think it's important to support it for both new and existing users. And since the Ray Transfer Matrix formulism specialises this more general formulism to a regular grid it's easy to support it too (as the demo shows). I don't think it would be as straightforward to do the reverse: write a function expecting a regular grid and have end users convert ragged grids into the correct form.

I could I suppose also show the usage with a ToroidalVoxelGrid, which would even more closely match what the existing packages do. But that case is sufficiently trivial (all these machine packages give exactly the inputs required for this function) that I think users would be able to work it our for themselves.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other big advantage of the looping method over the vectorized method is memory efficiency when sparse=True is passed through, which is important for computing the operators for large grids on memory-limited machines. This alone I think makes it worthwhile keeping the existing looping function. Hopefully with the added comments it'll be a bit easier to follow now, which I think was the essence of Vlad's original comment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the backward compatibility and existing codes as the most important reason for keeping the approach you proposed. The memory efficiency is a nice bonus.

I think that having an explicit example for trivial code is worth it should the code be used frequently. Do you see the use with ToroidalVoxelGrid as a sort of "default" approach? If so, I would be in favor of including that example. It could be a comment in existing demo if that is possible, not necessarily entirely new one.

Comment on lines +149 to +169
grid_index_1d_to_2d_map = {}
for k, idx2d in enumerate(ray_transfer_grid.invert_voxel_map()):
# We want the x and z elements, as the Ray Transfer grid is 3D and this
# inversion is going to be in 2D.
grid_index_1d_to_2d_map[k] = (idx2d[0].item(), idx2d[2].item())
grid_index_2d_to_1d_map = {}
nx, _, ny = ray_transfer_grid.voxel_map.shape
for i in range(nx):
for j in range(ny):
voxel_index = ray_transfer_grid.voxel_map[i, 0, j]
if voxel_index != -1:
grid_index_2d_to_1d_map[(i, j)] = voxel_index
# We now need an (Nx4x2) array of voxel vertices, which can be easily calculated.
voxel_centres = np.array([cell_centres[grid_index_1d_to_2d_map[i]]
for i in range(ray_transfer_grid.bins)])
vertex_displacements = np.array([[-cell_dx/2, -cell_dz/2],
[-cell_dx/2, cell_dz/2],
[cell_dx/2, cell_dz/2],
[cell_dx/2, -cell_dz/2]])
# Combine the (N,2) and (4,2) arrays to get an (N,4,2) array.
voxel_vertices = voxel_centres[:, None, :] + vertex_displacements[None, :, :]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this code can be skipped if the proposed implementation of the generate_derivative_operators function is accepted.

derivative_operators = admt.generate_derivative_operators(
    cell_vertices_r, cell_vertices_z, ray_transfer_grid.voxel_map.squeeze()
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBF quite a bit of this code could have been skipped if I hadn't missed the obvious fact that the 2D-to-1D map is simply the inverse of the 1D-to-2D map:

grid_index_2d_to_1d_map = {rz: k for (k, rz) in grid_index_1d_to_2d_map.items()}

And vice versa.

So actually, only one of the 2D-to-1D or 1D-to-2D maps is required and the other can always be computed trivially, without loss of generality. I'll make generate_derivative_operators optionally accept only one of these two arguments, and automatically compute the other if the user don't already provide both. That'll simplify the demo too while maintaining backwards compatibility.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8e2a399 allows passing only 1 voxel mapping to the derivative operators generator function, which significantly simplifies this for the end user even in the Ray Transfer case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is fine to leave these few extra lines here now. I suppose that this code could be simplified by adding properties or methods to grid classes in the future.

jacklovell and others added 6 commits May 18, 2026 09:39
* Fix the series of transforms to position and orient the bolometer
  cameras.
* Update geometry parameters such that the lines of sight all follow a
  consistant convention and specify the channel ordering in the
  docstring. This required adding the ability to rotate the sensors
  about the viewing axis to reverse the channel order of the
  horizontal-poloidal camera.
* Correct the docstring of load_bolometers to specify the correct
  number of 4-channel sensors in each camera.
* Expand docstrings and comments to better describe the intricacies of
  setting up the bolometer geometry.
* More thorough commenting of the function to make it easier to follow
  what is going on and why.
* Exploit the sparsity of the derivative opreators but using a
  dictionary-of-keys representation instead of dense numpy arrays. A
  dictionary is faster to insert single elements into inside the loop
  than a numpy array too.
* Remove the `np.isnan` checkc which are slow for single elements,
  replace with a cheap check for `None`.
* Support returning the operators as Scipy sparse arrays for use
  downstream. Default to returning as Numpy arrays for backwards
  compatibility.

These changes result in an order of magnitude speedup in this function.
This is done automatically if the derivative operators are
sparse. A dense operator is returned otherwise.
An alternative to `invert_regularised_nnls` using sparse weight and
penalty matrices. The numerical algorithm used is slightly different
to `scipy.optimize.nnls`, as it uses the TRF variant of a bounded
`scipy.optimize.lsq_linear` with the lower bound set to 0 to enforce
positivity. Since the results differ due to floating point precision,
the sparse variant is implemented as a separate function to maintain
backwards compatibility.
* Produce additional inversions which incorporate Generomak's
  tangentially-viewing channels, for comparison with the poloidal-only
  inversions.
* Correctly scale the emissivity.
* Fix removing the ray transfer grid from the world before
  forward-modelling the bolometer measurements.
* Better explain why we're building the 1D <-> 2D maps by hand instead
  of just using the ray transfer matrix capabilities directly: this is
  for generality.
* Use the sparse variant of the NNLS inversion for speed and memory
  efficiency. The grid is large enough for this to be worthwhile.
@jacklovell jacklovell changed the title WIP: Improve regularisation documentation and add ADMT demo Improve regularisation documentation and add ADMT demo Jul 15, 2026
@Mateasek Mateasek mentioned this pull request Jul 16, 2026
12 tasks
Comment thread cherab/generomak/diagnostics/bolometers.py
Comment thread cherab/generomak/diagnostics/bolometers.py
Comment thread cherab/tools/inversions/nnls.py
This makes it easier for users to explore the geometry and potentially
even edit it for testing purposes.
This is a more common term in the literature for a bolometer unit
integrating several (typically 4) channels.
* The 1D-to-2D and 2D-to-1D maps are the inverse of one another, so
  one can be computed from the other. Depending on how the end user
  forms their inversion grid it may be simpler to calculate the
  1D-to-2D or the 2D-to-1D, so allow the caller to pass either and
  compute any missing mapping. If the caller already has both
  mappings (machine packages like cherab-jet, cherab-mastu and
  cherab-aug already generate both) then accept both too.
* Add tests for auto-computing missing mappings.
* Tests uncovered a bug converting the Dyy sparse operator to dense
  when `sparse=False` was passed: fix that and complete test coverage
  for `admt_utils`.
* Simplify the ADMT script by only computing the 1D-to-2D map as this
  is fairly trivially obtained from the Ray Transfer object.
@jacklovell
jacklovell requested a review from skuba31 July 31, 2026 16:19
@jacklovell

Copy link
Copy Markdown
Member Author

I've implemented the changes from both reviews now: thanks to @vsnever and @skuba31. I am going to hold firm on the formulism in generate_derivative_operators though, as I think the benefits of sparseness and explicitness outweigh any potential performance gains or loss of generality of the vectorized version Vlad proposed before the iterative improvements from the reviews. As a compromise the function is now better commented and the interface is a bit easier to use for callers as reflected in the demo.

Once this is approved I'll do a squash merge as the commit history is longer than necessary to keep going forwards.

'TanMid1': {}, # Tangential
'TanPol1': {} # Combined poloidal/tangential
}
# poloidal rotations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the advantages of grouping a property for all cameras, but it is a bit confusing for me to navigate in. I found it more natural to group properties by camera, so that it is simpler to see where the camera is, without need to search through multiple sections of the code.

# Produce a voxel grid
########################################################################
print("Producing the voxel grid...")
# Define the centres of each voxel, as an (nx, ny, 2) array.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest renaming nx to nr and ny to nz. Using ny for number of voxels in vertical direction could cause some confusion as in tokamaks the y axis typically lies in midplane. It also results in somewhat awkward naming in pairs cell_r, cell_dx defined by nx and cell_z, cell_dz defined by ny.

@skuba31 skuba31 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work @jacklovell . I think that the documentation and code readability is significantly improved. The sparse support and ADMT demo are very useful additions. I have added a few minor comments for consideration before final approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants