Python API Reference
####################

This section provides the API reference for the µGrid Python module.
The Python bindings provide access to the core µGrid functionality
with a Pythonic interface.

.. contents:: Table of Contents
   :local:
   :depth: 2

Field Collections
*****************

Field collections manage groups of fields on structured grids.

GlobalFieldCollection
---------------------

.. py:class:: muGrid.GlobalFieldCollection(nb_grid_pts, nb_sub_pts=None, nb_ghosts_left=None, nb_ghosts_right=None, device=None)

   A GlobalFieldCollection manages a set of fields that share the same
   global grid structure. It can allocate fields in either host (CPU)
   or device (GPU) memory.

   :param nb_grid_pts: Grid dimensions, e.g., ``[64, 64]`` for 2D or ``[32, 32, 32]`` for 3D.
   :type nb_grid_pts: sequence of int
   :param nb_sub_pts: Number of sub-points per pixel for each sub-point type. Default is ``{}``.
   :type nb_sub_pts: dict, optional
   :param nb_ghosts_left: Ghost cells on low-index side. Default is no ghosts.
   :type nb_ghosts_left: sequence of int, optional
   :param nb_ghosts_right: Ghost cells on high-index side. Default is no ghosts.
   :type nb_ghosts_right: sequence of int, optional
   :param device: Device for field allocation. Can be a string (``"cpu"``, ``"cuda"``, ``"cuda:N"``,
      ``"rocm"``, ``"rocm:N"``) or a ``Device`` instance. Default is ``"cpu"``.
   :type device: str or Device, optional

   **Example**::

       >>> fc = muGrid.GlobalFieldCollection([64, 64])
       >>> field = fc.real_field("temperature")
       >>> field.p[:] = 300.0  # Set temperature to 300K

       >>> # GPU field collection
       >>> fc_gpu = muGrid.GlobalFieldCollection([64, 64], device="cuda")

   .. py:method:: real_field(name, components=(), sub_pt="pixel")

      Create a real-valued field.

      :param name: Unique name for the field.
      :type name: str
      :param components: Shape of field components. Default is ``()`` for scalar.
      :type components: tuple of int, optional
      :param sub_pt: Sub-point type. Default is ``"pixel"``.
      :type sub_pt: str, optional
      :returns: Wrapped field with ``.s``, ``.p``, ``.sg``, ``.pg`` accessors.
      :rtype: Field

   .. py:method:: complex_field(name, components=(), sub_pt="pixel")

      Create a complex-valued field.

      :param name: Unique name for the field.
      :type name: str
      :param components: Shape of field components. Default is ``()`` for scalar.
      :type components: tuple of int, optional
      :param sub_pt: Sub-point type. Default is ``"pixel"``.
      :type sub_pt: str, optional
      :returns: Wrapped field with ``.s``, ``.p``, ``.sg``, ``.pg`` accessors.
      :rtype: Field

   .. py:method:: int_field(name, components=(), sub_pt="pixel")

      Create an integer field.

      :param name: Unique name for the field.
      :type name: str
      :param components: Shape of field components. Default is ``()`` for scalar.
      :type components: tuple of int, optional
      :param sub_pt: Sub-point type. Default is ``"pixel"``.
      :type sub_pt: str, optional
      :returns: Wrapped field with ``.s``, ``.p``, ``.sg``, ``.pg`` accessors.
      :rtype: Field

   .. py:method:: uint_field(name, components=(), sub_pt="pixel")

      Create an unsigned integer field.

      :param name: Unique name for the field.
      :type name: str
      :param components: Shape of field components. Default is ``()`` for scalar.
      :type components: tuple of int, optional
      :param sub_pt: Sub-point type. Default is ``"pixel"``.
      :type sub_pt: str, optional
      :returns: Wrapped field with ``.s``, ``.p``, ``.sg``, ``.pg`` accessors.
      :rtype: Field

LocalFieldCollection
--------------------

.. py:class:: muGrid.LocalFieldCollection(spatial_dim, name="", nb_sub_pts=None, device=None)

   A LocalFieldCollection manages fields on a subset of pixels, typically
   used for material-specific data in heterogeneous simulations.

   :param spatial_dim: Spatial dimension (2 or 3).
   :type spatial_dim: int
   :param name: Name for the collection. Default is ``""``.
   :type name: str, optional
   :param nb_sub_pts: Number of sub-points per pixel for each sub-point type.
   :type nb_sub_pts: dict, optional
   :param device: Device for field allocation. Can be a string (``"cpu"``, ``"cuda"``, ``"cuda:N"``,
      ``"rocm"``, ``"rocm:N"``) or a ``Device`` instance. Default is ``"cpu"``.
   :type device: str or Device, optional

   The field creation methods (``real_field``, ``complex_field``, ``int_field``, ``uint_field``)
   are the same as for ``GlobalFieldCollection``.

Fields
******

The :class:`Field` class wraps C++ fields with convenient numpy array access.

Field
-----

.. py:class:: muGrid.Field(cpp_field)

   Python wrapper for muGrid fields providing numpy/cupy array views.

   This class wraps a C++ muGrid field and provides the following properties
   for accessing the underlying data as arrays:

   - ``s``: SubPt layout, excluding ghost regions
   - ``sg``: SubPt layout, including ghost regions
   - ``p``: Pixel layout, excluding ghost regions
   - ``pg``: Pixel layout, including ghost regions

   For CPU fields, the arrays are numpy arrays. For GPU fields (CUDA/ROCm),
   the arrays are CuPy arrays. Both are views into the underlying C++ data,
   so modifications to the arrays will modify the field data directly (zero-copy).

   :param cpp_field: The underlying C++ field object.

   .. py:property:: s

      SubPt layout array excluding ghost regions.

      Shape: ``(*components_shape, nb_sub_pts, *spatial_dims_without_ghosts)``

   .. py:property:: sg

      SubPt layout array including ghost regions.

      Shape: ``(*components_shape, nb_sub_pts, *spatial_dims_with_ghosts)``

   .. py:property:: p

      Pixel layout array excluding ghost regions.

      Shape: ``(nb_components * nb_sub_pts, *spatial_dims_without_ghosts)``

   .. py:property:: pg

      Pixel layout array including ghost regions.

      Shape: ``(nb_components * nb_sub_pts, *spatial_dims_with_ghosts)``

   .. py:property:: is_on_gpu

      Check if this field resides on GPU memory.

      :rtype: bool

   .. py:property:: device

      Get the device where this field resides (``'cpu'`` or ``'cuda:N'``).

      :rtype: str

wrap_field
----------

.. py:function:: muGrid.wrap_field(field)

   Wrap a C++ field in a Python Field object.

   :param field: The underlying C++ field.
   :returns: Wrapped field with numpy array access.
   :rtype: Field

Domain Decomposition
********************

Classes for parallel domain decomposition with MPI.

CartesianDecomposition
----------------------

.. py:class:: muGrid.CartesianDecomposition(communicator, nb_domain_grid_pts, nb_subdivisions=None, nb_ghosts_left=None, nb_ghosts_right=None, nb_sub_pts=None, device=None)

   CartesianDecomposition manages domain decomposition for MPI-parallel
   computations on structured grids, including ghost buffer regions for
   stencil operations.

   :param communicator: MPI communicator for parallel execution.
   :type communicator: Communicator
   :param nb_domain_grid_pts: Global domain grid dimensions.
   :type nb_domain_grid_pts: sequence of int
   :param nb_subdivisions: Number of subdivisions in each dimension. Default is automatic.
   :type nb_subdivisions: sequence of int, optional
   :param nb_ghosts_left: Ghost cells on low-index side. Default is no ghosts.
   :type nb_ghosts_left: sequence of int, optional
   :param nb_ghosts_right: Ghost cells on high-index side. Default is no ghosts.
   :type nb_ghosts_right: sequence of int, optional
   :param nb_sub_pts: Number of sub-points per pixel for each sub-point type.
   :type nb_sub_pts: dict, optional
   :param device: Device for field allocation. Can be a string (``"cpu"``, ``"cuda"``, ``"cuda:N"``,
      ``"rocm"``, ``"rocm:N"``) or a ``Device`` instance. Default is ``"cpu"``.
   :type device: str or Device, optional

   **Example**::

       >>> from muGrid import Communicator, CartesianDecomposition
       >>> comm = Communicator()
       >>> decomp = CartesianDecomposition(
       ...     comm,
       ...     nb_domain_grid_pts=[128, 128],
       ...     nb_subdivisions=[1, 1],
       ...     nb_ghosts_left=[1, 1],
       ...     nb_ghosts_right=[1, 1]
       ... )
       >>> field = decomp.real_field("displacement", components=(3,))

   .. py:property:: nb_grid_pts

      Local subdomain grid dimensions (alias for ``nb_subdomain_grid_pts``).

      :rtype: list of int

   .. py:method:: set_nb_sub_pts(sub_pt_type, nb_sub_pts)

      Set the number of sub-points for a given sub-point type.

      :param sub_pt_type: Name of the sub-point type (e.g., ``"quad"``).
      :type sub_pt_type: str
      :param nb_sub_pts: Number of sub-points per pixel for this type.
      :type nb_sub_pts: int

   .. py:method:: communicate_ghosts(field)

      Exchange ghost buffer data for a field.

      :param field: The field whose ghost buffers should be filled from neighbors.
      :type field: Field

   .. py:method:: reduce_ghosts(field)

      Accumulate ghost buffer contributions back to the interior domain.

      This is the adjoint operation of ``communicate_ghosts`` and is needed
      for transpose operations (e.g., divergence) with periodic BCs.
      After the operation, ghost buffers are zeroed.

      :param field: The field whose ghost buffers should be reduced to interior.
      :type field: Field

Communicator
------------

.. py:function:: muGrid.Communicator(communicator=None)

   Factory function for the communicator class.

   :param communicator: The bare MPI communicator (mpi4py or muGrid communicator object).
      Default is a serial communicator containing just the present process.
   :returns: A muGrid communicator object.

   **Example**::

       >>> from muGrid import Communicator
       >>> comm = Communicator()  # Serial communicator
       >>> # Or with MPI:
       >>> from mpi4py import MPI
       >>> comm = Communicator(MPI.COMM_WORLD)

Operators
*********

Discrete operators for stencil-based computations.

ConvolutionOperator
-------------------

.. py:class:: muGrid.GenericLinearOperator(offset, stencil)

   Applies convolution (stencil) operations to fields. Useful for computing
   gradients, Laplacians, and other discrete differential operators.

   :param offset: Offset of the stencil origin relative to the current pixel.
   :type offset: sequence of int
   :param stencil: Stencil coefficients. Shape determines the stencil size.
   :type stencil: array_like

   **Example**::

       >>> # Create a 2D Laplacian stencil
       >>> import numpy as np
       >>> stencil = np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]])
       >>> laplace = muGrid.GenericLinearOperator([-1, -1], stencil)
       >>> laplace.apply(input_field, output_field)

   .. py:method:: apply(nodal_field, quadrature_point_field)

      Apply convolution to fields.

      :param nodal_field: Input field.
      :type nodal_field: Field
      :param quadrature_point_field: Output field.
      :type quadrature_point_field: Field

   .. py:method:: transpose(quadrature_point_field, nodal_field, weights=None)

      Apply transpose convolution to fields.

      :param quadrature_point_field: Input field.
      :type quadrature_point_field: Field
      :param nodal_field: Output field.
      :type nodal_field: Field
      :param weights: Weights for the transpose operation.
      :type weights: sequence of float, optional

LaplaceOperator
---------------

.. py:class:: muGrid.LaplaceOperator(spatial_dim, scale=1.0)

   A hard-coded, optimized Laplacian stencil operator using the standard
   5-point (2D) or 7-point (3D) finite difference stencil.

   :param spatial_dim: Spatial dimension (2 or 3).
   :type spatial_dim: int
   :param scale: Scaling factor for the Laplacian. Default is ``1.0``.
   :type scale: float, optional

   **Example**::

       >>> laplace = muGrid.LaplaceOperator(2, scale=-1.0)
       >>> laplace.apply(input_field, output_field)

   .. py:method:: apply(input_field, output_field)

      Apply the Laplacian operator.

      :param input_field: Input field.
      :type input_field: Field
      :param output_field: Output field.
      :type output_field: Field

   .. py:method:: apply_increment(input_field, alpha, output_field)

      Apply Laplacian and add scaled result to output: ``output += alpha * L(input)``.

      :param input_field: Input field.
      :type input_field: Field
      :param alpha: Scaling factor.
      :type alpha: float
      :param output_field: Output field (updated in-place).
      :type output_field: Field

   .. py:method:: transpose(input_field, output_field, weights=None)

      Apply transpose operator. For Laplacian, this is the same as apply.

      :param input_field: Input field.
      :type input_field: Field
      :param output_field: Output field.
      :type output_field: Field
      :param weights: Weights (unused for Laplacian, included for API compatibility).
      :type weights: sequence of float, optional

FEMGradientOperator
-------------------

.. py:class:: muGrid.FEMGradientOperator(spatial_dim, grid_spacing=None)

   A hard-coded, optimized gradient operator using linear finite element
   shape functions on triangles (2D) or tetrahedra (3D).

   :param spatial_dim: Spatial dimension (2 or 3).
   :type spatial_dim: int
   :param grid_spacing: Grid spacing in each direction. Default is ``[1.0, ...]`` for each dimension.
   :type grid_spacing: sequence of float, optional

   **Example**::

       >>> grad = muGrid.FEMGradientOperator(2)
       >>> grad.apply(nodal_field, quadrature_point_gradient_field)

   .. py:method:: apply(nodal_field, quadrature_point_field)

      Apply the gradient operator (nodal values → quadrature point gradients).

      :param nodal_field: Input field at nodal points.
      :type nodal_field: Field
      :param quadrature_point_field: Output field at quadrature points.
      :type quadrature_point_field: Field

   .. py:method:: apply_increment(nodal_field, alpha, quadrature_point_field)

      Apply gradient and add scaled result to output: ``output += alpha * grad(input)``.

      :param nodal_field: Input field at nodal points.
      :type nodal_field: Field
      :param alpha: Scaling factor.
      :type alpha: float
      :param quadrature_point_field: Output field at quadrature points (updated in-place).
      :type quadrature_point_field: Field

   .. py:method:: transpose(quadrature_point_field, nodal_field, weights=None)

      Apply transpose (divergence) operator (quadrature points → nodal values).

      :param quadrature_point_field: Input field at quadrature points.
      :type quadrature_point_field: Field
      :param nodal_field: Output field at nodal points.
      :type nodal_field: Field
      :param weights: Weights for the transpose operation.
      :type weights: sequence of float, optional

   .. py:method:: transpose_increment(quadrature_point_field, alpha, nodal_field, weights=None)

      Apply transpose and add scaled result: ``output += alpha * div(input)``.

      :param quadrature_point_field: Input field at quadrature points.
      :type quadrature_point_field: Field
      :param alpha: Scaling factor.
      :type alpha: float
      :param nodal_field: Output field at nodal points (updated in-place).
      :type nodal_field: Field
      :param weights: Weights for the transpose operation.
      :type weights: sequence of float, optional

IsotropicStiffnessOperator
--------------------------

.. py:class:: muGrid.IsotropicStiffnessOperator2D(grid_spacing)
              muGrid.IsotropicStiffnessOperator3D(grid_spacing)

   Fused stiffness operators for isotropic linear elastic materials.

   These operators compute ``K @ u = B^T C B @ u`` for linear finite elements
   without storing the full stiffness matrix. Instead, they exploit the
   isotropic material structure:

   .. math::

      K = 2\mu G + \lambda V

   where G and V are geometry-only matrices (same for all voxels) and
   λ, μ are the Lamé parameters which can vary spatially.

   This reduces memory from O(N × 24²) for full K storage to O(N × 2)
   for spatially-varying isotropic materials, plus O(1) for the shared
   geometry matrices.

   :param grid_spacing: Grid spacing in each direction ``[hx, hy]`` (2D) or ``[hx, hy, hz]`` (3D).
   :type grid_spacing: sequence of float

   **Example**::

       >>> import numpy as np
       >>> import muGrid
       >>>
       >>> # Create 3D operator
       >>> stiffness = muGrid.IsotropicStiffnessOperator3D([0.1, 0.1, 0.1])
       >>>
       >>> # Setup fields (displacement at nodes, materials at elements)
       >>> fc = muGrid.CartesianDecomposition(
       ...     muGrid.Communicator(),
       ...     nb_domain_grid_pts=[32, 32, 32],
       ...     nb_ghosts_left=[1, 1, 1],
       ...     nb_ghosts_right=[1, 1, 1],
       ... )
       >>> u = fc.real_field("displacement", (3,))
       >>> f = fc.real_field("force", (3,))
       >>>
       >>> # Material parameters (one fewer element in each direction)
       >>> lam = fc.real_field("lambda")  # Lamé first parameter
       >>> mu = fc.real_field("mu")       # Shear modulus
       >>> lam.p[...] = 1.0  # Uniform material
       >>> mu.p[...] = 1.0
       >>>
       >>> # Apply stiffness: f = K @ u
       >>> fc.communicate_ghosts(u)
       >>> stiffness.apply(u, lam, mu, f)

   .. py:method:: apply(displacement, lam, mu, force)

      Apply the stiffness operator: ``force = K @ displacement``.

      :param displacement: Input displacement field at nodal points.
      :type displacement: Field
      :param lam: Lamé first parameter field at element centers.
      :type lam: Field
      :param mu: Shear modulus field at element centers.
      :type mu: Field
      :param force: Output force field at nodal points.
      :type force: Field

   .. py:method:: apply_increment(displacement, lam, mu, alpha, force)

      Apply stiffness and add scaled result: ``force += alpha * K @ displacement``.

      :param displacement: Input displacement field at nodal points.
      :type displacement: Field
      :param lam: Lamé first parameter field at element centers.
      :type lam: Field
      :param mu: Shear modulus field at element centers.
      :type mu: Field
      :param alpha: Scaling factor.
      :type alpha: float
      :param force: Output force field at nodal points (updated in-place).
      :type force: Field

FFT Engine
**********

Distributed FFT operations using pencil decomposition.

FFTEngine
---------

.. py:class:: muGrid.FFTEngine(nb_domain_grid_pts, communicator=None, nb_ghosts_left=None, nb_ghosts_right=None, nb_sub_pts=None)

   The FFTEngine provides distributed FFT operations on structured grids
   with MPI parallelization using pencil (2D) decomposition.

   :param nb_domain_grid_pts: Global grid dimensions ``[Nx, Ny]`` or ``[Nx, Ny, Nz]``.
   :type nb_domain_grid_pts: sequence of int
   :param communicator: MPI communicator. Default is serial execution.
   :type communicator: Communicator, optional
   :param nb_ghosts_left: Ghost cells on low-index side of each dimension.
   :type nb_ghosts_left: sequence of int, optional
   :param nb_ghosts_right: Ghost cells on high-index side of each dimension.
   :type nb_ghosts_right: sequence of int, optional
   :param nb_sub_pts: Number of sub-points per pixel.
   :type nb_sub_pts: dict, optional

   **Example**::

       >>> engine = muGrid.FFTEngine([64, 64])
       >>> real_field = engine.real_space_field("displacement", nb_components=3)
       >>> fourier_field = engine.fourier_space_field("displacement_k", nb_components=3)
       >>> engine.fft(real_field, fourier_field)
       >>> engine.ifft(fourier_field, real_field)
       >>> real_field.s[:] *= engine.normalisation

   .. py:method:: fft(input_field, output_field)

      Forward FFT: real space → Fourier space.

      The transform is unnormalized. To recover original data after
      ``ifft(fft(x))``, multiply by ``normalisation``.

      :param input_field: Real-space field (must be in this engine's real collection).
      :type input_field: Field
      :param output_field: Fourier-space field (must be in this engine's Fourier collection).
      :type output_field: Field

   .. py:method:: ifft(input_field, output_field)

      Inverse FFT: Fourier space → real space.

      The transform is unnormalized. To recover original data after
      ``ifft(fft(x))``, multiply by ``normalisation``.

      :param input_field: Fourier-space field (must be in this engine's Fourier collection).
      :type input_field: Field
      :param output_field: Real-space field (must be in this engine's real collection).
      :type output_field: Field

   .. py:method:: register_real_space_field(name, nb_components=1)

      Register a new real-space field.

      Raises an error if a field with the given name already exists.

      :param name: Unique field name.
      :type name: str
      :param nb_components: Number of components. Default is 1.
      :type nb_components: int, optional
      :returns: Wrapped real-valued field with array accessors.
      :rtype: Field
      :raises RuntimeError: If a field with the given name already exists.

   .. py:method:: register_fourier_space_field(name, nb_components=1)

      Register a new Fourier-space field.

      Raises an error if a field with the given name already exists.

      :param name: Unique field name.
      :type name: str
      :param nb_components: Number of components. Default is 1.
      :type nb_components: int, optional
      :returns: Wrapped complex-valued field with array accessors.
      :rtype: Field
      :raises RuntimeError: If a field with the given name already exists.

   .. py:method:: real_space_field(name, nb_components=1)

      Get or create a real-space field for FFT operations.

      If a field with the given name already exists, returns it.
      Otherwise creates a new field with the specified number of components.

      :param name: Unique field name.
      :type name: str
      :param nb_components: Number of components. Default is 1.
      :type nb_components: int, optional
      :returns: Wrapped real-valued field with array accessors.
      :rtype: Field

   .. py:method:: fourier_space_field(name, nb_components=1)

      Get or create a Fourier-space field for FFT operations.

      If a field with the given name already exists, returns it.
      Otherwise creates a new field with the specified number of components.

      :param name: Unique field name.
      :type name: str
      :param nb_components: Number of components. Default is 1.
      :type nb_components: int, optional
      :returns: Wrapped complex-valued field with array accessors.
      :rtype: Field

   .. py:attribute:: normalisation

      Normalization factor for FFT. Multiply by this after ``ifft(fft(x))``
      to recover the original data.

      :type: float

   .. py:attribute:: spatial_dim

      Spatial dimension of the grid (2 or 3).

      :type: int

   .. py:attribute:: fftfreq

      Normalized FFT frequencies for the local Fourier subdomain.

      Returns an array of shape ``[dim, local_fx, local_fy, ...]`` where each
      element is the normalized frequency in range ``[-0.5, 0.5)``.

      For MPI-parallel runs, this returns only the frequencies for the local
      subdomain owned by this rank.

      :type: numpy.ndarray

   .. py:attribute:: ifftfreq

      Integer FFT frequency indices for the local Fourier subdomain.

      Returns an array of shape ``[dim, local_fx, local_fy, ...]`` where each
      element is the integer frequency index.

      :type: numpy.ndarray

   .. py:attribute:: coords

      Normalized real-space coordinates for the local subdomain (excluding ghost cells).

      Returns an array of shape ``[dim, local_nx, local_ny, ...]`` where each
      element is the normalized coordinate in range ``[0, 1)``.

      :type: numpy.ndarray

   .. py:attribute:: icoords

      Integer real-space coordinate indices for the local subdomain (excluding ghost cells).

      Returns an array of shape ``[dim, local_nx, local_ny, ...]`` where each
      element is the integer coordinate index.

      :type: numpy.ndarray

   .. py:attribute:: coordsg

      Normalized real-space coordinates including ghost cells.

      Same as ``coords`` but includes ghost cells if configured.

      :type: numpy.ndarray

   .. py:attribute:: icoordsg

      Integer real-space coordinate indices including ghost cells.

      Same as ``icoords`` but includes ghost cells if configured.

      :type: numpy.ndarray

FFT Utilities
-------------

.. py:function:: muGrid.fft_normalization(nb_grid_pts)

   Return FFT normalization factor.

   :param nb_grid_pts: Number of grid points in each dimension.
   :type nb_grid_pts: sequence of int
   :returns: Normalization factor.
   :rtype: float

.. py:function:: muGrid.get_hermitian_grid_pts(nb_grid_pts)

   Return the number of grid points in the Hermitian (rfft) representation.

   :param nb_grid_pts: Number of grid points in each dimension.
   :type nb_grid_pts: sequence of int
   :returns: Number of Hermitian grid points.
   :rtype: tuple of int

File I/O
********

Classes for reading and writing fields to NetCDF files.

FileIONetCDF
------------

.. py:class:: muGrid.FileIONetCDF(file_name, open_mode="read", communicator=None)

   Provides NetCDF file I/O for muGrid fields with optional MPI support.

   :param file_name: Path to the NetCDF file.
   :type file_name: str
   :param open_mode: File open mode: ``"read"``, ``"write"``, ``"overwrite"``, or ``"append"``. Default is ``"read"``.
   :type open_mode: str or OpenMode, optional
   :param communicator: MPI communicator for parallel I/O. Default is serial.
   :type communicator: Communicator, optional

   **Example**::

       >>> file = muGrid.FileIONetCDF("output.nc", open_mode="overwrite")
       >>> file.register_field_collection(field_collection)
       >>> file.append_frame().write()

   .. py:method:: register_field_collection(collection)

      Register a field collection for I/O.

      :param collection: The field collection to register. If a ``CartesianDecomposition`` is
         passed, its underlying field collection is used.
      :type collection: GlobalFieldCollection, LocalFieldCollection, or CartesianDecomposition

   .. py:method:: append_frame()

      Append a new frame to the file.

      :returns: Frame object for writing.

   .. py:attribute:: OpenMode

      Enum for file open modes: ``Read``, ``Write``, ``Overwrite``, ``Append``.

Utilities
*********

Timer
-----

.. py:class:: muGrid.Timer()

   Hierarchical timer utility for performance measurement with context manager support.

   The Timer supports nested timing regions and can be used as a context manager
   for convenient timing of code blocks.

   **Example**::

       >>> timer = muGrid.Timer()
       >>> with timer("outer"):
       ...     with timer("inner"):
       ...         # ... some computation ...
       ...         pass
       >>> timer.print_summary()
       Timer Summary:
         outer: 0.1234s (1 calls)
           inner: 0.0567s (1 calls)

   .. py:method:: __call__(name)

      Context manager for timing a named code block.

      :param name: Name of the timing region.
      :type name: str
      :returns: Context manager that times the enclosed block.

   .. py:method:: get_time(name)

      Get total elapsed time for a timing region.

      :param name: Name of the timing region (use ``"/"`` for nested regions,
         e.g., ``"outer/inner"``).
      :type name: str
      :returns: Total elapsed time in seconds.
      :rtype: float

   .. py:method:: get_calls(name)

      Get the number of times a timing region was entered.

      :param name: Name of the timing region.
      :type name: str
      :returns: Number of calls.
      :rtype: int

   .. py:method:: print_summary()

      Print a formatted summary of all timing regions.

   .. py:method:: to_dict()

      Export timer data as a dictionary (useful for JSON serialization).

      :returns: Dictionary with timing data.
      :rtype: dict

Solvers
-------

.. py:module:: muGrid.Solvers

The Solvers module provides simple parallel iterative solvers.

.. py:function:: muGrid.Solvers.conjugate_gradients(comm, fc, b, x, hessp, tol=1e-6, maxiter=1000, callback=None, timer=None)

   Conjugate gradient method for matrix-free solution of the linear problem
   ``Ax = b``, where ``A`` is represented by the function ``hessp`` (which computes the
   product of ``A`` with a vector). The method iteratively refines the solution ``x``
   until the residual ``||Ax - b||`` is less than ``tol`` or until ``maxiter`` iterations
   are reached.

   :param comm: Communicator for parallel processing.
   :type comm: muGrid.Communicator
   :param fc: Collection for creating temporary fields used by the CG algorithm.
   :type fc: muGrid.GlobalFieldCollection, muGrid.LocalFieldCollection, or muGrid.CartesianDecomposition
   :param b: Right-hand side vector.
   :type b: muGrid.Field
   :param x: Initial guess for the solution (modified in place).
   :type x: muGrid.Field
   :param hessp: Function that computes the product of the Hessian matrix ``A`` with a vector.
      Signature: ``hessp(input_field, output_field)`` where both are ``muGrid.Field``.
   :type hessp: callable
   :param tol: Tolerance for convergence. Default is ``1e-6``.
   :type tol: float, optional
   :param maxiter: Maximum number of iterations. Default is ``1000``.
   :type maxiter: int, optional
   :param callback: Function to call after each iteration with signature:
      ``callback(iteration, state_dict)`` where ``state_dict`` contains keys
      ``"x"``, ``"r"``, ``"p"``, and ``"rr"`` (squared residual norm).
   :type callback: callable, optional
   :param timer: Timer object for performance profiling. If provided, the solver
      will record timing for various operations (hessp, dot products, updates).
   :type timer: muGrid.Timer, optional
   :returns: Solution to the system ``Ax = b`` (same as input field ``x``).
   :rtype: muGrid.Field
   :raises RuntimeError: If the algorithm does not converge within ``maxiter`` iterations,
      or if the Hessian is not positive definite.

Device Selection
****************

The ``Device`` class and ``DeviceType`` enum provide a Pythonic way to specify
where field data should be allocated.

Device
------

.. py:class:: muGrid.Device

   Represents a compute device (CPU or GPU) for field allocation.

   Factory methods provide convenient construction:

   .. py:staticmethod:: cpu()

      Create a CPU device.

      :returns: A Device representing the CPU.
      :rtype: Device

   .. py:staticmethod:: cuda(device_id=0)

      Create a CUDA GPU device.

      :param device_id: GPU device ID. Default is 0.
      :type device_id: int, optional
      :returns: A Device representing a CUDA GPU.
      :rtype: Device

   .. py:staticmethod:: rocm(device_id=0)

      Create a ROCm GPU device.

      :param device_id: GPU device ID. Default is 0.
      :type device_id: int, optional
      :returns: A Device representing a ROCm GPU.
      :rtype: Device

   .. py:staticmethod:: gpu(device_id=0)

      Create a GPU device using the default backend.

      Automatically selects the available GPU backend:

      - Returns CUDA device if CUDA is available
      - Returns ROCm device if ROCm is available (and CUDA is not)
      - Returns CPU device if no GPU backend is available

      This is the recommended way to request GPU execution without
      hard-coding a specific backend.

      :param device_id: GPU device ID. Default is 0.
      :type device_id: int, optional
      :returns: A Device for the default GPU backend.
      :rtype: Device

   .. py:method:: is_host()

      Check if this is a host (CPU) device.

      :returns: ``True`` if CPU device, ``False`` otherwise.
      :rtype: bool

   .. py:method:: is_device()

      Check if this is a device (GPU) memory location.

      :returns: ``True`` if GPU device, ``False`` otherwise.
      :rtype: bool

   .. py:method:: get_type()

      Get the device type.

      :returns: The device type enum value.
      :rtype: DeviceType

   .. py:method:: get_device_id()

      Get the device ID for multi-GPU systems.

      :returns: Device ID (0 for single-GPU or CPU).
      :rtype: int

   **Example**::

       >>> import muGrid
       >>> # Create devices
       >>> cpu = muGrid.Device.cpu()
       >>> gpu0 = muGrid.Device.cuda()
       >>> gpu1 = muGrid.Device.cuda(1)
       >>> # Check device type
       >>> cpu.is_host()
       True
       >>> gpu0.is_device()
       True
       >>> # Use with field collections
       >>> fc = muGrid.GlobalFieldCollection([64, 64], device=muGrid.Device.cuda())

DeviceType
----------

.. py:class:: muGrid.DeviceType

   Enumeration for device types. Values follow DLPack conventions.

   .. py:attribute:: CPU

      CPU device (value 1).

   .. py:attribute:: CUDA

      NVIDIA CUDA GPU device (value 2).

   .. py:attribute:: CUDAHost

      CUDA pinned host memory (value 3).

   .. py:attribute:: ROCm

      AMD ROCm GPU device (value 10).

   .. py:attribute:: ROCmHost

      ROCm pinned host memory (value 11).

Enumerations
************

.. py:class:: muGrid.IterUnit

   Enumeration for iteration unit types.

   .. py:attribute:: Pixel

      Iterate over pixels.

   .. py:attribute:: SubPt

      Iterate over sub-points.

.. py:class:: muGrid.StorageOrder

   Enumeration for array storage order.

   .. py:attribute:: ColMajor

      Column-major (Fortran) storage order.

   .. py:attribute:: RowMajor

      Row-major (C) storage order.

Module Constants
****************

The following constants indicate compile-time configuration:

.. py:data:: muGrid.has_mpi

   ``True`` if MPI support is enabled.

.. py:data:: muGrid.has_cuda

   ``True`` if CUDA GPU support is compiled in.

.. py:data:: muGrid.has_rocm

   ``True`` if ROCm/HIP GPU support is compiled in.

.. py:data:: muGrid.has_gpu

   ``True`` if any GPU support is available.

.. py:data:: muGrid.has_netcdf

   ``True`` if NetCDF I/O support is available.
