<commit_before>
"""
The base image interface.
"""

import numpy as np
from scipy.ndimage import map_coordinates

# Local imports
from nipy.core.transforms.affines import from_matrix_vector, \
                     to_matrix_vector
from nipy.core.api import AffineTransform, Image, CoordinateSystem
from nipy.core.reference.coordinate_map import compose, product as cmap_product
from nipy.algorithms.resample import resample

################################################################################
# class `LPITransform`
################################################################################

class LPITransform(AffineTransform):


   range = CoordinateSystem('xyz', name='world-LPI')

   def __init__(self, affine, lpi_axis_names):

      """
      >>> lpi = LPITransform(np.diag([3,4,5,1]), 'ijk')
      >>> lpi
      LPITransform(
         function_domain=CoordinateSystem(coord_names=('i', 'j', 'k'), name='voxel', coord_dtype=float64),
         function_range=CoordinateSystem(coord_names=('x', 'y', 'z'), name='world-LPI', coord_dtype=float64),
         affine=array([[ 3.,  0.,  0.,  0.],
                       [ 0.,  4.,  0.,  0.],
                       [ 0.,  0.,  5.,  0.],
                       [ 0.,  0.,  0.,  1.]])
      )
      >>> 
      """

      if affine.shape != (4,4):
          raise ValueError('affine must be a 4x4 matrix representing an affine transformation in homogeneous coordinates')
      domain = CoordinateSystem(lpi_axis_names, name='voxel')
      range = CoordinateSystem('xyz', name='world')
      AffineTransform.__init__(self, domain, self.range, affine)

   def reordered_range(self, order, name=''):
       raise NotImplementedError("the LPI world coordinates are always ['x','y','z'] and can't be reordered")

   def renamed_range(self, newnames, name=''):
       raise NotImplementedError("the LPI world coordinates are always ['x','y','z'] and can't be renamed")

   def __repr__(self):
       s_split = AffineTransform.__repr__(self).split('\n')
       s_split[0] = 'LPITransform('
       return '\n'.join(s_split)
       
################################################################################
# class `LPIImage`
################################################################################

class LPIImage(Image):

   """ The standard image for nipy, an Image with
       LPI output coordinates.

       This object is a subclass of Image that
       assumes the first 3 coordinates
       are spatial. 

       **Attributes**

       :metadata: dictionary

           Optional, user-defined, dictionnary used to carry around
           extra information about the data as it goes through
           transformations. The Image class does not garanty consistency
           of this information as the data is modified.

       :_data: 

           Private pointer to the data.

       **Properties**

       :affine: 4x4 ndarray

           Affine mapping from voxel axes to world coordinates
           (world coordinates are always forced to be 'x', 'y', 'z').

       :lpi_transform: LPITransform

           A CoordinateMap that relates all the spatial axes of the data
           to LPI 'xyz' coordinates.

       :coordmap: AffineTransform

           Coordinate map describing the relationship between
           all coordinates and axis_names.

       :axes: CoordinateSystem
  
           CoordinateSystem for the axes of the data.

       :world: CoordinateSystem
 
           CoordinateSystem for the world space of the data.


   Notes
   -----

   The data is stored in an undefined way: prescalings might need to
   be applied to it before using it, or the data might be loaded on
   demand. The best practice to access the data is not to access the
   _data attribute, but to use the `get_data` method.

   >>> data = np.empty((30,40,50))
   >>> affine = np.diag([3,4,5,1])
   >>> im = LPIImage(data, affine, 'ijk')
   >>> im.world
   CoordinateSystem(coord_names=('x', 'y', 'z'), name='world-LPI', coord_dtype=float64)
   >>> im.axes
   CoordinateSystem(coord_names=('i', 'j', 'k'), name='voxel', coord_dtype=float64)
   >>> im.lpi_transform
   LPITransform(
      function_domain=CoordinateSystem(coord_names=('i', 'j', 'k'), name='voxel', coord_dtype=float64),
      function_range=CoordinateSystem(coord_names=('x', 'y', 'z'), name='world-LPI', coord_dtype=float64),
      affine=array([[ 3.,  0.,  0.,  0.],
                    [ 0.,  4.,  0.,  0.],
                    [ 0.,  0.,  5.,  0.],
                    [ 0.,  0.,  0.,  1.]])
   )
   >>> 

   """



   #---------------------------------------------------------------------------
   # Attributes
   #---------------------------------------------------------------------------
   
   # User defined meta data
   metadata = dict()

   # The data (ndarray)
   _data = None

   # XXX: Need an attribute to determine in a clever way the
   # interplation order/method

   def __init__(self, data, affine, axis_names, metadata={}):
      """ Creates a new nipy image with an affine mapping.

      Parameters
      ----------

      data : ndarray
         ndarray representing the data.

      affine : 4x4 ndarray
         affine transformation to the reference coordinate system

      axis_names : [string]
         names of the axes in the coordinate system.
      """

      if len(axis_names) < 3:
         raise ValueError('LPIImage must have a minimum of 3 axes')

      # The first three axes are assumed to be the
      # spatial ones
      lpi_transform = LPITransform(affine, axis_names[:3])
      nonspatial_names = axis_names[3:]
        
      if nonspatial_names:
         nonspatial_affine_transform = AffineTransform.from_start_step(nonspatial_names, nonspatial_names, [0]*(data.ndim-3), [1]*(data.ndim-3))
         full_dimensional_affine_transform = cmap_product(lpi_transform, nonspatial_affine_transform)
      else:
         full_dimensional_affine_transform = lpi_transform 

      self._lpi_transform = lpi_transform

      Image.__init__(self, data, full_dimensional_affine_transform,
                     metadata=metadata)

   #---------------------------------------------------------------------------
   # Overwriting some parts of the Image interface
   #---------------------------------------------------------------------------

   def _getworld(self):
      return self.lpi_transform.function_range # == LPITransform.range
   world = property(_getworld, doc="World space.")

   def _get_affine(self):
      """
      Returns the affine of the LPITransform which will
      always be a 4x4 matrix.
      """
      return self.lpi_transform.affine
   affine = property(_get_affine, doc="4x4 Affine matrix")

   def reordered_world(self, order):
      raise NotImplementedError("the LPI world coordinates are always ['x','y','z'] and can't be reordered")

   def reordered_axes(self, order=None):

      """
      Return a new LPIImage whose axes have been reordered.
      The reordering must be such that the first 3
      coordinates remain the same.

      Parameters
      ----------

      order : sequence
          Order to use, defaults to reverse. The elements
          can be integers, strings or 2-tuples of strings.
          If they are strings, they should be in <commit_msg>adding increasing phrasing to doc of LPIImage<commit_after>
"""
The base image interface.
"""

import numpy as np
from scipy.ndimage import map_coordinates

# Local imports
from nipy.core.transforms.affines import from_matrix_vector, \
                     to_matrix_vector
from nipy.core.api import AffineTransform, Image, CoordinateSystem
from nipy.core.reference.coordinate_map import compose, product as cmap_product
from nipy.algorithms.resample import resample

################################################################################
# class `LPITransform`
################################################################################

class LPITransform(AffineTransform):


   range = CoordinateSystem('xyz', name='world-LPI')

   def __init__(self, affine, lpi_axis_names):

      """
      >>> lpi = LPITransform(np.diag([3,4,5,1]), 'ijk')
      >>> lpi
      LPITransform(
         function_domain=CoordinateSystem(coord_names=('i', 'j', 'k'), name='voxel', coord_dtype=float64),
         function_range=CoordinateSystem(coord_names=('x', 'y', 'z'), name='world-LPI', coord_dtype=float64),
         affine=array([[ 3.,  0.,  0.,  0.],
                       [ 0.,  4.,  0.,  0.],
                       [ 0.,  0.,  5.,  0.],
                       [ 0.,  0.,  0.,  1.]])
      )
      >>> 
      """

      if affine.shape != (4,4):
          raise ValueError('affine must be a 4x4 matrix representing an affine transformation in homogeneous coordinates')
      domain = CoordinateSystem(lpi_axis_names, name='voxel')
      range = CoordinateSystem('xyz', name='world')
      AffineTransform.__init__(self, domain, self.range, affine)

   def reordered_range(self, order, name=''):
       raise NotImplementedError("the LPI world coordinates are always ['x','y','z'] and can't be reordered")

   def renamed_range(self, newnames, name=''):
       raise NotImplementedError("the LPI world coordinates are always ['x','y','z'] and can't be renamed")

   def __repr__(self):
       s_split = AffineTransform.__repr__(self).split('\n')
       s_split[0] = 'LPITransform('
       return '\n'.join(s_split)
       
################################################################################
# class `LPIImage`
################################################################################

class LPIImage(Image):

   """ The standard image for nipy, an Image with
       LPI output coordinates.

       This object is a subclass of Image that
       assumes the first 3 coordinates
       are spatial. 

       **Attributes**

       :metadata: dictionary

           Optional, user-defined, dictionnary used to carry around
           extra information about the data as it goes through
           transformations. The Image class does not garanty consistency
           of this information as the data is modified.

       :_data: 

           Private pointer to the data.

       **Properties**

       :affine: 4x4 ndarray

           Affine mapping from voxel axes to world coordinates
           (world coordinates are always forced to be 'x', 'y', 'z').

       :lpi_transform: LPITransform

           A CoordinateMap that relates all the spatial axes of the data
           to LPI 'xyz' coordinates.

       :coordmap: AffineTransform

           Coordinate map describing the relationship between
           all coordinates and axis_names.

       :axes: CoordinateSystem
  
           CoordinateSystem for the axes of the data.

       :world: CoordinateSystem
 
           CoordinateSystem for the world space of the data.


   Notes
   -----

   The data is stored in an undefined way: prescalings might need to
   be applied to it before using it, or the data might be loaded on
   demand. The best practice to access the data is not to access the
   _data attribute, but to use the `get_data` method.

   >>> data = np.empty((30,40,50))
   >>> affine = np.diag([3,4,5,1])
   >>> im = LPIImage(data, affine, 'ijk')
   >>> im.world
   CoordinateSystem(coord_names=('x', 'y', 'z'), name='world-LPI', coord_dtype=float64)
   >>> im.axes
   CoordinateSystem(coord_names=('i', 'j', 'k'), name='voxel', coord_dtype=float64)
   >>> im.lpi_transform
   LPITransform(
      function_domain=CoordinateSystem(coord_names=('i', 'j', 'k'), name='voxel', coord_dtype=float64),
      function_range=CoordinateSystem(coord_names=('x', 'y', 'z'), name='world-LPI', coord_dtype=float64),
      affine=array([[ 3.,  0.,  0.,  0.],
                    [ 0.,  4.,  0.,  0.],
                    [ 0.,  0.,  5.,  0.],
                    [ 0.,  0.,  0.,  1.]])
   )
   >>> 

   """



   #---------------------------------------------------------------------------
   # Attributes
   #---------------------------------------------------------------------------
   
   # User defined meta data
   metadata = dict()

   # The data (ndarray)
   _data = None

   # XXX: Need an attribute to determine in a clever way the
   # interplation order/method

   def __init__(self, data, affine, axis_names, metadata={}):
      """ Creates a new nipy image with an affine mapping.

      Parameters
      ----------

      data : ndarray
         ndarray representing the data.

      affine : 4x4 ndarray
         affine transformation to the reference coordinate system

      axis_names : [string]
         names of the axes in the coordinate system.
      """

      if len(axis_names) < 3:
         raise ValueError('LPIImage must have a minimum of 3 axes')

      # The first three axes are assumed to be the
      # spatial ones
      lpi_transform = LPITransform(affine, axis_names[:3])
      nonspatial_names = axis_names[3:]
        
      if nonspatial_names:
         nonspatial_affine_transform = AffineTransform.from_start_step(nonspatial_names, nonspatial_names, [0]*(data.ndim-3), [1]*(data.ndim-3))
         full_dimensional_affine_transform = cmap_product(lpi_transform, nonspatial_affine_transform)
      else:
         full_dimensional_affine_transform = lpi_transform 

      self._lpi_transform = lpi_transform

      Image.__init__(self, data, full_dimensional_affine_transform,
                     metadata=metadata)

   #---------------------------------------------------------------------------
   # Overwriting some parts of the Image interface
   #---------------------------------------------------------------------------

   def _getworld(self):
      return self.lpi_transform.function_range # == LPITransform.range
   world = property(_getworld, doc="World space.")
   _doc['world'] = "The world space is LPI world space, sometimes"+\
                   "referred to as RAS world space. In this world"+\
                   "space, x increases left to right, y increases"+\
                   "posterior to anterior, z increases superior to inferior."

   def _get_affine(self):
      """
      Returns the affine of the LPITransform which will
      always be a 4x4 matrix.
      """
      return self.lpi_transform.affine
   affine = property(_get_affine, doc="4x4 Affine matrix")

   def reordered_world(self, order):
      raise NotImplementedError("the LPI world coordinates are always ['x','y','z'] and can't be reordered")

   def reordered_axes(self, order=None):

      """
      Return a new LPIImage whose axes have been reordered.
      The reordering must be such that the first 3
      coordinates remain the same.

      Parameters
      ----------

      order : sequence
          Order to use, defaults to reverse. The elements
          can be integers, strings or 2-tuples of strings.
          If they are strings, they should be in 
{'fraction_non_alphanumeric': 0.18073585794040492, 'fraction_numerical': 0.0110311427994888, 'mean_word_length': 2.3269187737748935, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5189330', 'n_tokens_mistral': 4268, 'n_tokens_neox': 3973, 'n_words': 1248}