Index: src/sas/sascalc/dataloader/readers/cansas_reader.py
===================================================================
--- src/sas/sascalc/dataloader/readers/cansas_reader.py	(revision 5f26aa48a5e34765d73c20fa10024d5319567854)
+++ src/sas/sascalc/dataloader/readers/cansas_reader.py	(revision 5f26aa48a5e34765d73c20fa10024d5319567854)
@@ -1179,9 +1179,9 @@
             written = written | self.write_node(pix, "z", item.pixel_size.z,
                                                 {"unit": item.pixel_size_unit})
-            written = written | self.write_node(det, "slit_length",
-                                                item.slit_length,
-                                                {"unit": item.slit_length_unit})
             if written == True:
                 self.append(pix, det)
+            self.write_node(det, "slit_length", item.slit_length,
+                {"unit": item.slit_length_unit})
+
 
     def _write_process_notes(self, datainfo, entry_node):
Index: src/sas/sascalc/dataloader/readers/cansas_reader_HDF5.py
===================================================================
--- src/sas/sascalc/dataloader/readers/cansas_reader_HDF5.py	(revision a4deca600c233c866bddf97395879f4fcd0758af)
+++ src/sas/sascalc/dataloader/readers/cansas_reader_HDF5.py	(revision a4deca600c233c866bddf97395879f4fcd0758af)
@@ -76,4 +76,6 @@
                 ## Add the last data set to the list of outputs
                 self.add_data_set()
+                ## Close the data file
+                self.raw_data.close()
         ## Return data set(s)
         return self.output
@@ -189,5 +191,7 @@
 
                     ## Sample Information
-                    elif key == u'Title' and self.parent_class == u'SASsample':
+                    elif key == u'Title' and self.parent_class == u'SASsample': # CanSAS 2.0 format
+                        self.current_datainfo.sample.name = data_point
+                    elif key == u'name' and self.parent_class == u'SASsample': # NXcanSAS format
                         self.current_datainfo.sample.name = data_point
                     elif key == u'thickness' and self.parent_class == u'SASsample':
@@ -213,5 +217,7 @@
                     elif key == u'name' and self.parent_class == u'SASprocess':
                         self.process.name = data_point
-                    elif key == u'Title' and self.parent_class == u'SASprocess':
+                    elif key == u'Title' and self.parent_class == u'SASprocess': # CanSAS 2.0 format
+                        self.process.name = data_point
+                    elif key == u'name' and self.parent_class == u'SASprocess': # NXcanSAS format
                         self.process.name = data_point
                     elif key == u'description' and self.parent_class == u'SASprocess':
@@ -376,4 +382,5 @@
         self.current_datainfo = DataInfo()
 
+
     def _initialize_new_data_set(self, parent_list = None):
         """
Index: src/sas/sascalc/file_converter/bsl_loader.py
===================================================================
--- src/sas/sascalc/file_converter/bsl_loader.py	(revision fe498b83c60d90385d24f137b55ab1ac6c88f718)
+++ src/sas/sascalc/file_converter/bsl_loader.py	(revision fe498b83c60d90385d24f137b55ab1ac6c88f718)
@@ -0,0 +1,130 @@
+from sas.sascalc.file_converter.core.bsl_loader import CLoader
+from sas.sascalc.dataloader.data_info import Data2D
+from copy import deepcopy
+import os
+import numpy as np
+
+class BSLParsingError(Exception):
+    pass
+
+class BSLLoader(CLoader):
+    """
+    Loads 2D SAS data from a BSL file.
+    CLoader is a C extension (found in c_ext/bsl_loader.c)
+
+    See http://www.diamond.ac.uk/Beamlines/Soft-Condensed-Matter/small-angle/SAXS-Software/CCP13/BSL.html
+    for more info on the BSL file format.
+    """
+
+    def __init__(self, filename):
+        """
+        Parses the BSL header file and sets instance variables apropriately
+
+        :param filename: Path to the BSL header file
+        """
+        header_file = open(filename, 'r')
+        data_info = {}
+        is_valid = True
+        err_msg = ""
+
+        [folder, filename] = os.path.split(filename)
+        # SAS data will be in file Xnn001.mdd
+        sasdata_filename = filename.replace('000.', '001.')
+        if sasdata_filename == filename:
+            err_msg = ("Invalid header filename {}.\nShould be of the format "
+                "Xnn000.XXX where X is any alphanumeric character and n is any"
+                " digit.").format(filename)
+            raise BSLParsingError(err_msg)
+
+        # First 2 lines are headers
+        header_file.readline()
+        header_file.readline()
+
+        while True:
+            metadata = header_file.readline().strip()
+            metadata = metadata.split()
+            data_filename = header_file.readline().strip()
+
+            if len(metadata) != 10:
+                is_valid = False
+                err_msg = "Invalid header file: {}".format(filename)
+                break
+
+            if data_filename != sasdata_filename:
+                last_file = (metadata[9] == '0')
+                if last_file: # Reached last file we have metadata for
+                    is_valid = False
+                    err_msg = "No metadata for {} found in header file: {}"
+                    err_msg = err_msg.format(sasdata_filename, filename)
+                    break
+                continue
+            try:
+                data_info = {
+                    'filename': os.path.join(folder, data_filename),
+                    'pixels': int(metadata[0]),
+                    'rasters': int(metadata[1]),
+                    'frames': int(metadata[2]),
+                    'swap_bytes': int(metadata[3])
+                }
+            except:
+                is_valid = False
+                err_msg = "Invalid metadata in header file for {}"
+                err_msg = err_msg.format(sasdata_filename)
+            break
+
+        header_file.close()
+        if not is_valid:
+            raise BSLParsingError(err_msg)
+
+        CLoader.__init__(self, data_info['filename'], data_info['frames'],
+            data_info['pixels'], data_info['rasters'], data_info['swap_bytes'])
+
+    def load_frames(self, frames):
+        frame_data = []
+        # Prepare axis values (arbitrary scale)
+        x = self.n_rasters * range(1, self.n_pixels+1)
+        y = [self.n_pixels * [i] for i in range(1, self.n_rasters+1)]
+        y = np.reshape(y, (1, self.n_pixels*self.n_rasters))[0]
+        x_bins = x[:self.n_pixels]
+        y_bins = y[0::self.n_pixels]
+
+        for frame in frames:
+            self.frame = frame
+            raw_frame_data = self.load_data()
+            data2d = Data2D(data=raw_frame_data, qx_data=x, qy_data=y)
+            data2d.x_bins = x_bins
+            data2d.y_bins = y_bins
+            frame_data.append(data2d)
+
+        return frame_data
+
+
+    def __setattr__(self, name, value):
+        if name == 'filename':
+            return self.set_filename(value)
+        elif name == 'n_frames':
+            return self.set_n_frames(value)
+        elif name == 'frame':
+            return self.set_frame(value)
+        elif name == 'n_pixels':
+            return self.set_n_pixels(value)
+        elif name == 'n_rasters':
+            return self.set_n_rasters(value)
+        elif name == 'swap_bytes':
+            return self.set_swap_bytes(value)
+        return CLoader.__setattr__(self, name, value)
+
+    def __getattr__(self, name):
+        if name == 'filename':
+            return self.get_filename()
+        elif name == 'n_frames':
+            return self.get_n_frames()
+        elif name == 'frame':
+            return self.get_frame()
+        elif name == 'n_pixels':
+            return self.get_n_pixels()
+        elif name == 'n_rasters':
+            return self.get_n_rasters()
+        elif name == 'swap_bytes':
+            return self.get_swap_bytes()
+        return CLoader.__getattr__(self, name)
Index: src/sas/sascalc/file_converter/c_ext/bsl_loader.c
===================================================================
--- src/sas/sascalc/file_converter/c_ext/bsl_loader.c	(revision dc8a5538706d3b922a54e3613c6c3c1cf0eb8ac3)
+++ src/sas/sascalc/file_converter/c_ext/bsl_loader.c	(revision dc8a5538706d3b922a54e3613c6c3c1cf0eb8ac3)
@@ -0,0 +1,291 @@
+#include <Python.h>
+#include <numpy/arrayobject.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include "structmember.h"
+#include "bsl_loader.h"
+
+typedef struct {
+    PyObject_HEAD
+    CLoader_params params;
+} CLoader;
+
+static PyObject *CLoader_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
+    CLoader *self;
+
+    self = (CLoader *)type->tp_alloc(type, 0);
+
+    return (PyObject *)self;
+}
+
+static PyObject *CLoader_init(CLoader *self, PyObject *args, PyObject *kwds) {
+    const char *filename;
+    const int n_frames;
+    const int n_pixels;
+    const int n_rasters;
+    const int swap_bytes;
+
+    if (self != NULL) {
+        if (!PyArg_ParseTuple(args, "siiii", &filename, &n_frames, &n_pixels, &n_rasters, &swap_bytes))
+            Py_RETURN_NONE;
+        if (!(self->params.filename = malloc(strlen(filename) + 1)))
+            Py_RETURN_NONE;
+        strcpy(self->params.filename, filename);
+        self->params.n_frames = n_frames;
+        self->params.n_pixels = n_pixels;
+        self->params.n_rasters = n_rasters;
+        self->params.swap_bytes = swap_bytes;
+    }
+
+    return 0;
+}
+
+static void CLoader_dealloc(CLoader *self) {
+    free(self->params.filename);
+    self->ob_type->tp_free((PyObject *)self);
+}
+
+static PyObject *to_string(CLoader *self, PyObject *params) {
+    char str[100];
+    sprintf(str,
+        "Filename: %s\nn_frames: %d\nframe: %d\nn_pixels: %d\nn_rasters: %d\nswap_bytes: %d",
+        self->params.filename,
+        self->params.n_frames,
+        self->params.frame,
+        self->params.n_pixels,
+        self->params.n_rasters,
+        self->params.swap_bytes);
+    return Py_BuildValue("s", str);
+}
+
+/*                    ----- Setters and Getters -----                        */
+
+static PyObject *get_filename(CLoader *self, PyObject *args) {
+    return Py_BuildValue("s", self->params.filename);
+}
+
+static PyObject *set_filename(CLoader *self, PyObject *args) {
+    const char *new_filename;
+    if (!PyArg_ParseTuple(args, "s", &new_filename))
+        return NULL;
+    strcpy(self->params.filename, new_filename);
+
+    return Py_BuildValue("s", self->params.filename);
+}
+
+static PyObject *get_n_frames(CLoader *self, PyObject *args) {
+    return Py_BuildValue("i", self->params.n_frames);
+}
+
+static PyObject *set_n_frames(CLoader *self, PyObject *args) {
+    int new_frames;
+    if (!PyArg_ParseTuple(args, "i", &new_frames))
+        return NULL;
+    self->params.n_frames = new_frames;
+
+    return Py_BuildValue("i", self->params.n_frames);
+}
+
+static PyObject *get_frame(CLoader *self, PyObject *args) {
+    return Py_BuildValue("i", self->params.frame);
+}
+
+static PyObject *set_frame(CLoader *self, PyObject *args) {
+    int new_frame;
+    if (!PyArg_ParseTuple(args, "i", &new_frame))
+        return NULL;
+    self->params.frame = new_frame;
+
+    return Py_BuildValue("i", self->params.frame);
+}
+
+static PyObject *get_n_pixels(CLoader *self, PyObject *args) {
+    return Py_BuildValue("i", self->params.n_pixels);
+}
+
+static PyObject *set_n_pixels(CLoader *self, PyObject *args) {
+    int new_pixels;
+    if (!PyArg_ParseTuple(args, "i", &new_pixels))
+        return NULL;
+    self->params.n_pixels = new_pixels;
+
+    return Py_BuildValue("i", self->params.n_pixels);
+}
+
+static PyObject *get_n_rasters(CLoader *self, PyObject *args) {
+    return Py_BuildValue("i", self->params.n_rasters);
+}
+
+static PyObject *set_n_rasters(CLoader *self, PyObject *args) {
+    int new_rasters;
+    if (!PyArg_ParseTuple(args, "i", &new_rasters))
+        return NULL;
+    self->params.n_rasters = new_rasters;
+
+    return Py_BuildValue("i", self->params.n_rasters);
+}
+
+static PyObject *get_swap_bytes(CLoader *self, PyObject *args) {
+    return Py_BuildValue("i", self->params.swap_bytes);
+}
+
+static PyObject *set_swap_bytes(CLoader *self, PyObject *args) {
+    int new_swap;
+    if (!PyArg_ParseTuple(args, "i", &new_swap))
+        return NULL;
+    self->params.swap_bytes = new_swap;
+
+    return Py_BuildValue("i", self->params.swap_bytes);
+}
+
+/*                        ----- Instance Methods -----                       */
+
+float reverse_float(const float in_float) {
+    // Reverse the order of the bytes of a float
+    float retval;
+    char *to_convert = (char *)&in_float;
+    char *return_float = (char *)&retval;
+
+    return_float[0] = to_convert[3];
+    return_float[1] = to_convert[2];
+    return_float[2] = to_convert[1];
+    return_float[3] = to_convert[0];
+
+    return retval;
+}
+
+static PyObject *load_data(CLoader *self, PyObject *args) {
+    int raster;
+    int pixel;
+    int frame_pos;
+    npy_intp size[2] = {self->params.n_rasters, self->params.n_pixels};
+    float cur_val;
+    FILE *input_file;
+    PyArrayObject *data;
+
+    // Create a new numpy array to store the data in
+    data = (PyArrayObject *)PyArray_SimpleNew(2, size, NPY_FLOAT);
+
+    // Attempt to open the file specified
+    input_file = fopen(self->params.filename, "rb");
+    if (!input_file) {
+        // BSL filenames are 10 characters long
+        // Filename validity checked in bsl_loader.py
+        size_t filename_start = strlen(self->params.filename) - 10;
+        char *filename = self->params.filename + filename_start;
+        char *err_msg = (char *)malloc(sizeof(char) * 32);
+
+        sprintf(err_msg, "Unable to open file: %s", filename);
+
+        PyErr_SetString(PyExc_RuntimeError, err_msg);
+        free(err_msg);
+        return NULL;
+    }
+
+    // Move the file cursor the the position where the data we're interested
+    // in begins
+    frame_pos = self->params.n_pixels * self->params.n_rasters * self->params.frame;
+    fseek(input_file, frame_pos*sizeof(float), SEEK_SET);
+
+    for (raster = 0; raster < self->params.n_rasters; raster++) {
+        for (pixel = 0; pixel < self->params.n_pixels; pixel++) {
+            // Try reading the file
+            if (fread(&cur_val, sizeof(float), 1, input_file) == 0) {
+                PyErr_SetString(PyExc_RuntimeError, "Error reading file or EOF reached.");
+                return NULL;
+            }
+
+            // Swap the order of the bytes read, if specified that we should do
+            // so in the header file
+            if (self->params.swap_bytes == 0)
+                cur_val = reverse_float(cur_val);
+
+            // Add the read value to the numpy array
+            PyArray_SETITEM(data, PyArray_GETPTR2(data, raster, pixel), PyFloat_FromDouble(cur_val));
+        }
+    }
+
+    fclose(input_file);
+
+    return Py_BuildValue("N", data);
+}
+
+/*                           ----- Class Registration -----                  */
+
+static PyMethodDef CLoader_methods[] = {
+    { "to_string", (PyCFunction)to_string, METH_VARARGS, "Print the objects params" },
+    { "get_filename", (PyCFunction)get_filename, METH_VARARGS, "Get the filename" },
+    { "set_filename", (PyCFunction)set_filename, METH_VARARGS, "Set the filename" },
+    { "get_n_frames", (PyCFunction)get_n_frames, METH_VARARGS, "Get n_frames" },
+    { "set_n_frames", (PyCFunction)set_n_frames, METH_VARARGS, "Set n_frames" },
+    { "get_frame", (PyCFunction)get_frame, METH_VARARGS, "Get the frame that will be loaded" },
+    { "set_frame", (PyCFunction)set_frame, METH_VARARGS, "Set the frame that will be loaded" },
+    { "get_n_pixels", (PyCFunction)get_n_pixels, METH_VARARGS, "Get n_pixels" },
+    { "set_n_pixels", (PyCFunction)set_n_pixels, METH_VARARGS, "Set n_pixels" },
+    { "get_n_rasters", (PyCFunction)get_n_rasters, METH_VARARGS, "Get n_rasters" },
+    { "set_n_rasters", (PyCFunction)set_n_rasters, METH_VARARGS, "Set n_rasters" },
+    { "get_swap_bytes", (PyCFunction)get_swap_bytes, METH_VARARGS, "Get swap_bytes" },
+    { "set_swap_bytes", (PyCFunction)set_swap_bytes, METH_VARARGS, "Set swap_bytes" },
+    { "load_data", (PyCFunction)load_data, METH_VARARGS, "Load the data into a numpy array" },
+    {NULL}
+};
+
+static PyMemberDef CLoader_members[] = {
+    {NULL}
+};
+
+static PyTypeObject CLoaderType = {
+    PyObject_HEAD_INIT(NULL)
+    0,                         /*ob_size*/
+    "CLoader",             /*tp_name*/
+    sizeof(CLoader),             /*tp_basicsize*/
+    0,                         /*tp_itemsize*/
+    (destructor)CLoader_dealloc, /*tp_dealloc*/
+    0,                         /*tp_print*/
+    0,                         /*tp_getattr*/
+    0,                         /*tp_setattr*/
+    0,                         /*tp_compare*/
+    0,                         /*tp_repr*/
+    0,                         /*tp_as_number*/
+    0,                         /*tp_as_sequence*/
+    0,                         /*tp_as_mapping*/
+    0,                         /*tp_hash */
+    0,                         /*tp_call*/
+    0,                         /*tp_str*/
+    0,                         /*tp_getattro*/
+    0,                         /*tp_setattro*/
+    0,                         /*tp_as_buffer*/
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
+    "CLoader objects",           /* tp_doc */
+    0,		               /* tp_traverse */
+    0,		               /* tp_clear */
+    0,		               /* tp_richcompare */
+    0,		               /* tp_weaklistoffset */
+    0,		               /* tp_iter */
+    0,		               /* tp_iternext */
+    CLoader_methods,             /* tp_methods */
+    CLoader_members,             /* tp_members */
+    0,                         /* tp_getset */
+    0,                         /* tp_base */
+    0,                         /* tp_dict */
+    0,                         /* tp_descr_get */
+    0,                         /* tp_descr_set */
+    0,                         /* tp_dictoffset */
+    (initproc)CLoader_init,      /* tp_init */
+    0,                         /* tp_alloc */
+    CLoader_new,                 /* tp_new */
+};
+
+PyMODINIT_FUNC
+initbsl_loader(void)
+{
+    PyObject *module;
+    module = Py_InitModule("bsl_loader", NULL);
+    import_array();
+
+    if (PyType_Ready(&CLoaderType) < 0)
+        return;
+
+    Py_INCREF(&CLoaderType);
+    PyModule_AddObject(module, "CLoader", (PyObject *)&CLoaderType);
+}
Index: src/sas/sascalc/file_converter/c_ext/bsl_loader.h
===================================================================
--- src/sas/sascalc/file_converter/c_ext/bsl_loader.h	(revision 535e18165fb7319948e27d8876a389fd5c738129)
+++ src/sas/sascalc/file_converter/c_ext/bsl_loader.h	(revision 535e18165fb7319948e27d8876a389fd5c738129)
@@ -0,0 +1,19 @@
+#ifndef bsl_loader_h
+#define bsl_loader_h
+
+typedef struct {
+    // File to load
+    char *filename;
+    // Number of frames in the file
+    int n_frames;
+    // Frame to load
+    int frame;
+    // Number of pixels in the file
+    int n_pixels;
+    // Number of rasters in the file
+    int n_rasters;
+    // Whether or not the bytes are in reverse order
+    int swap_bytes;
+} CLoader_params;
+
+#endif
Index: src/sas/sascalc/file_converter/cansas_writer.py
===================================================================
--- src/sas/sascalc/file_converter/cansas_writer.py	(revision 0b1a677a6bec9b3b30979ce36e64645b3028908b)
+++ src/sas/sascalc/file_converter/cansas_writer.py	(revision 0b1a677a6bec9b3b30979ce36e64645b3028908b)
@@ -0,0 +1,109 @@
+from sas.sascalc.dataloader.readers.cansas_reader import Reader as CansasReader
+from sas.sascalc.dataloader.data_info import Data1D
+
+import inspect
+
+class CansasWriter(CansasReader):
+
+    def write(self, filename, frame_data, sasentry_attrs=None):
+        """
+        Write the content of a Data1D as a CanSAS XML file
+
+        :param filename: name of the file to write
+        :param datainfo: Data1D object
+        """
+        # Create XML document
+        doc, _ = self._to_xml_doc(frame_data, sasentry_attrs)
+        # Write the file
+        file_ref = open(filename, 'w')
+        if self.encoding == None:
+            self.encoding = "UTF-8"
+        doc.write(file_ref, encoding=self.encoding,
+                  pretty_print=True, xml_declaration=True)
+        file_ref.close()
+
+
+    def _to_xml_doc(self, frame_data, sasentry_attrs=None):
+        """
+        Create an XML document to contain the content of an array of Data1Ds
+
+        :param frame_data: An array of Data1D objects
+        """
+        valid_class = all([issubclass(data.__class__, Data1D) for data in frame_data])
+        if not valid_class:
+            raise RuntimeError, ("The cansas writer expects an array of "
+                "Data1D instances")
+
+        # Get PIs and create root element
+        pi_string = self._get_pi_string()
+        # Define namespaces and create SASroot object
+        main_node = self._create_main_node()
+        # Create ElementTree, append SASroot and apply processing instructions
+        base_string = pi_string + self.to_string(main_node)
+        base_element = self.create_element_from_string(base_string)
+        doc = self.create_tree(base_element)
+        # Create SASentry Element
+        entry_node = self.create_element("SASentry", sasentry_attrs)
+        root = doc.getroot()
+        root.append(entry_node)
+
+        # Use the first element in the array for writing metadata
+        datainfo = frame_data[0]
+        # Add Title to SASentry
+        self.write_node(entry_node, "Title", datainfo.title)
+        # Add Run to SASentry
+        self._write_run_names(datainfo, entry_node)
+        # Add Data info to SASEntry
+        for data_info in frame_data:
+            self._write_data(data_info, entry_node)
+        # Transmission Spectrum Info
+        self._write_trans_spectrum(datainfo, entry_node)
+        # Sample info
+        self._write_sample_info(datainfo, entry_node)
+        # Instrument info
+        instr = self._write_instrument(datainfo, entry_node)
+        #   Source
+        self._write_source(datainfo, instr)
+        #   Collimation
+        self._write_collimation(datainfo, instr)
+        #   Detectors
+        self._write_detectors(datainfo, instr)
+        # Processes info
+        self._write_process_notes(datainfo, entry_node)
+        # Note info
+        self._write_notes(datainfo, entry_node)
+        # Return the document, and the SASentry node associated with
+        #      the data we just wrote
+
+        return doc, entry_node
+
+    def _write_data(self, datainfo, entry_node):
+        """
+        Writes the I and Q data to the XML file
+
+        :param datainfo: The Data1D object the information is coming from
+        :param entry_node: lxml node ElementTree object to be appended to
+        """
+        node = self.create_element("SASdata")
+        self.append(node, entry_node)
+
+        for i in range(len(datainfo.x)):
+            point = self.create_element("Idata")
+            node.append(point)
+            self.write_node(point, "Q", datainfo.x[i],
+                            {'unit': datainfo.x_unit})
+            if len(datainfo.y) >= i:
+                self.write_node(point, "I", datainfo.y[i],
+                                {'unit': datainfo.y_unit})
+            if datainfo.dy != None and len(datainfo.dy) > i:
+                self.write_node(point, "Idev", datainfo.dy[i],
+                                {'unit': datainfo.y_unit})
+            if datainfo.dx != None and len(datainfo.dx) > i:
+                self.write_node(point, "Qdev", datainfo.dx[i],
+                                {'unit': datainfo.x_unit})
+            if datainfo.dxw != None and len(datainfo.dxw) > i:
+                self.write_node(point, "dQw", datainfo.dxw[i],
+                                {'unit': datainfo.x_unit})
+            if datainfo.dxl != None and len(datainfo.dxl) > i:
+                self.write_node(point, "dQl", datainfo.dxl[i],
+                                {'unit': datainfo.x_unit})
Index: src/sas/sascalc/file_converter/nxcansas_writer.py
===================================================================
--- src/sas/sascalc/file_converter/nxcansas_writer.py	(revision a14fa9928d4098d512dcaee6da7e8c07014f90a6)
+++ src/sas/sascalc/file_converter/nxcansas_writer.py	(revision a14fa9928d4098d512dcaee6da7e8c07014f90a6)
@@ -0,0 +1,202 @@
+"""
+    NXcanSAS 1/2D data reader for writing HDF5 formatted NXcanSAS files.
+"""
+
+import h5py
+import numpy as np
+import re
+import os
+
+from sas.sascalc.dataloader.readers.cansas_reader_HDF5 import Reader as Cansas2Reader
+from sas.sascalc.dataloader.data_info import Data1D, Data2D
+
+class NXcanSASWriter(Cansas2Reader):
+    """
+    A class for writing in NXcanSAS data files. Any number of data sets may be
+    written to the file. Currently 1D and 2D SAS data sets are supported
+
+    NXcanSAS spec: http://download.nexusformat.org/sphinx/classes/contributed_definitions/NXcanSAS.html
+
+    :Dependencies:
+        The NXcanSAS writer requires h5py => v2.5.0 or later.
+    """
+
+    def write(self, dataset, filename):
+        """
+        Write an array of Data1d or Data2D objects to an NXcanSAS file, as
+        one SASEntry with multiple SASData elements. The metadata of the first
+        elememt in the array will be written as the SASentry metadata
+        (detector, instrument, sample, etc).
+
+        :param dataset: A list of Data1D or Data2D objects to write
+        :param filename: Where to write the NXcanSAS file
+        """
+
+        def _h5_string(string):
+            """
+            Convert a string to a numpy string in a numpy array. This way it is
+            written to the HDF5 file as a fixed length ASCII string and is
+            compatible with the Reader read() method.
+            """
+            if not isinstance(string, str):
+                string = str(string)
+
+            return np.array([np.string_(string)])
+
+        def _h5_float(x):
+            if not (isinstance(x, list)):
+                x = [x]
+            return np.array(x, dtype=np.float32)
+
+        valid_data = all([issubclass(d.__class__, (Data1D, Data2D)) for d in dataset])
+        if not valid_data:
+            raise ValueError("All entries of dataset must be Data1D or Data2D objects")
+
+        # Get run name and number from first Data object
+        data_info = dataset[0]
+        run_number = ''
+        run_name = ''
+        if len(data_info.run) > 0:
+            run_number = data_info.run[0]
+            if len(data_info.run_name) > 0:
+                run_name = data_info.run_name[run_number]
+
+        f = h5py.File(filename, 'w')
+        sasentry = f.create_group('sasentry01')
+        sasentry['definition'] = _h5_string('NXcanSAS')
+        sasentry['run'] = _h5_string(run_number)
+        sasentry['run'].attrs['name'] = run_name
+        sasentry['title'] = _h5_string(data_info.title)
+        sasentry.attrs['canSAS_class'] = 'SASentry'
+        sasentry.attrs['version'] = '1.0'
+
+        i = 1
+
+        for data_obj in dataset:
+            data_entry = sasentry.create_group("sasdata{0:0=2d}".format(i))
+            data_entry.attrs['canSAS_class'] = 'SASdata'
+            if isinstance(data_obj, Data1D):
+                self._write_1d_data(data_obj, data_entry)
+            elif isinstance(data_obj, Data2D):
+                self._write_2d_data(data_obj, data_entry)
+            i += 1
+
+        data_info = dataset[0]
+        sample_entry = sasentry.create_group('sassample')
+        sample_entry.attrs['canSAS_class'] = 'SASsample'
+        sample_entry['name'] = _h5_string(data_info.sample.name)
+        sample_attrs = ['thickness', 'temperature']
+        for key in sample_attrs:
+            if getattr(data_info.sample, key) is not None:
+                sample_entry.create_dataset(key,
+                    data=_h5_float(getattr(data_info.sample, key)))
+
+        instrument_entry = sasentry.create_group('sasinstrument')
+        instrument_entry.attrs['canSAS_class'] = 'SASinstrument'
+        instrument_entry['name'] = _h5_string(data_info.instrument)
+
+        source_entry = instrument_entry.create_group('sassource')
+        source_entry.attrs['canSAS_class'] = 'SASsource'
+        if data_info.source.radiation is None:
+            source_entry['radiation'] = _h5_string('neutron')
+        else:
+            source_entry['radiation'] = _h5_string(data_info.source.radiation)
+
+        if len(data_info.collimation) > 0:
+            i = 1
+            for coll_info in data_info.collimation:
+                collimation_entry = instrument_entry.create_group(
+                    'sascollimation{0:0=2d}'.format(i))
+                collimation_entry.attrs['canSAS_class'] = 'SAScollimation'
+                if coll_info.length is not None:
+                    collimation_entry['SDD'] = _h5_float(coll_info.length)
+                    collimation_entry['SDD'].attrs['units'] = coll_info.length_unit
+                if coll_info.name is not None:
+                    collimation_entry['name'] = _h5_string(coll_info.name)
+        else:
+            collimation_entry = instrument_entry.create_group('sascollimation01')
+
+        if len(data_info.detector) > 0:
+            i = 1
+            for det_info in data_info.detector:
+                detector_entry = instrument_entry.create_group(
+                    'sasdetector{0:0=2d}'.format(i))
+                detector_entry.attrs['canSAS_class'] = 'SASdetector'
+                if det_info.distance is not None:
+                    detector_entry['SDD'] = _h5_float(det_info.distance)
+                    detector_entry['SDD'].attrs['units'] = det_info.distance_unit
+                if det_info.name is not None:
+                    detector_entry['name'] = _h5_string(det_info.name)
+                else:
+                    detector_entry['name'] = _h5_string('')
+                i += 1
+        else:
+            detector_entry = instrument_entry.create_group('sasdetector01')
+            detector_entry.attrs['canSAS_class'] = 'SASdetector'
+            detector_entry.attrs['name'] = ''
+
+        # TODO: implement writing SASnote
+        i = 1
+        note_entry = sasentry.create_group('sasnote{0:0=2d}'.format(i))
+        note_entry.attrs['canSAS_class'] = 'SASnote'
+
+        f.close()
+
+    def _write_1d_data(self, data_obj, data_entry):
+        """
+        Writes the contents of a Data1D object to a SASdata h5py Group
+
+        :param data_obj: A Data1D object to write to the file
+        :param data_entry: A h5py Group object representing the SASdata
+        """
+        data_entry.attrs['signal'] = 'I'
+        data_entry.attrs['I_axes'] = 'Q'
+        data_entry.attrs['I_uncertainties'] = 'Idev'
+        data_entry.attrs['Q_indicies'] = 0
+
+        dI = data_obj.dy
+        if dI is None:
+            dI = np.zeros((data_obj.y.shape))
+
+        data_entry.create_dataset('Q', data=data_obj.x)
+        data_entry.create_dataset('I', data=data_obj.y)
+        data_entry.create_dataset('Idev', data=dI)
+
+    def _write_2d_data(self, data, data_entry):
+        """
+        Writes the contents of a Data2D object to a SASdata h5py Group
+
+        :param data: A Data2D object to write to the file
+        :param data_entry: A h5py Group object representing the SASdata
+        """
+        data_entry.attrs['signal'] = 'I'
+        data_entry.attrs['I_axes'] = 'Q,Q'
+        data_entry.attrs['I_uncertainties'] = 'Idev'
+        data_entry.attrs['Q_indicies'] = [0,1]
+
+        (n_rows, n_cols) = (len(data.y_bins), len(data.x_bins))
+
+        if n_rows == 0 and n_cols == 0:
+            # Calculate rows and columns, assuming detector is square
+            # Same logic as used in PlotPanel.py _get_bins
+            n_cols = int(np.floor(np.sqrt(len(data.qy_data))))
+            n_rows = int(np.floor(len(data.qy_data) / n_cols))
+
+            if n_rows * n_cols != len(data.qy_data):
+                raise ValueError("Unable to calculate dimensions of 2D data")
+
+        I = np.reshape(data.data, (n_rows, n_cols))
+        dI = np.zeros((n_rows, n_cols))
+        if not all(data.err_data == [None]):
+            dI = np.reshape(data.err_data, (n_rows, n_cols))
+        qx =  np.reshape(data.qx_data, (n_rows, n_cols))
+        qy = np.reshape(data.qy_data, (n_rows, n_cols))
+
+        I_entry = data_entry.create_dataset('I', data=I)
+        I_entry.attrs['units'] = data.I_unit
+        Qx_entry = data_entry.create_dataset('Qx', data=qx)
+        Qx_entry.attrs['units'] = data.Q_unit
+        Qy_entry = data_entry.create_dataset('Qy', data=qy)
+        Qy_entry.attrs['units'] = data.Q_unit
+        Idev_entry = data_entry.create_dataset('Idev', data=dI)
+        Idev_entry.attrs['units'] = data.I_unit
Index: src/sas/sascalc/file_converter/otoko_loader.py
===================================================================
--- src/sas/sascalc/file_converter/otoko_loader.py	(revision 0b1a677a6bec9b3b30979ce36e64645b3028908b)
+++ src/sas/sascalc/file_converter/otoko_loader.py	(revision 0b1a677a6bec9b3b30979ce36e64645b3028908b)
@@ -0,0 +1,150 @@
+"""
+Here we handle loading of "OTOKO" data (for more info about this format see
+the comment in load_otoko_data).  Given the paths of header and data files, we
+aim to load the data into numpy arrays for use later.
+"""
+
+import itertools
+import os
+import struct
+import numpy as np
+
+class CStyleStruct:
+    """A nice and easy way to get "C-style struct" functionality."""
+    def __init__(self, **kwds):
+        self.__dict__.update(kwds)
+
+class OTOKOParsingError(Exception):
+    pass
+
+class OTOKOData:
+    def __init__(self, q_axis, data_axis):
+        self.q_axis = q_axis
+        self.data_axis = data_axis
+
+class OTOKOLoader(object):
+
+    def __init__(self, qaxis_path, data_path):
+        self.qaxis_path = qaxis_path
+        self.data_path = data_path
+
+    def load_otoko_data(self):
+        """
+        Loads "OTOKO" data, which is a format that stores each axis separately.
+        An axis is represented by a "header" file, which in turn will give details
+        of one or more binary files where the actual data is stored.
+
+        Given the paths of two header files, this function will load each axis in
+        turn.  If loading is successfull then an instance of the OTOKOData class
+        will be returned, else an exception will be raised.
+
+        For more information on the OTOKO file format, please see:
+        http://www.diamond.ac.uk/Home/Beamlines/small-angle/SAXS-Software/CCP13/
+        XOTOKO.html
+        """
+        q_axis    = self._load_otoko_axis(self.qaxis_path)
+        data_axis = self._load_otoko_axis(self.data_path)
+
+        return OTOKOData(q_axis, data_axis)
+
+    def _load_otoko_axis(self, header_path):
+        """
+        Loads an "OTOKO" axis, given the header file path.  Essentially, the
+        header file contains information about the data in the form of integer
+        "indicators", as well as the names of each of the binary files which are
+        assumed to be in the same directory as the header.
+        """
+        if not os.path.exists(header_path):
+            raise OTOKOParsingError("The header file %s does not exist." % header_path)
+
+        binary_file_info_list = []
+        total_frames = 0
+        header_dir = os.path.dirname(os.path.abspath(header_path))
+
+        with open(header_path, "r") as header_file:
+            lines = header_file.readlines()
+            if len(lines) < 4:
+                raise OTOKOParsingError("Expected more lines in %s." % header_path)
+
+            info = lines[0] + lines[1]
+
+            def pairwise(iterable):
+                """
+                s -> (s0,s1), (s2,s3), (s4, s5), ...
+                From http://stackoverflow.com/a/5389547/778572
+                """
+                a = iter(iterable)
+                return itertools.izip(a, a)
+
+            for indicators, filename in pairwise(lines[2:]):
+                indicators = indicators.split()
+
+                if len(indicators) != 10:
+                    raise OTOKOParsingError(
+                        "Expected 10 integer indicators on line 3 of %s." \
+                        % header_path)
+                if not all([i.isdigit() for i in indicators]):
+                    raise OTOKOParsingError(
+                        "Expected all indicators on line 3 of %s to be integers." \
+                        % header_path)
+
+                binary_file_info = CStyleStruct(
+                    # The indicators at indices 4 to 8 are always zero since they
+                    # have been reserved for future use by the format.  Also, the
+                    # "last_file" indicator seems to be there for legacy reasons,
+                    # as it doesn't appear to be something we have to bother
+                    # enforcing correct use of; we just define the last file as
+                    # being the last file in the list.
+                    file_path  = os.path.join(header_dir, filename.strip()),
+                    n_channels = int(indicators[0]),
+                    n_frames   = int(indicators[1]),
+                    dimensions = int(indicators[2]),
+                    swap_bytes = int(indicators[3]) == 0,
+                    last_file  = int(indicators[9]) == 0 # We don't use this.
+                )
+                if binary_file_info.dimensions != 1:
+                    msg = "File {} has {} dimensions, expected 1. Is it a BSL file?"
+                    raise OTOKOParsingError(msg.format(filename.strip(),
+                        binary_file_info.dimensions))
+
+                binary_file_info_list.append(binary_file_info)
+
+                total_frames += binary_file_info.n_frames
+
+        # Check that all binary files are listed in the header as having the same
+        # number of channels, since I don't think CorFunc can handle ragged data.
+        all_n_channels = [info.n_channels for info in binary_file_info_list]
+        if not all(all_n_channels[0] == c for c in all_n_channels):
+            raise OTOKOParsingError(
+                "Expected all binary files listed in %s to have the same number of channels." % header_path)
+
+        data = np.zeros(shape=(total_frames, all_n_channels[0]))
+        frames_so_far = 0
+
+        for info in binary_file_info_list:
+            if not os.path.exists(info.file_path):
+                raise OTOKOParsingError(
+                    "The data file %s does not exist." % info.file_path)
+
+            with open(info.file_path, "rb") as binary_file:
+                # Ideally we'd like to use numpy's fromfile() to read in binary
+                # data, but we are forced to roll our own float-by-float file
+                # reader because of the rules imposed on us by the file format;
+                # namely, if the swap indicator flag has been raised then the bytes
+                # of each float occur in reverse order.
+                for frame in range(info.n_frames):
+                    for channel in range(info.n_channels):
+                        b = bytes(binary_file.read(4))
+                        if info.swap_bytes:
+                            b = b[::-1] # "Extended slice" syntax, used to reverse.
+                        value = struct.unpack('f', b)[0]
+                        data[frames_so_far + frame][channel] = value
+
+                frames_so_far += info.n_frames
+
+        return CStyleStruct(
+            header_path = header_path,
+            data = data,
+            binary_file_info_list = binary_file_info_list,
+            header_info = info
+        )
Index: src/sas/sascalc/file_converter/red2d_writer.py
===================================================================
--- src/sas/sascalc/file_converter/red2d_writer.py	(revision 0b1a677a6bec9b3b30979ce36e64645b3028908b)
+++ src/sas/sascalc/file_converter/red2d_writer.py	(revision 0b1a677a6bec9b3b30979ce36e64645b3028908b)
@@ -0,0 +1,36 @@
+import os
+import time
+from sas.sascalc.dataloader.readers.red2d_reader import Reader as Red2DReader
+
+class Red2DWriter(Red2DReader):
+
+    def write(self, filename, data, thread):
+        """
+        Write to .dat
+
+        :param filename: file name to write
+        :param data: data2D
+        """
+        # Write the file
+        fd = open(filename, 'w')
+        t = time.localtime()
+        time_str = time.strftime("%H:%M on %b %d %y", t)
+
+        header_str = "Data columns are Qx - Qy - I(Qx,Qy)\n\nASCII data"
+        header_str += " created at %s \n\n" % time_str
+        # simple 2D header
+        fd.write(header_str)
+        # write qx qy I values
+        for i in range(len(data.data)):
+            if thread.isquit():
+                fd.close()
+                os.remove(filename)
+                return False
+
+            fd.write("%g  %g  %g\n" % (data.qx_data[i],
+                                        data.qy_data[i],
+                                       data.data[i]))
+
+        fd.close()
+
+        return True
