changeset 75:d5e1d4ea2b7e draft default tip

planemo upload for repository https://github.com/rolfverberg/galaxytools commit 6afde341a94586fe3972bdbbfbf5dabd5e8dec69
author rv43
date Thu, 23 Mar 2023 13:39:14 +0000
parents 4f4ee8db5f67
children
files fit.py tomo_combine.xml tomo_find_center.py tomo_find_center.xml tomo_macros.xml tomo_reconstruct.py tomo_reconstruct.xml tomo_reduce.py tomo_reduce.xml workflow/run_tomo.py
diffstat 10 files changed, 275 insertions(+), 201 deletions(-) [+]
line wrap: on
line diff
--- a/fit.py	Tue Mar 21 17:40:03 2023 +0000
+++ b/fit.py	Thu Mar 23 13:39:14 2023 +0000
@@ -35,20 +35,18 @@
 except:
     have_xarray = False
 
-#try:
-#    from .general import illegal_value, is_int, is_dict_series, is_index, index_nearest, \
-#            almost_equal, quick_plot #, eval_expr
-#except:
-#    try:
-#        from sys import path as syspath
-#        syspath.append(f'/nfs/chess/user/rv43/msnctools/msnctools')
-#        from general import illegal_value, is_int, is_dict_series, is_index, index_nearest, \
-#                almost_equal, quick_plot #, eval_expr
-#    except:
-#        from general import illegal_value, is_int, is_dict_series, is_index, index_nearest, \
-#                almost_equal, quick_plot #, eval_expr
-from general import illegal_value, is_int, is_dict_series, is_index, index_nearest, \
-        almost_equal, quick_plot #, eval_expr
+try:
+    from .general import illegal_value, is_int, is_dict_series, is_index, index_nearest, \
+            almost_equal, quick_plot #, eval_expr
+except:
+    try:
+        from sys import path as syspath
+        syspath.append(f'/nfs/chess/user/rv43/msnctools/msnctools')
+        from general import illegal_value, is_int, is_dict_series, is_index, index_nearest, \
+                almost_equal, quick_plot #, eval_expr
+    except:
+        from general import illegal_value, is_int, is_dict_series, is_index, index_nearest, \
+                almost_equal, quick_plot #, eval_expr
 
 from sys import float_info
 float_min = float_info.min
--- a/tomo_combine.xml	Tue Mar 21 17:40:03 2023 +0000
+++ b/tomo_combine.xml	Thu Mar 23 13:39:14 2023 +0000
@@ -1,44 +1,56 @@
-<tool id="tomo_combine" name="Tomo Combine Reconstructed Stacks" version="0.2.0" python_template_version="3.9">
+<tool id="tomo_combine" name="Tomo Combine Reconstructed Stacks" version="1.0.0" python_template_version="3.9">
     <description>Combine reconstructed tomography stacks</description>
     <macros>
         <import>tomo_macros.xml</import>
     </macros>
     <expand macro="requirements" />
-    <command detect_errors="exit_code"><![CDATA[
-        mkdir combine_pngs;
-        $__tool_directory__/tomo_combine.py
-        -i '$recon_stacks'
-        -c '$config'
-        --x_bounds $x_bounds.low $x_bounds.upp
-        --y_bounds $y_bounds.low $y_bounds.upp
-        --z_bounds $z_bounds.low $z_bounds.upp
-        --output_data 'output_data.npy'
-        --output_config 'output_config.yaml'
-        -l '$log'
-    ]]></command>
+    <command detect_errors="exit_code">
+        <![CDATA[
+            mkdir tomo_combine_plots;
+            $__tool_directory__/tomo_combine.py
+            --input_file "$input_file"
+            --output_file "output.nex"
+            --galaxy_flag
+            #if str($x_bounds.type_selector) == "enter_range"
+                --x_bounds $x_bounds.low $x_bounds.upp
+            #end if
+            #if str($y_bounds.type_selector) == "enter_range"
+                --y_bounds $y_bounds.low $y_bounds.upp
+            #end if
+            -l '$log'
+        ]]>
+    </command>
     <inputs>
-        <expand macro="common_inputs"/>
-        <param name="recon_stacks" type='data' format='npz' optional='false' label="Reconstructed stacks"/>
-        <section name="x_bounds" title="Reconstructed range in x direction">
-            <param name="low" type="integer" value = "-1" label="Lower x-bound"/>
-            <param name="upp" type="integer" value = "-1" label="Upper x-bound"/>
-        </section>
-        <section name="y_bounds" title="Reconstructed range in y direction">
-            <param name="low" type="integer" value = "-1" label="Lower y-bound"/>
-            <param name="upp" type="integer" value = "-1" label="Upper y-bound"/>
-        </section>
-        <section name="z_bounds" title="Reconstructed range in z direction">
-            <param name="low" type="integer" value = "-1" label="Lower z-bound"/>
-            <param name="upp" type="integer" value = "-1" label="Upper z-bound"/>
-        </section>
+        <param name="input_file" type="data" format="nex" optional="false" label="Reconstructed tomography data"/>
+        <conditional name="x_bounds">
+            <param name="type_selector" type="select" label="Choose reconstructed image range in x-direction">
+                <option value="full_range" selected="true">Use the full image range</option>
+                <option value="enter_range">Manually enter the image range</option>
+            </param>
+            <when value="full_range"/>
+            <when value="enter_range">
+                <param name="low" type="integer" value="-1" optional="false" label="Lower image range index in x-direction"/>
+                <param name="upp" type="integer" value="-1" optional="false" label="Upper image range index in x-direction"/>
+            </when>
+        </conditional>
+        <conditional name="y_bounds">
+            <param name="type_selector" type="select" label="Choose reconstructed image range in y-direction">
+                <option value="full_range" selected="true">Use the full image range</option>
+                <option value="enter_range">Manually enter the image range</option>
+            </param>
+            <when value="full_range"/>
+            <when value="enter_range">
+                <param name="low" type="integer" value="-1" optional="false" label="Lower image range index in y-direction"/>
+                <param name="upp" type="integer" value="-1" optional="false" label="Upper image range index in y-direction"/>
+            </when>
+        </conditional>
     </inputs>
     <outputs>
         <expand macro="common_outputs"/>
-        <data name="output_data" format="npy" label="Combined tomography stacks" from_work_dir="output_data.npy"/>
-        <collection name="combine_pngs" type="list" label="Recontructed slices midway in each combined dimension">
-            <discover_datasets pattern="__name_and_ext__" directory="combine_pngs"/>
+        <collection name="tomo_combine_plots" type="list" label="Combine recontructed data images">
+            <discover_datasets pattern="__name_and_ext__" directory="tomo_combine_plots"/>
         </collection>
-        <data name="output_config" format="tomo.config.yaml" label="Output config combine reconstruction" from_work_dir="output_config.yaml"/>
+        <data name="output_file" format="nex" label="Reconstructed tomography data" from_work_dir="output.nex"/>
     </outputs>
     <help><![CDATA[
         Combine reconstructed tomography images.
--- a/tomo_find_center.py	Tue Mar 21 17:40:03 2023 +0000
+++ b/tomo_find_center.py	Thu Mar 23 13:39:14 2023 +0000
@@ -14,7 +14,7 @@
 def __main__():
     # Parse command line arguments
     parser = argparse.ArgumentParser(
-            description='Reduce tomography data')
+            description='Find the center axis for a tomography reconstruction')
     parser.add_argument('-i', '--input_file',
             required=True,
             type=pathlib.Path,
@@ -91,5 +91,8 @@
     # stopping memory monitoring
 #    tracemalloc.stop()
 
+    logging.info('Completed find center axis')
+
+
 if __name__ == "__main__":
     __main__()
--- a/tomo_find_center.xml	Tue Mar 21 17:40:03 2023 +0000
+++ b/tomo_find_center.xml	Thu Mar 23 13:39:14 2023 +0000
@@ -1,66 +1,42 @@
-<tool id="tomo_find_center" name="Tomo Find Center Axis" version="0.2.0" python_template_version="3.9">
+<tool id="tomo_find_center" name="Tomo Find Center Axis" version="1.0.0" python_template_version="3.9">
     <description>Find the center axis for a tomography reconstruction</description>
     <macros>
         <import>tomo_macros.xml</import>
     </macros>
-    <expand macro="requirements" />
-    <command detect_errors="exit_code"><![CDATA[
-        mkdir find_center_pngs;
-        $__tool_directory__/tomo_find_center.py
-        -i '$red_stacks'
-        -c '$config'
-        --row_bounds $row_bounds.row_bound_low $row_bounds.row_bound_upp
-        --center_rows $recon_centers.lower_row $recon_centers.upper_row
-        #if str( $set.set_selector ) == "yes"
-            --center_type_selector '$set.center_type.center_type_selector'
-            #if str( $set.center_type.center_type_selector ) == "user"
-                --set_center '$set.center_type.set_center'
+    <expand macro="requirements"/>
+    <command detect_errors="exit_code">
+        <![CDATA[
+            mkdir tomo_find_centers_plots;
+            $__tool_directory__/tomo_find_center.py
+            --input_file "$input_file"
+            --output_file "output.yaml"
+            --galaxy_flag
+            #if str($center_rows.type_selector) == "enter_range"
+                --center_rows $center_rows.low $center_rows.upp
             #end if
-            --set_range '$set.set_range'
-            --set_step '$set.set_step'
-        #end if
-        --output_config 'output_config.yaml'
-        -l '$log'
-    ]]></command>
+            -l "$log"
+        ]]>
+    </command>
     <inputs>
-        <expand macro="common_inputs"/>
-        <param name="red_stacks" type="data" format="npz" optional="false" label="Reduced stacks"/>
-        <section name="row_bounds" title="Reconstruction row bounds">
-            <param name="row_bound_low" type="integer" value="-1" label="Lower bound"/>
-            <param name="row_bound_upp" type="integer" value="-1" label="Upper bound"/>
-        </section>
-        <section name="recon_centers" title="Reconstruction rows to establish center axis">
-            <param name="lower_row" type="integer" value="-1" label="Lower row"/>
-            <param name="upper_row" type="integer" value="-1" label="Upper row"/>
-        </section>
-        <conditional name="set">
-            <param name="set_selector" type="select" label="Reconstruct slices for a set of center positions?">
-                <option value="no" selected="true">No</option>
-                <option value="yes">Yes</option>
+        <param name="input_file" type="data" format="nex" optional="false" label="Reduced tomography data"/>
+        <conditional name="center_rows">
+            <param name="type_selector" type="select" label="Choose find center axis planes">
+                <option value="full_range" selected="true">Use the top and bottom image planes</option>
+                <option value="enter_range">Manually select the image planes</option>
             </param>
-            <when value="no"/>
-            <when value="yes">
-                <conditional name="center_type">
-                    <param name="center_type_selector" argument="--center_type_selector" type="select" label="Choose the center (C) of the set">
-                        <option value="vo" selected="true">Use the center obtained by Nghia Vo’s method</option>
-                        <option value="user">Enter the center of the set</option>
-                    </param>
-                    <when value="vo"/>
-                    <when value="user">
-                        <param name="set_center" argument="--set_center" type="integer" value="0" label="Center (C) of the set in detector pixels (0: center of the detector row)"/>
-                    </when>
-                </conditional>
-                <param name="set_range" argument="--set_range" type="float" value="20" label="Half-width (H) of the set ranch in detector pixels"/>
-                <param name="set_step" argument="--set_step" type="float" value="5" label="Step size (S) in detector pixels (reconstruct slices for centers at [C-H, C-H+S, ..., C+H])"/>
+            <when value="full_range"/>
+            <when value="enter_range">
+                <param name="low" type="integer" value="-1" optional="false" label="Lower plane index in vertical direction"/>
+                <param name="upp" type="integer" value="-1" optional="false" label="Upper plane index in vertical direction"/>
             </when>
         </conditional>
     </inputs>
     <outputs>
         <expand macro="common_outputs"/>
-        <collection name="find_center_pngs" type="list" label="Recontructed slices at various centers">
-            <discover_datasets pattern="__name_and_ext__" directory="find_center_pngs"/>
+        <collection name="tomo_find_centers_plots" type="list" label="Find center images">
+            <discover_datasets pattern="__name_and_ext__" directory="tomo_find_centers_plots"/>
         </collection>
-        <data name="output_config" format="tomo.config.yaml" label="Output config find center" from_work_dir="output_config.yaml"/>
+        <data name="output_file" format="yaml" label="Center axis info" from_work_dir="output.yaml"/>
     </outputs>
     <help><![CDATA[
         Find the center axis for a tomography reconstruction.
--- a/tomo_macros.xml	Tue Mar 21 17:40:03 2023 +0000
+++ b/tomo_macros.xml	Thu Mar 23 13:39:14 2023 +0000
@@ -10,19 +10,13 @@
     <xml name="citations">
         <citations>
             <citation type="bibtex">
-@misc{github_files,
+@misc{chess_tomography,
   author = {Verberg, Rolf},
-  year = {2022},
+  year = {2023},
   title = {Tomo Reconstruction},
 }</citation>
         </citations>
     </xml>
-    <!--
-    <xml name="common_inputs">
-        <param name="config" type='data' format='yaml' optional='false' label="Input config"/>
-        <param name="config" type='data' format='tomo.config.yaml' optional='true' label="Input config"/>
-    </xml>
-    -->
     <xml name="common_outputs">
         <data name="log" format="txt" label="Log"/>
     </xml>
--- a/tomo_reconstruct.py	Tue Mar 21 17:40:03 2023 +0000
+++ b/tomo_reconstruct.py	Thu Mar 23 13:39:14 2023 +0000
@@ -14,7 +14,7 @@
 def __main__():
     # Parse command line arguments
     parser = argparse.ArgumentParser(
-            description='Reduce tomography data')
+            description='Perform a tomography reconstruction')
     parser.add_argument('-i', '--input_file',
             required=True,
             type=pathlib.Path,
@@ -106,5 +106,8 @@
     # stopping memory monitoring
 #    tracemalloc.stop()
 
+    logging.info('Completed tomography reconstruction')
+
+
 if __name__ == "__main__":
     __main__()
--- a/tomo_reconstruct.xml	Tue Mar 21 17:40:03 2023 +0000
+++ b/tomo_reconstruct.xml	Thu Mar 23 13:39:14 2023 +0000
@@ -1,43 +1,58 @@
-<tool id="tomo_reconstruct" name="Tomo Reconstruction" version="0.2.0" python_template_version="3.9">
+<tool id="tomo_reconstruct" name="Tomo Reconstruction" version="1.0.0" python_template_version="3.9">
     <description>Perform a tomography reconstruction</description>
     <macros>
         <import>tomo_macros.xml</import>
     </macros>
-    <expand macro="requirements" />
-    <command detect_errors="exit_code"><![CDATA[
-        mkdir center_slice_pngs;
-        $__tool_directory__/tomo_reconstruct.py
-        -i '$red_stacks'
-        -c '$config'
-        #if str($center_offsets.type_selector) == "offsets_manual"
-            --center_offsets $center_offsets.lower_center_offset $center_offsets.upper_center_offset
-        #end if
-        --output_data 'output_data.npz'
-        --output_config 'output_config.yaml'
-        -l '$log'
-    ]]></command>
+    <expand macro="requirements"/>
+    <command detect_errors="exit_code">
+        <![CDATA[
+            mkdir tomo_reconstruct_plots;
+            $__tool_directory__/tomo_reconstruct.py
+            --input_file "$input_file"
+            --center_file "$center_file"
+            --output_file "output.nex"
+            --galaxy_flag
+            #if str($x_bounds.type_selector) == "enter_range"
+                --x_bounds $x_bounds.low $x_bounds.upp
+            #end if
+            #if str($y_bounds.type_selector) == "enter_range"
+                --y_bounds $y_bounds.low $y_bounds.upp
+            #end if
+            -l "$log"
+        ]]>
+    </command>
     <inputs>
-        <expand macro="common_inputs"/>
-        <param name="red_stacks" type='data' format='npz' optional='false' label="Reduced stacks"/>
-        <conditional name="center_offsets">
-            <param name="type_selector" type="select" label="Read reconstruction center axis offsets from file or enter enter manually">
-                <option value="offsets_from_file" selected="true">Read center axis offsets from file</option>
-                <option value="offsets_manual">Manually enter center axis offsets</option>
+        <param name="input_file" type="data" format="nex" optional="false" label="Reduced tomography data"/>
+        <param name="center_file" type="data" format="yaml" optional="false" label="Center axis input file"/>
+        <conditional name="x_bounds">
+            <param name="type_selector" type="select" label="Choose reconstructed image range in x-direction">
+                <option value="full_range" selected="true">Use the full image range</option>
+                <option value="enter_range">Manually enter the image range</option>
             </param>
-            <when value="offsets_from_file"/>
-            <when value="offsets_manual">
-                <param name="lower_center_offset" type="float" value = "0" optional="false" label="Lower center offset"/>
-                <param name="upper_center_offset" type="float" value = "0" optional="false" label="Upper center offset"/>
+            <when value="full_range"/>
+            <when value="enter_range">
+                <param name="low" type="integer" value="-1" optional="false" label="Lower image range index in x-direction"/>
+                <param name="upp" type="integer" value="-1" optional="false" label="Upper image range index in x-direction"/>
+            </when>
+        </conditional>
+        <conditional name="y_bounds">
+            <param name="type_selector" type="select" label="Choose reconstructed image range in y-direction">
+                <option value="full_range" selected="true">Use the full image range</option>
+                <option value="enter_range">Manually enter the image range</option>
+            </param>
+            <when value="full_range"/>
+            <when value="enter_range">
+                <param name="low" type="integer" value="-1" optional="false" label="Lower image range index in y-direction"/>
+                <param name="upp" type="integer" value="-1" optional="false" label="Upper image range index in y-direction"/>
             </when>
         </conditional>
     </inputs>
     <outputs>
         <expand macro="common_outputs"/>
-        <data name="output_data" format="npz" label="Reconstructed tomography stacks" from_work_dir="output_data.npz"/>
-        <collection name="center_slice_pngs" type="list" label="Recontructed slices midway in each dimension">
-            <discover_datasets pattern="__name_and_ext__" directory="center_slice_pngs"/>
+        <collection name="tomo_reconstruct_plots" type="list" label="Data recontruction images">
+            <discover_datasets pattern="__name_and_ext__" directory="tomo_reconstruct_plots"/>
         </collection>
-        <data name="output_config" format="tomo.config.yaml" label="Output config reconstruction" from_work_dir="output_config.yaml"/>
+        <data name="output_file" format="nex" label="Reconstructed tomography data" from_work_dir="output.nex"/>
     </outputs>
     <help><![CDATA[
         Reconstruct tomography images.
--- a/tomo_reduce.py	Tue Mar 21 17:40:03 2023 +0000
+++ b/tomo_reduce.py	Thu Mar 23 13:39:14 2023 +0000
@@ -18,7 +18,7 @@
     parser.add_argument('-i', '--input_file',
             required=True,
             type=pathlib.Path,
-            help='''Full or relative path to the input file (in yaml or nxs format).''')
+            help='''Full or relative path to the input file (in yaml or Nexus format).''')
     parser.add_argument('-o', '--output_file',
             required=True,
             type=pathlib.Path,
@@ -92,5 +92,8 @@
     # Stop memory monitoring
 #    tracemalloc.stop()
 
+    logging.info('Completed tomography data reduction')
+
+
 if __name__ == "__main__":
     __main__()
--- a/tomo_reduce.xml	Tue Mar 21 17:40:03 2023 +0000
+++ b/tomo_reduce.xml	Thu Mar 23 13:39:14 2023 +0000
@@ -1,29 +1,53 @@
-<tool id="tomo_reduce" name="Tomo Reduce" version="0.1.0" python_template_version="3.9">
+<tool id="tomo_reduce" name="Tomo Reduce" version="1.0.0" python_template_version="3.9">
     <description>Reduce tomography images</description>
     <macros>
         <import>tomo_macros.xml</import>
     </macros>
-    <expand macro="requirements" />
+    <expand macro="requirements"/>
     <command detect_errors="exit_code">
         <![CDATA[
             mkdir tomo_reduce_plots;
             $__tool_directory__/tomo_reduce.py
-            --input_file '$input_file'
-            --output_file 'output.nxs'
+            --input_file "$input_file"
+            --output_file "output.nex"
             --galaxy_flag
-            --img_x_bounds $x_bounds.x_bound_low $x_bounds.x_bound_upp
-            -l '$log'
+            #if str($station.type_selector) == "id3b"
+                #if str($station.img_bounds.type_selector) == "enter_range"
+                    --img_x_bounds $station.img_bounds.low $station.img_bounds.upp
+                #end if
+            #end if
+            -l "$log"
         ]]>
     </command>
     <inputs>
         <param name="input_file" type="data" optional="false" label="Input file"/>
+        <conditional name="station">
+            <param name="type_selector" type="select" display="radio" label="Choose station">
+                <option value="id3a" selected="true">id3a (SMB)</option>
+                <option value="id3b">id3b (FMB)</option>
+            </param>
+            <when value="id3a"/>
+            <when value="id3b">
+                <conditional name="img_bounds">
+                    <param name="type_selector" type="select" display="radio" label="Choose image range for data reduction">
+                        <option value="enter_range" selected="true">Manually enter the image range</option>
+                        <option value="full_range">Use the full image range</option>
+                    </param>
+                    <when value="enter_range">
+                        <param name="low" type="integer" value="-1" optional="false" label="Lower image range index in vertical direction"/>
+                        <param name="upp" type="integer" value="-1" optional="false" label="Upper image range index in vertical direction"/>
+                    </when>
+                    <when value="full_range"/>
+                </conditional>
+            </when>
+        </conditional>
     </inputs>
     <outputs>
         <expand macro="common_outputs"/>
-        <collection name="tomo_reduce_plots" type="list" label="Tomo data reduction images">
+        <collection name="tomo_reduce_plots" type="list" label="Data reduction images">
             <discover_datasets pattern="__name_and_ext__" directory="tomo_reduce_plots"/>
         </collection>
-        <data name="output_file" format="nxs" label="Reduced tomography data" from_work_dir="output.nxs"/>
+        <data name="output_file" format="nex" label="Reduced tomography data" from_work_dir="output.nex"/>
     </outputs>
     <help>
         <![CDATA[
--- a/workflow/run_tomo.py	Tue Mar 21 17:40:03 2023 +0000
+++ b/workflow/run_tomo.py	Thu Mar 23 13:39:14 2023 +0000
@@ -15,7 +15,7 @@
 
 from multiprocessing import cpu_count
 from nexusformat.nexus import *
-from os import mkdir
+from os import mkdir, environ
 from os import path as os_path
 try:
     from skimage.transform import iradon
@@ -99,7 +99,7 @@
             self.num_core = num_core
 
     def __enter__(self):
-        self.num_core_org = ne.set_num_threads(self.num_core)
+        self.num_core_org = ne.set_num_threads(min(self.num_core, ne.MAX_THREADS))
 
     def __exit__(self, exc_type, exc_value, traceback):
         ne.set_num_threads(self.num_core_org)
@@ -164,28 +164,42 @@
             self.num_core= cpu_count()
 
     def read(self, filename):
-        extension = os_path.splitext(filename)[1]
-        if extension == '.yml' or extension == '.yaml':
-            with open(filename, 'r') as f:
-                config = safe_load(f)
-#            if len(config) > 1:
-#                raise ValueError(f'Multiple root entries in {filename} not yet implemented')
-#            if len(list(config.values())[0]) > 1:
-#                raise ValueError(f'Multiple sample maps in {filename} not yet implemented')
-            return(config)
-        elif extension == '.nxs':
-            with NXFile(filename, mode='r') as nxfile:
-                nxroot = nxfile.readfile()
-            return(nxroot)
+        logger.info(f'looking for {filename}')
+        if self.galaxy_flag:
+            try:
+                with open(filename, 'r') as f:
+                    config = safe_load(f)
+                return(config)
+            except:
+                try:
+                    with NXFile(filename, mode='r') as nxfile:
+                        nxroot = nxfile.readfile()
+                    return(nxroot)
+                except:
+                    raise ValueError(f'Unable to open ({filename})')
         else:
-            raise ValueError(f'Invalid filename extension ({extension})')
+            extension = os_path.splitext(filename)[1]
+            if extension == '.yml' or extension == '.yaml':
+                with open(filename, 'r') as f:
+                    config = safe_load(f)
+#                if len(config) > 1:
+#                    raise ValueError(f'Multiple root entries in {filename} not yet implemented')
+#                if len(list(config.values())[0]) > 1:
+#                    raise ValueError(f'Multiple sample maps in {filename} not yet implemented')
+                return(config)
+            elif extension == '.nxs':
+                with NXFile(filename, mode='r') as nxfile:
+                    nxroot = nxfile.readfile()
+                return(nxroot)
+            else:
+                raise ValueError(f'Invalid filename extension ({extension})')
 
     def write(self, data, filename):
         extension = os_path.splitext(filename)[1]
         if extension == '.yml' or extension == '.yaml':
             with open(filename, 'w') as f:
                 safe_dump(data, f)
-        elif extension == '.nxs':
+        elif extension == '.nxs' or extension == '.nex':
             data.save(filename, mode='w')
         elif extension == '.nc':
             data.to_netcdf(os_path=filename)
@@ -287,14 +301,18 @@
         nxentry = nxroot[nxroot.attrs['default']]
         if not isinstance(nxentry, NXentry):
             raise ValueError(f'Invalid nxentry ({nxentry})')
-        if self.galaxy_flag:
-            if center_rows is not None:
-                center_rows = tuple(center_rows)
-                if not is_int_pair(center_rows):
+        if center_rows is not None:
+            if self.galaxy_flag:
+                if not is_int_pair(center_rows, ge=-1):
                     raise ValueError(f'Invalid parameter center_rows ({center_rows})')
-        elif center_rows is not None:
-            logger.warning(f'Ignoring parameter center_rows ({center_rows})')
-            center_rows = None
+                if (center_rows[0] != -1 and center_rows[1] != -1 and
+                        center_rows[0] > center_rows[1]):
+                    center_rows = (center_rows[1], center_rows[0])
+                else:
+                    center_rows = tuple(center_rows)
+            else:
+                logger.warning(f'Ignoring parameter center_rows ({center_rows})')
+                center_rows = None
         if self.galaxy_flag:
             if center_stack_index is not None and not is_int(center_stack_index, ge=0):
                 raise ValueError(f'Invalid parameter center_stack_index ({center_stack_index})')
@@ -360,10 +378,10 @@
         if self.test_mode:
             lower_row = self.test_config['lower_row']
         elif self.galaxy_flag:
-            if center_rows is None:
+            if center_rows is None or center_rows[0] == -1:
                 lower_row = 0
             else:
-                lower_row = min(center_rows)
+                lower_row = center_rows[0]
                 if not 0 <= lower_row < tomo_fields_shape[2]-1:
                     raise ValueError(f'Invalid parameter center_rows ({center_rows})')
         else:
@@ -374,7 +392,6 @@
         logger.debug('Finding center...')
         t0 = time()
         lower_center_offset = self._find_center_one_plane(
-                #np.asarray(nxentry.reduced_data.data.tomo_fields[center_stack_index,:,lower_row,:]),
                 nxentry.reduced_data.data.tomo_fields[center_stack_index,:,lower_row,:],
                 lower_row, thetas, eff_pixel_size, cross_sectional_dim, path=path,
                 num_core=self.num_core)
@@ -386,10 +403,10 @@
         if self.test_mode:
             upper_row = self.test_config['upper_row']
         elif self.galaxy_flag:
-            if center_rows is None:
+            if center_rows is None or center_rows[1] == -1:
                 upper_row = tomo_fields_shape[2]-1
             else:
-                upper_row = max(center_rows)
+                upper_row = center_rows[1]
                 if not lower_row < upper_row < tomo_fields_shape[2]:
                     raise ValueError(f'Invalid parameter center_rows ({center_rows})')
         else:
@@ -499,15 +516,31 @@
         # Resize the reconstructed tomography data
         #   reconstructed data order in each stack: row/z,x,y
         if self.test_mode:
-            x_bounds = self.test_config.get('x_bounds')
-            y_bounds = self.test_config.get('y_bounds')
+            x_bounds = tuple(self.test_config.get('x_bounds'))
+            y_bounds = tuple(self.test_config.get('y_bounds'))
             z_bounds = None
         elif self.galaxy_flag:
-            if x_bounds is not None and not is_int_pair(x_bounds, ge=0,
-                    lt=tomo_recon_stacks[0].shape[1]):
+            x_max = tomo_recon_stacks[0].shape[1]
+            if x_bounds is None:
+                x_bounds = (0, x_max)
+            elif is_int_pair(x_bounds, ge=-1, le=x_max):
+                x_bounds = tuple(x_bounds)
+                if x_bounds[0] == -1:
+                    x_bounds = (0, x_bounds[1])
+                if x_bounds[1] == -1:
+                    x_bounds = (x_bounds[0], x_max)
+            if not is_index_range(x_bounds, ge=0, le=x_max):
                 raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
-            if y_bounds is not None and not is_int_pair(y_bounds, ge=0,
-                    lt=tomo_recon_stacks[0].shape[1]):
+            y_max = tomo_recon_stacks[0].shape[1]
+            if y_bounds is None:
+                y_bounds = (0, y_max)
+            elif is_int_pair(y_bounds, ge=-1, le=y_max):
+                y_bounds = tuple(y_bounds)
+                if y_bounds[0] == -1:
+                    y_bounds = (0, y_bounds[1])
+                if y_bounds[1] == -1:
+                    y_bounds = (y_bounds[0], y_max)
+            if not is_index_range(y_bounds, ge=0, le=y_max):
                 raise ValueError(f'Invalid parameter y_bounds ({y_bounds})')
             z_bounds = None
         else:
@@ -535,17 +568,17 @@
         if num_tomo_stacks == 1:
             basetitle = 'recon'
         else:
-            basetitle = f'recon stack {i+1}'
+            basetitle = f'recon stack'
         for i, stack in enumerate(tomo_recon_stacks):
-            title = f'{basetitle} {res_title} xslice{x_slice}'
+            title = f'{basetitle} {i+1} {res_title} xslice{x_slice}'
             quick_imshow(stack[z_range[0]:z_range[1],x_slice,y_range[0]:y_range[1]],
                     title=title, path=path, save_fig=self.save_figs, save_only=self.save_only,
                     block=self.block)
-            title = f'{basetitle} {res_title} yslice{y_slice}'
+            title = f'{basetitle} {i+1} {res_title} yslice{y_slice}'
             quick_imshow(stack[z_range[0]:z_range[1],x_range[0]:x_range[1],y_slice],
                     title=title, path=path, save_fig=self.save_figs, save_only=self.save_only,
                     block=self.block)
-            title = f'{basetitle} {res_title} zslice{z_slice}'
+            title = f'{basetitle} {i+1} {res_title} zslice{z_slice}'
             quick_imshow(stack[z_slice,x_range[0]:x_range[1],y_range[0]:y_range[1]],
                     title=title, path=path, save_fig=self.save_figs, save_only=self.save_only,
                     block=self.block)
@@ -740,7 +773,7 @@
 
         # Take median
         if tdf_stack.ndim == 2:
-            tdf = tdf_stack
+            tdf = tdf_stack.astype('float64')
         elif tdf_stack.ndim == 3:
             tdf = np.median(tdf_stack, axis=0)
             del tdf_stack
@@ -808,7 +841,7 @@
            We don’t typically account for them but potentially could.
         """
         if tbf_stack.ndim == 2:
-            tbf = tbf_stack
+            tbf = tbf_stack.astype('float64')
         elif tbf_stack.ndim == 3:
             tbf = np.median(tbf_stack, axis=0)
             del tbf_stack
@@ -823,7 +856,7 @@
 
         # Set any non-positive values to one
         # (avoid negative bright field values for spikes in dark field)
-        tbf[tbf < 1] = 1
+        tbf[tbf < 1.0] = 1.0
 
         # Plot bright field
         if self.galaxy_flag:
@@ -873,9 +906,6 @@
 
         # Select image bounds
         title = f'tomography image at theta={round(theta, 2)+0}'
-        if (img_x_bounds is not None and not is_index_range(img_x_bounds, ge=0,
-                le=first_image.shape[0])):
-            raise ValueError(f'Invalid parameter img_x_bounds ({img_x_bounds})')
         if nxentry.instrument.source.attrs['station'] in ('id1a3', 'id3a'):
             pixel_size = nxentry.instrument.detector.x_pixel_size
             # Try to get a fit from the bright field
@@ -977,6 +1007,14 @@
             if self.galaxy_flag:
                 if img_x_bounds is None:
                     img_x_bounds = (0, first_image.shape[0])
+                elif is_int_pair(img_x_bounds, ge=-1, le=first_image.shape[0]):
+                    img_x_bounds = tuple(img_x_bounds)
+                    if img_x_bounds[0] == -1:
+                        img_x_bounds = (0, img_x_bounds[1])
+                    if img_x_bounds[1] == -1:
+                        img_x_bounds = (img_x_bounds[0], first_image.shape[0])
+                if not is_index_range(img_x_bounds, ge=0, le=first_image.shape[0]):
+                    raise ValueError(f'Invalid parameter img_x_bounds ({img_x_bounds})')
             else:
                 quick_imshow(first_image, title=title)
                 print('Select vertical data reduction range from first tomography image')
@@ -1031,23 +1069,29 @@
         """
         # Get full bright field
         tbf = np.asarray(reduced_data.data.bright_field)
-        tbf_shape = tbf.shape
+        img_shape = tbf.shape
 
         # Get image bounds
-        img_x_bounds = tuple(reduced_data.get('img_x_bounds', (0, tbf_shape[0])))
-        img_y_bounds = tuple(reduced_data.get('img_y_bounds', (0, tbf_shape[1])))
+        img_x_bounds = tuple(reduced_data.get('img_x_bounds', (0, img_shape[0])))
+        img_y_bounds = tuple(reduced_data.get('img_y_bounds', (0, img_shape[1])))
+        if img_x_bounds == (0, img_shape[0]) and img_y_bounds == (0, img_shape[1]):
+            resize_flag = False
+        else:
+            resize_flag = True
 
         # Get resized dark field
-#        if 'dark_field' in data:
-#            tbf = np.asarray(reduced_data.data.dark_field[
-#                    img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]])
-#        else:
-#            logger.warning('Dark field unavailable')
-#            tdf = None
-        tdf = None
+        if 'dark_field' in reduced_data.data:
+            if resize_flag:
+                tdf = np.asarray(reduced_data.data.dark_field[
+                        img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]])
+            else:
+                tdf = np.asarray(reduced_data.data.dark_field)
+        else:
+            logger.warning('Dark field unavailable')
+            tdf = None
 
         # Resize bright field
-        if img_x_bounds != (0, tbf.shape[0]) or img_y_bounds != (0, tbf.shape[1]):
+        if resize_flag:
             tbf = tbf[img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]]
 
         # Get the tomography images
@@ -1107,13 +1151,15 @@
         else:
             path = self.output_folder
         for i, tomo_stack in enumerate(tomo_stacks):
-            # Resize the tomography images
-            # Right now the range is the same for each set in the image stack.
-            if img_x_bounds != (0, tbf.shape[0]) or img_y_bounds != (0, tbf.shape[1]):
+            # Resize the tomography images as needed
+            # Right now the range is the same for each set in the image stack
+            if resize_flag:
                 t0 = time()
                 tomo_stack = tomo_stack[:,img_x_bounds[0]:img_x_bounds[1],
                         img_y_bounds[0]:img_y_bounds[1]].astype('float64')
                 logger.debug(f'Resizing tomography images took {time()-t0:.2f} seconds')
+            else:
+                tomo_stack = tomo_stack.astype('float64')
 
             # Subtract dark field
             if tdf is not None: