coveralls: 
[![Coverage Status](https://coveralls.io/builds/10809685/badge)](https://coveralls.io/builds/10809685)

Coverage increased (+0.5%) to 75.646% when pulling **ff8d94bd9539a608b8588295309b45ac9a9ffb5d on 45_twix_transform** into **964f2460e46378c29e78d280f999128f34e829df on master**.

diff --git a/suspect/_transforms.py b/suspect/_transforms.py
index 78bc68a..caddbfe 100644
--- a/suspect/_transforms.py
+++ b/suspect/_transforms.py
@@ -38,3 +38,33 @@ def transformation_matrix(x_vector, y_vector, translation, spacing):
         for j in range(4):
             matrix[i, j] *= spacing[j]
     return matrix
+
+
+def rotation_matrix(angle, axis):
+    """
+    Creates a 3x3 matrix which rotates `angle` radians around `axis`
+    
+    Parameters
+    ----------
+    angle : float
+        The angle in radians to rotate around the axis
+    axis : array
+        The unit vector around which to rotate
+        
+    Returns
+    -------
+    matrix : array
+    """
+    c = numpy.cos(angle)
+    s = numpy.sin(angle)
+    matrix = numpy.zeros((3, 3))
+    matrix[0, 0] = c + axis[0] ** 2 * (1 - c)
+    matrix[0, 1] = axis[0] * axis[1] * (1 - c) - axis[2] * s
+    matrix[0, 2] = axis[0] * axis[2] * (1 - c) + axis[1] * s
+    matrix[1, 0] = axis[1] * axis[0] * (1 - c) + axis[2] * s
+    matrix[1, 1] = c + axis[1] ** 2 * (1 - c)
+    matrix[1, 2] = axis[1] * axis[2] * (1 - c) - axis[0] * s
+    matrix[2, 0] = axis[2] * axis[0] * (1 - c) - axis[1] * s
+    matrix[2, 1] = axis[2] * axis[1] * (1 - c) + axis[0] * s
+    matrix[2, 2] = c + axis[2] ** 2 * (1 - c)
+    return matrix
diff --git a/suspect/_version.py b/suspect/_version.py
index d3ec452..acafb4d 100644
--- a/suspect/_version.py
+++ b/suspect/_version.py
@@ -1,1 +1,1 @@
-__version__ = "0.2.0"
+__version__ = "0.3.0a"
diff --git a/suspect/io/twix.py b/suspect/io/twix.py
index 6f5c335..9daa82b 100644
--- a/suspect/io/twix.py
+++ b/suspect/io/twix.py
@@ -1,7 +1,8 @@
-from suspect import MRSData
+from suspect import MRSData, transformation_matrix, rotation_matrix
 
 import struct
 import numpy
+#import quaternion
 import re
 
 # This file largely relies on information from Siemens regarding the structure
@@ -58,13 +59,25 @@ class TwixBuilder(object):
             "patient_id": self.header_params["patient_id"],
             "patient_birthdate": self.header_params["patient_birthdate"]
         }
-        mrs_data = MRSData(data, self.header_params["dt"], self.header_params["f0"], metadata=metadata)
+        mrs_data = MRSData(data,
+                           self.header_params["dt"],
+                           self.header_params["f0"],
+                           metadata=metadata,
+                           transform=self.header_params["transform"])
 
         return mrs_data
 
 
+def read_double(name, header_string):
+    substring = re.search(r"<ParamDouble.\"{}\">  {{ <Precision> \d+(  -?[0-9\.]+)?  }}".format(name), header_string)
+    if not substring:
+        raise KeyError(r'ParamDouble."{}" not found in header string'.format(name))
+    number_string = substring.group(1)
+    return float(number_string) if number_string else 0
+
+
 def parse_twix_header(header_string):
-    # print(header_string)
+    #print(header_string)
     # get the name of the protocol being acquired
     protocol_name_string = re.search(r"<ParamString.\"tProtocolName\">  { \".+\"  }\n", header_string).group()
     protocol_name = protocol_name_string.split("\"")[3]
@@ -100,12 +113,49 @@ def parse_twix_header(header_string):
             break
     else:
         raise KeyError("Unable to identify Dwell Time from header")
+
+    # get voxel size
+    ro_fov = read_double("VoI_RoFOV", header_string)
+    pe_fov = read_double("VoI_PeFOV", header_string)
+    slice_thickness = read_double("VoI_SliceThickness", header_string)
+
+    # get position information
+    pos_sag = read_double("VoI_Position_Sag", header_string)
+    pos_cor = read_double("VoI_Position_Cor", header_string)
+    pos_tra = read_double("VoI_Position_Tra", header_string)
+
+    # get orientation information
+    in_plane_rot = read_double("VoI_InPlaneRotAngle", header_string)
+    normal_sag = read_double("VoI_Normal_Sag", header_string)
+    normal_cor = read_double("VoI_Normal_Cor", header_string)
+    normal_tra = read_double("VoI_Normal_Tra", header_string)
+
+    # the orientation is stored in a somewhat strange way - a normal vector and
+    # a rotation angle. to get the row vector, we first use Gram-Schmidt to
+    # make [-1, 0, 0] (the default row vector) orthogonal to the normal, and
+    # then rotate that vector by the rotation angle (which we do here with a
+    # quaternion (not any more, quaternion library has issues with Travis)
+    x_vector = numpy.array([-1, 0, 0])
+    normal_vector = numpy.array([normal_sag, normal_cor, normal_tra])
+    orthogonal_x = x_vector - numpy.dot(x_vector, normal_vector) * normal_vector
+    orthonormal_x = orthogonal_x / numpy.linalg.norm(orthogonal_x)
+    #rotation_quaternion = quaternion.from_rotation_vector(in_plane_rot * normal_vector)
+    #row_vector2 = quaternion.rotate_vectors(rotation_quaternion, orthonormal_x)
+    rot_matrix = rotation_matrix(in_plane_rot, normal_vector)
+    row_vector = numpy.dot(rot_matrix, orthonormal_x)
+    column_vector = numpy.cross(row_vector, normal_vector)
+    transform = transformation_matrix(row_vector,
+                                      column_vector,
+                                      [pos_sag, pos_cor, pos_tra],
+                                      [ro_fov, pe_fov, slice_thickness])
+
     return {"protocol_name": protocol_name,
             "patient_name": patient_name,
             "patient_id": patient_id,
             "patient_birthdate": patient_birthday,
             "dt": dwell_time,
-            "f0": frequency
+            "f0": frequency,
+            "transform": transform
             }
 
 

Get spatial information out of twix files
At the moment, only rda files load a transform to enable them to be co-registered to structural MRI images. It should in theory be possible to read this information out of the twix file as well.
diff --git a/tests/test_mrs/test_twix.py b/tests/test_mrs/test_twix.py
index 4c95f5f..2b3b374 100644
--- a/tests/test_mrs/test_twix.py
+++ b/tests/test_mrs/test_twix.py
@@ -15,6 +15,12 @@ def test_veriofile():
     assert data.np == 2048
     assert data.dt == 2.5e-4
     numpy.testing.assert_almost_equal(data.f0, 123.261716)
+    numpy.testing.assert_allclose(data.transform, numpy.array(
+        [[-20, 0, 0, 4.917676],
+         [0, 20, 0, 57.525424],
+         [0, 0, -20, 43.220339],
+         [0, 0, 0, 1]]
+    ))
 
 
 #def test_skyra():

pip install -e .
parse_log_pytest
requirements.txt
pytest mock
3.5
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
asteval==0.9.26
async-generator==1.10
attrs==22.2.0
Babel==2.11.0
backcall==0.2.0
bleach==4.1.0
certifi==2021.5.30
charset-normalizer==2.0.12
decorator==5.1.1
defusedxml==0.7.1
docutils==0.18.1
entrypoints==0.4
future==1.0.0
idna==3.10
imagesize==1.4.1
importlib-metadata==4.8.3
iniconfig==1.1.1
ipykernel==5.5.6
ipython==7.16.3
ipython-genutils==0.2.0
jedi==0.17.2
Jinja2==3.0.3
jsonschema==3.2.0
jupyter-client==7.1.2
jupyter-core==4.9.2
jupyterlab-pygments==0.1.2
lmfit==1.0.3
MarkupSafe==2.0.1
mistune==0.8.4
mock==5.2.0
nbclient==0.5.9
nbconvert==6.0.7
nbformat==5.1.3
nbsphinx==0.8.8
nest-asyncio==1.6.0
numpy==1.19.5
packaging==21.3
pandocfilters==1.5.1
Parsley==1.3
parso==0.7.1
pexpect==4.9.0
pickleshare==0.7.5
pluggy==1.0.0
prompt-toolkit==3.0.36
ptyprocess==0.7.0
py==1.11.0
pydicom==2.3.1
Pygments==2.14.0
pyparsing==3.1.4
pyrsistent==0.18.0
pytest==7.0.1
python-dateutil==2.9.0.post0
pytz==2025.2
PyWavelets==1.1.1
pyzmq==25.1.2
requests==2.27.1
scipy==1.5.4
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
-e git+https://github.com/openmrslab/suspect.git@964f2460e46378c29e78d280f999128f34e829df#egg=suspect
testpath==0.6.0
tomli==1.2.3
tornado==6.1
traitlets==4.3.3
typing_extensions==4.1.1
uncertainties==3.1.7
urllib3==1.26.20
wcwidth==0.2.13
webencodings==0.5.1
zipp==3.6.0

name: suspect
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - asteval==0.9.26
      - async-generator==1.10
      - attrs==22.2.0
      - babel==2.11.0
      - backcall==0.2.0
      - bleach==4.1.0
      - charset-normalizer==2.0.12
      - decorator==5.1.1
      - defusedxml==0.7.1
      - docutils==0.18.1
      - entrypoints==0.4
      - future==1.0.0
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - ipykernel==5.5.6
      - ipython==7.16.3
      - ipython-genutils==0.2.0
      - jedi==0.17.2
      - jinja2==3.0.3
      - jsonschema==3.2.0
      - jupyter-client==7.1.2
      - jupyter-core==4.9.2
      - jupyterlab-pygments==0.1.2
      - lmfit==1.0.3
      - markupsafe==2.0.1
      - mistune==0.8.4
      - mock==5.2.0
      - nbclient==0.5.9
      - nbconvert==6.0.7
      - nbformat==5.1.3
      - nbsphinx==0.8.8
      - nest-asyncio==1.6.0
      - numpy==1.19.5
      - packaging==21.3
      - pandocfilters==1.5.1
      - parsley==1.3
      - parso==0.7.1
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - pluggy==1.0.0
      - prompt-toolkit==3.0.36
      - ptyprocess==0.7.0
      - py==1.11.0
      - pydicom==2.3.1
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pyrsistent==0.18.0
      - pytest==7.0.1
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pywavelets==1.1.1
      - pyzmq==25.1.2
      - requests==2.27.1
      - scipy==1.5.4
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - testpath==0.6.0
      - tomli==1.2.3
      - tornado==6.1
      - traitlets==4.3.3
      - typing-extensions==4.1.1
      - uncertainties==3.1.7
      - urllib3==1.26.20
      - wcwidth==0.2.13
      - webencodings==0.5.1
      - zipp==3.6.0
prefix: /opt/conda/envs/suspect

tests/test_mrs/test_twix.py::test_veriofile
tests/test_mrs/test_twix.py::test_twix_nofile
suspect/_transforms.py suspect/_version.py suspect/io/twix.py
suspect/_transforms.py suspect/_version.py suspect/io/twix.py