Repository: davidchall/topas2numpy

Issue:
Detect best NumPy dtype (use unsigned int for SurfaceTrackCount)
When the TOPAS scorer mode is set to 'SurfaceTrackCount' then the result is an integer. It would be best if the numpy dtype of the loaded data was set to an unsigned integer type in this case. 

It seems this library loads such arrrays as 'float64' type which uses only 53 of the 64 bits to store the mantissa, meaning 11/64 bits are wasted in the case of 'SurfaceTrackCount', which unecessarily increases filesize.

It's also unexpected to receive a NumPy array of type 'float64' when the data consists of unsigned integers so this may have a usability impact also.

Is it possible to change this library so that it bases the data type whether the scorer is 'SurfaceTrackCount' or something else?

Discussion:

Patch:
diff --git a/topas2numpy/binned.py b/topas2numpy/binned.py
index dac1dfe..2b65a6d 100644
--- a/topas2numpy/binned.py
+++ b/topas2numpy/binned.py
@@ -47,15 +47,15 @@ class BinnedResult(object):
         dimensions: list of BinnedDimension objects
         data:       dict of scored data
     """
-    def __init__(self, filepath):
+    def __init__(self, filepath, dtype=float):
         self.path = filepath
         _, ext = os.path.splitext(self.path)
         if ext == '.bin':
-            self._read_binary()
+            self._read_binary(dtype)
         elif ext == '.csv':
-            self._read_ascii()
+            self._read_ascii(dtype)
 
-    def _read_binary(self):
+    def _read_binary(self, dtype):
         """Reads data and metadata from binary format."""
         # NOTE: binary files store binned data using Fortran-like ordering.
         # Dimensions are iterated like z, y, x (so x changes fastest)
@@ -64,7 +64,7 @@ class BinnedResult(object):
         with open(header_path) as f_header:
             self._read_header(f_header.read())
 
-        data = np.fromfile(self.path)
+        data = np.fromfile(self.path, dtype=dtype)
 
         # separate data by statistic
         data = data.reshape((len(self.statistics), -1), order='F')
@@ -76,7 +76,7 @@ class BinnedResult(object):
 
         self.data = data
 
-    def _read_ascii(self):
+    def _read_ascii(self, dtype):
         """Reads data and metadata from ASCII format."""
         # NOTE: ascii files store binned data using C-like ordering.
         # Dimensions are iterated like x, y, z (so z changes fastest)
@@ -88,7 +88,7 @@ class BinnedResult(object):
                     header_str += line
         self._read_header(header_str)
 
-        data = np.loadtxt(self.path, delimiter=',', unpack=True, ndmin=1)
+        data = np.loadtxt(self.path, dtype=dtype, delimiter=',', unpack=True, ndmin=1)
 
         # separate data by statistic (neglecting bin columns when necessary)
         n_dim = len(self.dimensions)


Test patch:
diff --git a/tests/test_binned.py b/tests/test_binned.py
index 19e68b3..04e7a37 100644
--- a/tests/test_binned.py
+++ b/tests/test_binned.py
@@ -12,6 +12,9 @@ Tests for TOPAS binned reading.
 import unittest
 import os.path
 
+# third-party imports
+import numpy as np
+
 # project imports
 from topas2numpy import BinnedResult
 
@@ -55,6 +58,7 @@ class TestAscii1D(unittest.TestCase):
         assert self.result.statistics[0] == 'Sum'
         assert len(self.result.data) == 1
         data = self.result.data['Sum']
+        assert data.dtype == np.float64
         assert data.shape[0] == self.result.dimensions[0].n_bins
         assert data.shape[1] == self.result.dimensions[1].n_bins
         assert data.shape[2] == self.result.dimensions[2].n_bins
@@ -62,7 +66,7 @@ class TestAscii1D(unittest.TestCase):
 
 class TestAscii2D(unittest.TestCase):
     def setUp(self):
-        self.result = BinnedResult(ascii_2d_path)
+        self.result = BinnedResult(ascii_2d_path, dtype=np.uint32)
 
     def test_quantity(self):
         assert self.result.quantity == 'SurfaceTrackCount'
@@ -88,6 +92,7 @@ class TestAscii2D(unittest.TestCase):
         assert self.result.statistics[0] == 'Sum'
         assert len(self.result.data) == 1
         data = self.result.data['Sum']
+        assert data.dtype == np.uint32
         assert data.shape[0] == self.result.dimensions[0].n_bins
         assert data.shape[1] == self.result.dimensions[1].n_bins
         assert data.shape[2] == self.result.dimensions[2].n_bins