changeset 71:1cf15b61cd83 draft

planemo upload for repository https://github.com/rolfverberg/galaxytools commit 366e516aef0735af2998c6ff3af037181c8d5213
author rv43
date Mon, 20 Mar 2023 13:56:57 +0000
parents 97c4e2cbbad9
children d5329ca0210d
files fit.py read_image.py read_image.xml run_link_to_galaxy run_tomo_all run_tomo_combine run_tomo_find_center run_tomo_reconstruct run_tomo_reduce sobhani-3249-A.yaml tomo_combine.py tomo_find_center.py tomo_macros.xml tomo_reconstruct.py tomo_reduce.py tomopy_galaxy.yml tomopy_galaxy_tool_dev.yml workflow/__main__.py workflow/link_to_galaxy.py workflow/models.py workflow/run_tomo.py
diffstat 21 files changed, 294 insertions(+), 308 deletions(-) [+]
line wrap: on
line diff
--- a/fit.py	Fri Mar 10 16:39:22 2023 +0000
+++ b/fit.py	Mon Mar 20 13:56:57 2023 +0000
@@ -20,7 +20,10 @@
 from os import cpu_count, getpid, listdir, mkdir, path
 from re import compile, sub
 from shutil import rmtree
-from sympy import diff, simplify
+try:
+    from sympy import diff, simplify
+except:
+    pass
 try:
     from joblib import Parallel, delayed
     have_joblib = True
@@ -32,12 +35,20 @@
 except:
     have_xarray = False
 
-from .general import illegal_value, is_int, is_dict_series, is_index, index_nearest, \
+#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
-#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
 
 from sys import float_info
 float_min = float_info.min
@@ -1810,8 +1821,8 @@
             self.fit(**kwargs)
 
     @classmethod
-    def fit_map(cls, x, ymap, models, normalize=True, **kwargs):
-        return(cls(x, ymap, models, normalize=normalize, **kwargs))
+    def fit_map(cls, ymap, models, x=None, normalize=True, **kwargs):
+        return(cls(ymap, x=x, models=models, normalize=normalize, **kwargs))
 
     @property
     def best_errors(self):
--- a/read_image.py	Fri Mar 10 16:39:22 2023 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-#!/usr/bin/env python3
-
-import logging
-
-import argparse
-import pathlib
-import sys
-
-def __main__():
-
-    # Parse command line arguments
-    parser = argparse.ArgumentParser(
-            description='Read a raw or reconstructed image')
-    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).''')
-    parser.add_argument('--image_type',
-            required=False,
-            help='Image type (dark, bright, tomo_raw, tomo_reduced, or reconstructed')
-    parser.add_argument('--image_index',
-            required=False,
-            type=int,
-            help='Image index (only for raw or reduced images')
-    parser.add_argument('-l', '--log',
-#            type=argparse.FileType('w'),
-            default=sys.stdout,
-            help='Logging stream or filename')
-    parser.add_argument('--log_level',
-            choices=logging._nameToLevel.keys(),
-            default='INFO',
-            help='''Specify a preferred logging level.''')
-    args = parser.parse_args()
-
-    # Set log configuration
-    # When logging to file, the stdout log level defaults to WARNING
-    logging_format = '%(asctime)s : %(levelname)s - %(module)s : %(funcName)s - %(message)s'
-    level = logging.getLevelName(args.log_level)
-    if args.log is sys.stdout:
-        logging.basicConfig(format=logging_format, level=level, force=True,
-                handlers=[logging.StreamHandler()])
-    else:
-        if isinstance(args.log, str):
-            logging.basicConfig(filename=f'{args.log}', filemode='w',
-                    format=logging_format, level=level, force=True)
-        elif isinstance(args.log, io.TextIOWrapper):
-            logging.basicConfig(filemode='w', format=logging_format, level=level,
-                    stream=args.log, force=True)
-        else:
-            raise(ValueError(f'Invalid argument --log: {args.log}'))
-        stream_handler = logging.StreamHandler()
-        logging.getLogger().addHandler(stream_handler)
-        stream_handler.setLevel(logging.WARNING)
-        stream_handler.setFormatter(logging.Formatter(logging_format))
-
-    # Log command line arguments
-    logging.info(f'input_file = {args.input_file}')
-    logging.info(f'image_type = {args.image_type}')
-    logging.info(f'image_index = {args.image_index}')
-    logging.debug(f'log = {args.log}')
-    logging.debug(f'is log stdout? {args.log is sys.stdout}')
-
-    # Instantiate Tomo object
-    tomo = Tomo(galaxy_flag=args.galaxy_flag)
-
-    # Read input file
-    data = tomo.read(args.input_file)
-    print(f'data:\n{data}')
-
-if __name__ == "__main__":
-    __main__()
-
--- a/read_image.xml	Fri Mar 10 16:39:22 2023 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-<tool id="read_image" name="Read and Plot a Tomography Image" version="0.2.0" python_template_version="3.9">
-    <description>Read and plot a raw, reduced  or reconstructed tomography image</description>
-    <macros>
-        <import>tomo_macros.xml</import>
-    </macros>
-    <command detect_errors="exit_code"><![CDATA[
-        $__tool_directory__/read_image.py
-        --input_file '$input_file'
-        --image_index $image_index
-        -l '$log'
-    ]]></command>
-    <inputs>
-        <param name="input_file" type="data" optional="false" label="Input file"/>
-        <param name="image_index" type='integer' value="-1" label="Image index"/>
-    </inputs>
-    <outputs>
-        <expand macro="common_outputs"/>
-    </outputs>
-    <help><![CDATA[
-        Read and plot a raw, reduced  or reconstructed tomography image.
-    ]]></help>
-    <expand macro="citations"/>
-</tool>
--- a/run_link_to_galaxy	Fri Mar 10 16:39:22 2023 +0000
+++ b/run_link_to_galaxy	Mon Mar 20 13:56:57 2023 +0000
@@ -1,6 +1,5 @@
 #!/bin/bash
 
-#python -m workflow link_to_galaxy -i tenstom_1304r-1.nxs -g 'https://galaxy.classe.cornell.edu' -a 'fbea44f58986b87b40bb9315c496e687'
-#python -m workflow link_to_galaxy -i tenstom_1304r-1.nxs -g 'https://galaxy-dev.classe.cornell.edu' -a 'bd404baf78eef76657277f33021d408f'
-
-python -m workflow link_to_galaxy -i sobhani-3249-A.yaml -g 'https://galaxy-dev.classe.cornell.edu' -a 'bd404baf78eef76657277f33021d408f'
+python -m workflow link_to_galaxy -i sobhani-3249-A_start.nxs -g 'https://galaxy-dev.classe.cornell.edu' -a 'bd404baf78eef76657277f33021d408f'
+python -m workflow link_to_galaxy -i tenstom_1304r-1_start.nxs -g 'https://galaxy-dev.classe.cornell.edu' -a 'bd404baf78eef76657277f33021d408f'
+python -m workflow link_to_galaxy -i tenstom_1304r-1_one_stack_start.nxs -g 'https://galaxy-dev.classe.cornell.edu' -a 'bd404baf78eef76657277f33021d408f'
--- a/run_tomo_all	Fri Mar 10 16:39:22 2023 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-#!/bin/bash
-
-# From workflow
-#python -m workflow run_tomo -i sobhani-3249-A.yaml -o sobhani-3249-A.nxs -n 48 -s 'no'
-#python -m workflow run_tomo -i tenstom_1304r-1_one_stack.yaml -o tenstom_1304r-1_one_stack.nxs -n 48 -s 'no'
-python -m workflow run_tomo -i tenstom_1304r-1.yaml -o tenstom_1304r-1.nxs -n 48 -s 'no'
--- a/run_tomo_combine	Fri Mar 10 16:39:22 2023 +0000
+++ b/run_tomo_combine	Mon Mar 20 13:56:57 2023 +0000
@@ -1,4 +1,9 @@
 #!/bin/bash
 
+# From workflow
+#python -m workflow run_tomo -i sobhani-3249-A_recon.nxs -o sobhani-3249-A_recon_combined.nxs -n 48 -s 'only' --combine_data
+#python -m workflow run_tomo -i tenstom_1304r-1_one_stack_recon.nxs -o tenstom_1304r-1_one_stack_recon_combined.nxs -n 48 -s 'only' --combine_data
+#python -m workflow run_tomo -i tenstom_1304r-1_recon.nxs -o tenstom_1304r-1_recon_combined.nxs -n 48 -s 'only' --combine_data
+
 # As Galaxy tool:
-python tomo_combine.py -i tenstom_1304r-1_recon.nxs -o tenstom_1304r-1_recon_combined.nxs --log_level INFO
+python tomo_combine.py -i tenstom_1304r-1_recon.nxs -o tenstom_1304r-1_recon_combined.nxs --log_level INFO -l tomo.log --galaxy_flag
--- a/run_tomo_find_center	Fri Mar 10 16:39:22 2023 +0000
+++ b/run_tomo_find_center	Mon Mar 20 13:56:57 2023 +0000
@@ -1,9 +1,11 @@
 #!/bin/bash
 
 # From workflow
-python -m workflow run_tomo -i sobhani-3249-A_reduce.nxs -o sobhani-3249-A_centers.yaml -n 48 -s 'only' --find_center
+#python -m workflow run_tomo -i sobhani-3249-A_reduce.nxs -o sobhani-3249-A_centers.yaml -n 48 -s 'only' --find_center
+#python -m workflow run_tomo -i tenstom_1304r-1_one_stack_reduce.nxs -o tenstom_1304r-1_one_stack_centers.yaml -n 48 -s 'only' --find_center
+#python -m workflow run_tomo -i tenstom_1304r-1_reduce.nxs -o tenstom_1304r-1_centers.yaml -n 48 -s 'only' --find_center
 
 # As Galaxy tool:
-#python tomo_find_center.py -i sobhani-3249-A_reduce.nxs -o sobhani-3249-A_centers.yaml --log_level INFO -l tomo.log --galaxy_flag --center_rows 50 270
-#python tomo_find_center.py -i tenstom_1304r-1_one_stack_reduce.nxs -o tenstom_1304r-1_one_stack_centers.yaml --log_level INFO
-#python tomo_find_center.py -i tenstom_1304r-1_reduce.nxs -o tenstom_1304r-1_centers.yaml --log_level INFO
+python tomo_find_center.py -i sobhani-3249-A_reduce.nxs -o sobhani-3249-A_centers.yaml --log_level INFO -l tomo.log --galaxy_flag --center_rows 50 270
+python tomo_find_center.py -i tenstom_1304r-1_one_stack_reduce.nxs -o tenstom_1304r-1_one_stack_centers.yaml --log_level INFO -l tomo.log --galaxy_flag --center_rows 75 850
+python tomo_find_center.py -i tenstom_1304r-1_reduce.nxs -o tenstom_1304r-1_centers.yaml --log_level INFO -l tomo.log --galaxy_flag
--- a/run_tomo_reconstruct	Fri Mar 10 16:39:22 2023 +0000
+++ b/run_tomo_reconstruct	Mon Mar 20 13:56:57 2023 +0000
@@ -1,9 +1,11 @@
 #!/bin/bash
 
 # From workflow
-python -m workflow run_tomo -i sobhani-3249-A_reduce.nxs -c sobhani-3249-A_centers.yaml -o sobhani-3249-A_recon.nxs -n 48 -s 'only' --reconstruct_data
+#python -m workflow run_tomo -i sobhani-3249-A_reduce.nxs -c sobhani-3249-A_centers.yaml -o sobhani-3249-A_recon.nxs -n 48 -s 'only' --reconstruct_data
+#python -m workflow run_tomo -i tenstom_1304r-1_one_stack_reduce.nxs -c tenstom_1304r-1_one_stack_centers.yaml -o tenstom_1304r-1_one_stack_recon.nxs -n 48 -s 'only' --reconstruct_data
+#python -m workflow run_tomo -i tenstom_1304r-1_reduce.nxs -c tenstom_1304r-1_centers.yaml -o tenstom_1304r-1_recon.nxs -n 48 -s 'only' --reconstruct_data
 
 # As Galaxy tool:
-#python tomo_reconstruct.py -i sobhani-3249-A_reduce.nxs -c sobhani-3249-A_centers.yaml -o sobhani-3249-A_recon.nxs --log_level INFO -l tomo.log --galaxy_flag --x_bounds 650 1050 --y_bounds 270 1430
-#python tomo_reconstruct.py -i tenstom_1304r-1_one_stack_reduce.nxs -c tenstom_1304r-1_one_stack_centers.yaml -o tenstom_1304r-1_one_stack_recon.nxs --log_level INFO
-#python tomo_reconstruct.py -i tenstom_1304r-1_reduce.nxs -c tenstom_1304r-1_centers.yaml -o tenstom_1304r-1_recon.nxs --log_level INFO
+python tomo_reconstruct.py -i sobhani-3249-A_reduce.nxs -c sobhani-3249-A_centers.yaml -o sobhani-3249-A_recon.nxs --log_level INFO -l tomo.log --galaxy_flag --x_bounds 650 1050 --y_bounds 270 1430
+python tomo_reconstruct.py -i tenstom_1304r-1_one_stack_reduce.nxs -c tenstom_1304r-1_one_stack_centers.yaml -o tenstom_1304r-1_one_stack_recon.nxs --log_level INFO -l tomo.log --galaxy_flag --x_bounds 629 1450 --y_bounds 606 1468
+python tomo_reconstruct.py -i tenstom_1304r-1_reduce.nxs -c tenstom_1304r-1_centers.yaml -o tenstom_1304r-1_recon.nxs --log_level INFO -l tomo.log --galaxy_flag --x_bounds 629 1450 --y_bounds 606 1468
--- a/run_tomo_reduce	Fri Mar 10 16:39:22 2023 +0000
+++ b/run_tomo_reduce	Mon Mar 20 13:56:57 2023 +0000
@@ -1,9 +1,11 @@
 #!/bin/bash
 
 # From workflow
-python -m workflow run_tomo -i sobhani-3249-A.yaml -o sobhani-3249-A_reduce.nxs -n 48 -s 'only' --reduce_data
+#python -m workflow run_tomo -i sobhani-3249-A.yaml -o sobhani-3249-A_reduce.nxs -n 48 -s 'only' --reduce_data
+#python -m workflow run_tomo -i tenstom_1304r-1_one_stack.yaml -o tenstom_1304r-1_one_stack_reduce.nxs -n 48 -s 'only' --reduce_data
+#python -m workflow run_tomo -i tenstom_1304r-1.yaml -o tenstom_1304r-1_reduce.nxs -n 48 -s 'only' --reduce_data
 
 # As Galaxy tool:
-#python tomo_reduce.py -i sobhani-3249-A.yaml -o sobhani-3249-A_reduce.nxs --log_level INFO -l tomo.log --galaxy_flag --img_x_bounds 620 950
-#python tomo_reduce.py -i tenstom_1304r-1_one_stack.yaml -o tenstom_1304r-1_one_stack_reduce.nxs --log_level INFO -l tomo.log --galaxy_flag
-#python tomo_reduce.py -i tenstom_1304r-1.yaml -o tenstom_1304r-1_reduce.nxs --log_level INFO -l tomo.log --galaxy_flag #--img_x_bounds 713 1388
+python tomo_reduce.py -i sobhani-3249-A_start.nxs -o sobhani-3249-A_reduce.nxs --log_level INFO -l tomo.log --galaxy_flag --img_x_bounds 620 950
+python tomo_reduce.py -i tenstom_1304r-1_one_stack_start.nxs -o tenstom_1304r-1_one_stack_reduce.nxs --log_level INFO -l tomo.log --galaxy_flag #--img_x_bounds 582 1509
+python tomo_reduce.py -i tenstom_1304r-1_start.nxs -o tenstom_1304r-1_reduce.nxs --log_level INFO -l tomo.log --galaxy_flag #--img_x_bounds 713 1388
--- a/sobhani-3249-A.yaml	Fri Mar 10 16:39:22 2023 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-sample_maps:
-- cycle: 2022-1
-  btr: sobhani-3249-A
-  title: sobhani-3249-A
-  station: id3b
-  sample:
-    name: tomo7C
-  detector:
-    prefix: andor2
-    rows: 1436
-    columns: 1700
-    pixel_size:
-    - 0.0065
-    - 0.0065
-    lens_magnification: 5.0
-  tomo_fields:
-    spec_file: /nfs/chess/scratch/user/rv43/2022-1/id3b/sobhani-3249-A/tomo7C/tomo7C
-    scan_numbers:
-    - 1
-    stack_info:
-    - scan_number: 1
-      starting_image_offset: 5
-      num_image: 360
-      ref_x: 0.0
-      ref_z: 6.1845
-    theta_range:
-      start: 0.0
-      end: 180.0
-      num: 360
-      start_index: 4
-  bright_field:
-    spec_file: /nfs/chess/scratch/user/rv43/2022-1/id3b/sobhani-3249-A/tomo7C/tomo7C_flat
-    scan_numbers:
-    - 1
-    stack_info:
-    - scan_number: 1
-      starting_image_offset: 1
-      num_image: 20
-      ref_x: 0.0
-      ref_z: 6.1845
--- a/tomo_combine.py	Fri Mar 10 16:39:22 2023 +0000
+++ b/tomo_combine.py	Mon Mar 20 13:56:57 2023 +0000
@@ -23,6 +23,9 @@
             required=False,
             type=pathlib.Path,
             help='''Full or relative path to the output file (in yaml format).''')
+    parser.add_argument('--galaxy_flag',
+            action='store_true',
+            help='''Use this flag to run the scripts as a galaxy tool.''')
     parser.add_argument('-l', '--log',
 #            type=argparse.FileType('w'),
             default=sys.stdout,
@@ -31,6 +34,16 @@
             choices=logging._nameToLevel.keys(),
             default='INFO',
             help='''Specify a preferred logging level.''')
+    parser.add_argument('--x_bounds',
+            required=False,
+            nargs=2,
+            type=int,
+            help='''Boundaries of reconstructed images in x-direction.''')
+    parser.add_argument('--y_bounds',
+            required=False,
+            nargs=2,
+            type=int,
+            help='''Boundaries of reconstructed images in y-direction.''')
     args = parser.parse_args()
 
     # Set log configuration
@@ -63,18 +76,21 @@
     logging.debug(f'log = {args.log}')
     logging.debug(f'is log stdout? {args.log is sys.stdout}')
     logging.debug(f'log_level = {args.log_level}')
+    logging.info(f'x_bounds = {args.x_bounds}')
+    logging.info(f'y_bounds = {args.y_bounds}')
 
     # Instantiate Tomo object
-    tomo = Tomo()
+    tomo = Tomo(galaxy_flag=args.galaxy_flag)
 
     # Read input file
     data = tomo.read(args.input_file)
 
     # Combine the reconstructed tomography stacks
-    data = tomo.combine_data(data)
+    data = tomo.combine_data(data, x_bounds=args.x_bounds, y_bounds=args.y_bounds)
 
     # Write output file
-    data = tomo.write(data, args.output_file)
+    if data is not None:
+        data = tomo.write(data, args.output_file)
 
     # Displaying memory usage
 #    logging.info(f'Memory usage: {tracemalloc.get_traced_memory()}')
--- a/tomo_find_center.py	Fri Mar 10 16:39:22 2023 +0000
+++ b/tomo_find_center.py	Mon Mar 20 13:56:57 2023 +0000
@@ -24,7 +24,6 @@
             type=pathlib.Path,
             help='''Full or relative path to the output file (in yaml format).''')
     parser.add_argument('--center_rows',
-            required=True,
             nargs=2,
             type=int,
             help='''Center finding rows.''')
@@ -81,7 +80,7 @@
     data = tomo.read(args.input_file)
 
     # Find the calibrated center axis info
-    data = tomo.find_centers(data, center_rows=tuple(args.center_rows))
+    data = tomo.find_centers(data, center_rows=args.center_rows)
 
     # Write output file
     data = tomo.write(data, args.output_file)
--- a/tomo_macros.xml	Fri Mar 10 16:39:22 2023 +0000
+++ b/tomo_macros.xml	Mon Mar 20 13:56:57 2023 +0000
@@ -2,8 +2,9 @@
     <xml name="requirements">
         <requirements>
             <requirement type="package" version="1.0.3">lmfit</requirement>
+            <requirement type="package" version="3.5.2">matplotlib</requirement>
             <requirement type="package" version="1.0.0">nexusformat</requirement>
-            <requirement type="package" version="1.11.0">tomopy</requirement>
+            <requirement type="package" version="1.12.2">tomopy</requirement>
         </requirements>
     </xml>
     <xml name="citations">
--- a/tomo_reconstruct.py	Fri Mar 10 16:39:22 2023 +0000
+++ b/tomo_reconstruct.py	Mon Mar 20 13:56:57 2023 +0000
@@ -22,11 +22,11 @@
     parser.add_argument('-c', '--center_file',
             required=True,
             type=pathlib.Path,
-            help='''Full or relative path to the center info file (in Nexus format).''')
+            help='''Full or relative path to the center info file (in yaml format).''')
     parser.add_argument('-o', '--output_file',
             required=False,
             type=pathlib.Path,
-            help='''Full or relative path to the output file (in yaml format).''')
+            help='''Full or relative path to the output file (in Nexus format).''')
     parser.add_argument('--galaxy_flag',
             action='store_true',
             help='''Use this flag to run the scripts as a galaxy tool.''')
--- a/tomo_reduce.py	Fri Mar 10 16:39:22 2023 +0000
+++ b/tomo_reduce.py	Mon Mar 20 13:56:57 2023 +0000
@@ -20,7 +20,7 @@
             type=pathlib.Path,
             help='''Full or relative path to the input file (in yaml or nxs format).''')
     parser.add_argument('-o', '--output_file',
-            required=False,
+            required=True,
             type=pathlib.Path,
             help='''Full or relative path to the output file (in Nexus format).''')
     parser.add_argument('--galaxy_flag',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tomopy_galaxy.yml	Mon Mar 20 13:56:57 2023 +0000
@@ -0,0 +1,11 @@
+name: tomopy_galaxy
+channels:
+  - conda-forge
+  - defaults
+dependencies:
+  - lmfit==1.0.3
+  - matplotlib==3.5.2
+  - nexusformat==1.0.0
+  - pip==22.3.1
+  - python==3.9.13
+  - tomopy==1.12.2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tomopy_galaxy_tool_dev.yml	Mon Mar 20 13:56:57 2023 +0000
@@ -0,0 +1,16 @@
+name: tomopy_galaxy_tool_dev
+channels:
+  - conda-forge
+  - defaults
+dependencies:
+#  - ephemeris
+  - lmfit==1.1.0
+  - matplotlib==3.7.1
+  - nexusformat==1.0.0
+  - pip==23.0.1
+  - pydantic==1.10.6
+  - python==3.9.13
+  - tomopy==1.12.2
+  - pip:
+    - bioblend==1.1.1
+    - planemo==0.75.7
--- a/workflow/__main__.py	Fri Mar 10 16:39:22 2023 +0000
+++ b/workflow/__main__.py	Mon Mar 20 13:56:57 2023 +0000
@@ -202,9 +202,9 @@
         const='reconstruct_data',
         action='append_const',
         help='''Use this flag to create and add reconstructed data data to the input file.''')
-tomo_parser.add_argument('--combine_datas',
+tomo_parser.add_argument('--combine_data',
         dest='modes',
-        const='combine_datas',
+        const='combine_data',
         action='append_const',
         help='''Use this flag to combine reconstructed data data and add to the input file.''')
 
--- a/workflow/link_to_galaxy.py	Fri Mar 10 16:39:22 2023 +0000
+++ b/workflow/link_to_galaxy.py	Mon Mar 20 13:56:57 2023 +0000
@@ -37,10 +37,12 @@
 def link_to_galaxy(filename:str, galaxy=None, user=None, password=None, api_key=None) -> None:
     # Read input file
     extension = path.splitext(filename)[1]
-    if extension == '.yml' or extension == '.yaml':
-        with open(filename, 'r') as f:
-            data = safe_load(f)
-    elif extension == '.nxs':
+# RV yaml input not incorporated yet, since Galaxy can't use pyspec right now
+#    if extension == '.yml' or extension == '.yaml':
+#        with open(filename, 'r') as f:
+#            data = safe_load(f)
+#    elif extension == '.nxs':
+    if extension == '.nxs':
         with NXFile(filename, mode='r') as nxfile:
             data = nxfile.readfile()
     else:
@@ -53,7 +55,9 @@
         nxroot = NXroot()
         for sample_map in wf.sample_maps:
             import_scanparser(sample_map.station)
-            sample_map.construct_nxentry(nxroot, include_raw_data=False)
+# RV raw data must be included, since Galaxy can't use pyspec right now
+#            sample_map.construct_nxentry(nxroot, include_raw_data=False)
+            sample_map.construct_nxentry(nxroot, include_raw_data=True)
         nxentry = nxroot[nxroot.attrs['default']]
     elif isinstance(data, NXroot):
         nxentry = data[data.attrs['default']]
--- a/workflow/models.py	Fri Mar 10 16:39:22 2023 +0000
+++ b/workflow/models.py	Mon Mar 20 13:56:57 2023 +0000
@@ -11,9 +11,9 @@
 
 from functools import cache
 from pathlib import PosixPath
-from pydantic import validator, ValidationError, conint, confloat, constr, \
-        conlist, FilePath, PrivateAttr
 from pydantic import BaseModel as PydanticBaseModel
+from pydantic import validator, ValidationError, conint, confloat, constr, conlist, FilePath, \
+        PrivateAttr
 from nexusformat.nexus import *
 from time import time
 from typing import Optional, Literal
@@ -23,17 +23,35 @@
 except:
     pass
 
-from msnctools.general import is_int, is_num, input_int, input_int_list, input_num, input_yesno, \
-        input_menu, index_nearest, string_to_list, file_exists_and_readable
+try:
+    from msnctools.general import is_int, is_num, input_int, input_int_list, input_num, \
+            input_yesno, input_menu, index_nearest, string_to_list, file_exists_and_readable
+except:
+    from general import is_int, is_num, input_int, input_int_list, input_num, \
+            input_yesno, input_menu, index_nearest, string_to_list, file_exists_and_readable
 
 
 def import_scanparser(station):
     if station in ('id1a3', 'id3a'):
-        from msnctools.scanparsers import SMBRotationScanParser
-        globals()['ScanParser'] = SMBRotationScanParser
+        try:
+            from msnctools.scanparsers import SMBRotationScanParser
+            globals()['ScanParser'] = SMBRotationScanParser
+        except:
+            try:
+                from scanparsers import SMBRotationScanParser
+                globals()['ScanParser'] = SMBRotationScanParser
+            except:
+                pass
     elif station in ('id3b'):
-        from msnctools.scanparsers import FMBRotationScanParser
-        globals()['ScanParser'] = FMBRotationScanParser
+        try:
+            from msnctools.scanparsers import FMBRotationScanParser
+            globals()['ScanParser'] = FMBRotationScanParser
+        except:
+            try:
+                from scanparsers import FMBRotationScanParser
+                globals()['ScanParser'] = FMBRotationScanParser
+            except:
+                pass
     else:
         raise RuntimeError(f'Invalid station: {station}')
 
@@ -45,10 +63,9 @@
         try:
             parser = ScanParser(spec_file, scan_number)
             try:
-                scan_type = parser.get_scan_type()
+                scan_type = parser.scan_type
             except:
                 scan_type = None
-                pass
         except:
             scan_numbers.remove(scan_number)
     return(scan_numbers)
@@ -434,15 +451,6 @@
     def get_scanparser(self, scan_number):
         return(get_scanparser(self.spec_file, scan_number))
 
-#    def get_detector_data(self, detector_prefix, scan_number=None, scan_step_index=None):
-#        if scan_number is None:
-#            scan_number = self.scan_numbers[0]
-#        if scan_step_index is None:
-#            scan_info = self.stack_info[self.get_scan_index(scan_number)]
-#            scan_step_index = scan_info['starting_image_offset']
-#        parser = self.get_scanparser(scan_number)
-#        return(parser.get_detector_data(detector_prefix, scan_step_index))
-
     def get_detector_data(self, detector_prefix, scan_number=None, scan_step_index=None):
         image_stacks = []
         if scan_number is None:
@@ -460,10 +468,13 @@
             else:
                 image_stacks.append(parser.get_detector_data(detector_prefix,
                         image_offset+scan_step_index))
-        if len(image_stacks) == 1:
+        if scan_number is not None and scan_step_index is not None:
+            # Return a single image for a specific scan_number and scan_step_index request
             return(image_stacks[0])
         else:
+            # Return a list otherwise
             return(image_stacks)
+        return(image_stacks)
 
     def scan_numbers_cli(self, attr_desc, **kwargs):
         available_scan_numbers = self.available_scan_numbers
@@ -475,8 +486,11 @@
                 available_scan_numbers = []
                 for scan_number in self.available_scan_numbers:
                     parser = self.get_scanparser(scan_number)
-                    if parser.scan_type == scan_type:
-                        available_scan_numbers.append(scan_number)
+                    try:
+                        if parser.scan_type == scan_type:
+                            available_scan_numbers.append(scan_number)
+                    except:
+                        pass
             elif scan_type == 'df1':
                 tomo_scan_numbers = kwargs['tomo_scan_numbers']
                 available_scan_numbers = []
@@ -587,8 +601,8 @@
             scan_index = self.get_scan_index(scan_number)
 
             # Select the image set
-            last_image_index = image_offset+num_image-1
-            print(f'Available good image set index range: [{image_offset}, {last_image_index}]')
+            last_image_index = image_offset+num_image
+            print(f'Available good image set index range: [{image_offset}, {last_image_index})')
             image_set_approved = False
             if scan_index is not None:
                 scan_info = stack_info[scan_index]
@@ -597,16 +611,16 @@
                 image_set_approved = input_yesno(f'Accept these values (y/n)?', 'y')
             if not image_set_approved:
                 print(f'Default starting image offset and number of images: '+
-                        f'{image_offset} and {last_image_index-image_offset}')
+                        f'{image_offset} and {num_image}')
                 image_set_approved = input_yesno(f'Accept these values (y/n)?', 'y')
                 if image_set_approved:
                     offset = image_offset
                     num = last_image_index-offset
                 while not image_set_approved:
                     offset = input_int(f'Enter the starting image offset', ge=image_offset,
-                            le=last_image_index-1)#, default=image_offset)
+                            lt=last_image_index)#, default=image_offset)
                     num = input_int(f'Enter the number of images', ge=1,
-                            le=last_image_index-offset+1)#, default=last_image_index-offset+1)
+                            le=last_image_index-offset)#, default=last_image_index-offset)
                     print(f'Current starting image offset and number of images: {offset} and {num}')
                     image_set_approved = input_yesno(f'Accept these values (y/n)?', 'y')
                 if scan_index is not None:
@@ -688,7 +702,7 @@
             scan_numbers = [scan_number]
         for scan_number in scan_numbers:
             parser = self.get_scanparser(scan_number)
-            horizontal_shifts.append(parser.get_horizontal_shift())
+            horizontal_shifts.append(parser.horizontal_shift)
         if len(horizontal_shifts) == 1:
             return(horizontal_shifts[0])
         else:
@@ -702,7 +716,7 @@
             scan_numbers = [scan_number]
         for scan_number in scan_numbers:
             parser = self.get_scanparser(scan_number)
-            vertical_shifts.append(parser.get_vertical_shift())
+            vertical_shifts.append(parser.vertical_shift)
         if len(vertical_shifts) == 1:
             return(vertical_shifts[0])
         else:
@@ -728,22 +742,20 @@
             return
 
         # Select the theta range for the tomo reconstruction from the first scan
+        theta_range_approved = False
         thetas = np.linspace(spec_theta_start, spec_theta_end, spec_num_theta)
         delta_theta = thetas[1]-thetas[0]
-        theta_range_approved = False
-        print(f'Theta range obtained from SPEC data: [{spec_theta_start}, {spec_theta_end})')
+        print(f'Theta range obtained from SPEC data: [{spec_theta_start}, {spec_theta_end}]')
         print(f'Theta step size = {delta_theta}')
         print(f'Number of theta values: {spec_num_theta}')
-        exit('Done')
         default_start = None
         default_end = None
         if station in ('id1a3', 'id3a'):
             theta_range_approved = input_yesno(f'Accept this theta range (y/n)?', 'y')
             if theta_range_approved:
-                theta_start = spec_theta_start
-                theta_end = spec_theta_end
-                num_theta = spec_num_theta
-                theta_index_start = 0
+                self.theta_range = {'start': float(spec_theta_start), 'end': float(spec_theta_end),
+                        'num': int(spec_num_theta), 'start_index': 0}
+                return
         elif station in ('id3b'):
             if spec_theta_start <= 0.0 and spec_theta_end >= 180.0:
                 default_start = 0
@@ -765,7 +777,8 @@
                     f'{theta_end})')
             print(f'Number of theta values: {num_theta}')
             theta_range_approved = input_yesno(f'Accept this theta range (y/n)?', 'y')
-        self.thetas = np.linspace(theta_start, theta_end, num_theta)
+        self.theta_range = {'start': float(theta_start), 'end': float(theta_end),
+                'num': int(num_theta), 'start_index': int(theta_index_start)}
 
     def image_range_cli(self, attr_desc, detector_prefix):
         stack_info = self.stack_info
@@ -805,13 +818,17 @@
         btr = cli_kwargs.get('btr')
         station = cli_kwargs.get('station')
         detector = cli_kwargs.get('detector')
+        sample_name = cli_kwargs.get('sample_name')
         print(f'\n -- Configure the location of the {attr_desc}scan data -- ')
         if station in ('id1a3', 'id3a'):
             basedir = f'/nfs/chess/{station}/{cycle}/{btr}'
             runs = [d for d in os.listdir(basedir) if os.path.isdir(os.path.join(basedir, d))]
 #RV            index = 15-1
 #RV            index = 7-1
-            index = input_menu(runs, header='Choose a sample directory')
+            if sample_name is not None and sample_name in runs:
+                index = runs.index(sample_name)
+            else:
+                index = input_menu(runs, header='Choose a sample directory')
             self.spec_file = f'{basedir}/{runs[index]}/spec.log'
             self.scan_numbers_cli(attr_desc, station=station, scan_type='ts1')
         else:
@@ -845,8 +862,8 @@
 
     def cli(self):
         print('\n -- Configure the sample metadata -- ')
-#RV        self.name = 'test'
 #RV        self.name = 'sobhani-3249-A'
+#RV        self.name = 'tenstom_1304r-1'
         self.set_single_attr_cli('name', 'the sample name')
 #RV        self.description = 'test sample'
         self.set_single_attr_cli('description', 'a description of the sample (optional)')
@@ -917,17 +934,19 @@
             use_detector_config = input_yesno(f'Current detector settings:\n{self.detector}\n'+
                     f'Keep these settings? (y/n)')
         if not use_detector_config:
-#RV            have_detector_config = True
-            have_detector_config = input_yesno(f'Is a detector configuration file available? (y/n)')
-            if have_detector_config:
-#RV                detector_config_file = 'retiga.yaml'
-#RV                detector_config_file = 'andor2.yaml'
-                detector_config_file = input(f'Enter detector configuration file name: ')
+            menu_options = ['not listed', 'andor2', 'manta', 'retiga']
+            input_mode = input_menu(menu_options, header='Choose one of the following detector '+
+                    'configuration options')
+            if input_mode:
+                detector_config_file = f'{menu_options[input_mode]}.yaml'
                 have_detector_config = self.detector.construct_from_yaml(detector_config_file)
+            else:
+                have_detector_config = False
             if not have_detector_config:
                 self.set_single_attr_cli('detector', 'detector')
         self.set_single_attr_cli('tomo_fields', 'Tomo field', chain_attr_desc=True,
-                cycle=self.cycle, btr=self.btr, station=self.station, detector=self.detector)
+                cycle=self.cycle, btr=self.btr, station=self.station, detector=self.detector,
+                sample_name=self.sample.name)
         if self.station in ('id1a3', 'id3a'):
             have_dark_field = True
             tomo_spec_file = self.tomo_fields.spec_file
@@ -1001,7 +1020,7 @@
             nxspec_scans[field_name] = field.construct_nxcollection(image_key, thetas,
                     self.detector)
             if include_raw_data:
-                image_stacks.append(field.get_detector_data(self.detector.prefix))
+                image_stacks += field.get_detector_data(self.detector.prefix)
                 for scan_number in field.scan_numbers:
                     parser = field.get_scanparser(scan_number)
                     scan_info = field.stack_info[field.get_scan_index(scan_number)]
--- a/workflow/run_tomo.py	Fri Mar 10 16:39:22 2023 +0000
+++ b/workflow/run_tomo.py	Mon Mar 20 13:56:57 2023 +0000
@@ -32,13 +32,24 @@
     pass
 from yaml import safe_load, safe_dump
 
-from msnctools.fit import Fit
-from msnctools.general import illegal_value, is_int, is_int_pair, is_num, is_index_range, \
-        input_int, input_num, input_yesno, input_menu, draw_mask_1d, select_image_bounds, \
-        select_one_image_bound, clear_imshow, quick_imshow, clear_plot, quick_plot
+try:
+    from msnctools.fit import Fit
+except:
+    from fit import Fit
+try:
+    from msnctools.general import illegal_value, is_int, is_int_pair, is_num, is_index_range, \
+            input_int, input_num, input_yesno, input_menu, draw_mask_1d, select_image_bounds, \
+            select_one_image_bound, clear_imshow, quick_imshow, clear_plot, quick_plot
+except:
+    from general import illegal_value, is_int, is_int_pair, is_num, is_index_range, \
+            input_int, input_num, input_yesno, input_menu, draw_mask_1d, select_image_bounds, \
+            select_one_image_bound, clear_imshow, quick_imshow, clear_plot, quick_plot
 
-from workflow.models import import_scanparser, FlatField, TomoField, TomoWorkflow
-from workflow.__version__ import __version__
+try:
+    from workflow.models import import_scanparser, FlatField, TomoField, TomoWorkflow
+    from workflow.__version__ import __version__
+except:
+    pass
 
 num_core_tomopy_limit = 24
 
@@ -266,7 +277,7 @@
  
         return(nxroot)
 
-    def find_centers(self, nxroot, center_rows=None):
+    def find_centers(self, nxroot, center_rows=None, center_stack_index=None):
         """Find the calibrated center axis info
         """
         logger.info('Find the calibrated center axis info')
@@ -277,13 +288,19 @@
         if not isinstance(nxentry, NXentry):
             raise ValueError(f'Invalid nxentry ({nxentry})')
         if self.galaxy_flag:
-            if center_rows is None:
-                raise ValueError(f'Missing parameter center_rows ({center_rows})')
-            if not is_int_pair(center_rows):
-                raise ValueError(f'Invalid parameter center_rows ({center_rows})')
+            if center_rows is not None:
+                center_rows = tuple(center_rows)
+                if not is_int_pair(center_rows):
+                    raise ValueError(f'Invalid parameter center_rows ({center_rows})')
         elif center_rows is not None:
-            logging.warning(f'Ignoring parameter center_rows ({center_rows})')
+            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})')
+        elif center_stack_index is not None:
+            logger.warning(f'Ignoring parameter center_stack_index ({center_stack_index})')
+            center_stack_index = None
 
         # Create plot galaxy path directory and path if needed
         if self.galaxy_flag:
@@ -298,26 +315,28 @@
             raise KeyError(f'Unable to find valid reduced data in {nxentry}.')
 
         # Select the image stack to calibrate the center axis
-        #   reduced data axes order: stack,row,theta,column
+        #   reduced data axes order: stack,theta,row,column
         #   Note: Nexus cannot follow a link if the data it points to is too big,
         #         so get the data from the actual place, not from nxentry.data
-        num_tomo_stacks = nxentry.reduced_data.data.tomo_fields.shape[0]
+        tomo_fields_shape = nxentry.reduced_data.data.tomo_fields.shape
+        if len(tomo_fields_shape) != 4 or any(True for dim in tomo_fields_shape if not dim):
+            raise KeyError('Unable to load the required reduced tomography stack')
+        num_tomo_stacks = tomo_fields_shape[0]
         if num_tomo_stacks == 1:
             center_stack_index = 0
-            center_stack = np.asarray(nxentry.reduced_data.data.tomo_fields[0])
-            if not center_stack.size:
-                raise KeyError('Unable to load the required reduced tomography stack')
             default = 'n'
         else:
             if self.test_mode:
                 center_stack_index = self.test_config['center_stack_index']-1 # make offset 0
+            elif self.galaxy_flag:
+                if center_stack_index is None:
+                    center_stack_index = int(num_tomo_stacks/2)
+                if center_stack_index >= num_tomo_stacks:
+                    raise ValueError(f'Invalid parameter center_stack_index ({center_stack_index})')
             else:
                 center_stack_index = input_int('\nEnter tomography stack index to calibrate the '
-                        'center axis', ge=0, le=num_tomo_stacks-1, default=int(num_tomo_stacks/2))
-            center_stack = \
-                    np.asarray(nxentry.reduced_data.data.tomo_fields[center_stack_index])
-            if not center_stack.size:
-                raise KeyError('Unable to load the required reduced tomography stack')
+                        'center axis', ge=1, le=num_tomo_stacks, default=int(1+num_tomo_stacks/2))
+                center_stack_index -= 1
             default = 'y'
 
         # Get thetas (in degrees)
@@ -331,26 +350,35 @@
             eff_pixel_size = nxentry.instrument.detector.x_pixel_size
 
         # Get cross sectional diameter
-        cross_sectional_dim = center_stack.shape[2]*eff_pixel_size
+        cross_sectional_dim = tomo_fields_shape[3]*eff_pixel_size
         logger.debug(f'cross_sectional_dim = {cross_sectional_dim}')
 
         # Determine center offset at sample row boundaries
         logger.info('Determine center offset at sample row boundaries')
 
         # Lower row center
-        # center_stack order: row,theta,column
         if self.test_mode:
             lower_row = self.test_config['lower_row']
         elif self.galaxy_flag:
-            lower_row = min(center_rows)
-            if not 0 <= lower_row < center_stack.shape[0]-1:
-                raise ValueError(f'Invalid parameter center_rows ({center_rows})')
+            if center_rows is None:
+                lower_row = 0
+            else:
+                lower_row = min(center_rows)
+                if not 0 <= lower_row < tomo_fields_shape[2]-1:
+                    raise ValueError(f'Invalid parameter center_rows ({center_rows})')
         else:
-            lower_row = select_one_image_bound(center_stack[:,0,:], 0, bound=0,
+            lower_row = select_one_image_bound(
+                    nxentry.reduced_data.data.tomo_fields[center_stack_index,0,:,:], 0, bound=0,
                     title=f'theta={round(thetas[0], 2)+0}',
-                    bound_name='row index to find lower center', default=default)
-        lower_center_offset = self._find_center_one_plane(center_stack[lower_row,:,:], lower_row,
-                thetas, eff_pixel_size, cross_sectional_dim, path=path, num_core=self.num_core)
+                    bound_name='row index to find lower center', default=default, raise_error=True)
+        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)
+        logger.debug(f'... done in {time()-t0:.2f} seconds')
         logger.debug(f'lower_row = {lower_row:.2f}')
         logger.debug(f'lower_center_offset = {lower_center_offset:.2f}')
 
@@ -358,18 +386,27 @@
         if self.test_mode:
             upper_row = self.test_config['upper_row']
         elif self.galaxy_flag:
-            upper_row = max(center_rows)
-            if not lower_row < upper_row < center_stack.shape[0]:
-                raise ValueError(f'Invalid parameter center_rows ({center_rows})')
+            if center_rows is None:
+                upper_row = tomo_fields_shape[2]-1
+            else:
+                upper_row = max(center_rows)
+                if not lower_row < upper_row < tomo_fields_shape[2]:
+                    raise ValueError(f'Invalid parameter center_rows ({center_rows})')
         else:
-            upper_row = select_one_image_bound(center_stack[:,0,:], 0,
-                    bound=center_stack.shape[0]-1, title=f'theta={round(thetas[0], 2)+0}',
-                    bound_name='row index to find upper center', default=default)
-        upper_center_offset = self._find_center_one_plane(center_stack[upper_row,:,:], upper_row,
-                thetas, eff_pixel_size, cross_sectional_dim, path=path, num_core=self.num_core)
+            upper_row = select_one_image_bound(
+                    nxentry.reduced_data.data.tomo_fields[center_stack_index,0,:,:], 0,
+                    bound=tomo_fields_shape[2]-1, title=f'theta={round(thetas[0], 2)+0}',
+                    bound_name='row index to find upper center', default=default, raise_error=True)
+        logger.debug('Finding center...')
+        t0 = time()
+        upper_center_offset = self._find_center_one_plane(
+                #np.asarray(nxentry.reduced_data.data.tomo_fields[center_stack_index,:,upper_row,:]),
+                nxentry.reduced_data.data.tomo_fields[center_stack_index,:,upper_row,:],
+                upper_row, thetas, eff_pixel_size, cross_sectional_dim, path=path,
+                num_core=self.num_core)
+        logger.debug(f'... done in {time()-t0:.2f} seconds')
         logger.debug(f'upper_row = {upper_row:.2f}')
         logger.debug(f'upper_center_offset = {upper_center_offset:.2f}')
-        del center_stack
 
         center_config = {'lower_row': lower_row, 'lower_center_offset': lower_center_offset,
                 'upper_row': upper_row, 'upper_center_offset': upper_center_offset}
@@ -409,11 +446,6 @@
             raise KeyError(f'Unable to find valid reduced data in {nxentry}.')
 
         # Create an NXprocess to store image reconstruction (meta)data
-#        if 'reconstructed_data' in nxentry:
-#            logger.warning(f'Existing reconstructed data in {nxentry} will be overwritten.')
-#            del nxentry['reconstructed_data']
-#        if 'data' in nxentry and 'reconstructed_data' in nxentry.data:
-#            del nxentry.data['reconstructed_data']
         nxprocess = NXprocess()
 
         # Get rotation axis rows and centers
@@ -430,7 +462,7 @@
         thetas = np.asarray(nxentry.reduced_data.rotation_angle)
 
         # Reconstruct tomography data
-        #   reduced data axes order: stack,row,theta,column
+        #   reduced data axes order: stack,theta,row,column
         #   reconstructed data order in each stack: row/z,x,y
         #   Note: Nexus cannot follow a link if the data it points to is too big,
         #         so get the data from the actual place, not from nxentry.data
@@ -442,9 +474,15 @@
         num_tomo_stacks = nxentry.reduced_data.data.tomo_fields.shape[0]
         tomo_recon_stacks = num_tomo_stacks*[np.array([])]
         for i in range(num_tomo_stacks):
+            # Convert reduced data stack from theta,row,column to row,theta,column
+            logger.debug(f'Reading reduced data stack {i+1}...')
+            t0 = time()
             tomo_stack = np.asarray(nxentry.reduced_data.data.tomo_fields[i])
-            if not tomo_stack.size:
-                raise KeyError(f'Unable to load tomography stack {i} for reconstruction')
+            logger.debug(f'... done in {time()-t0:.2f} seconds')
+            if len(tomo_stack.shape) != 3 or any(True for dim in tomo_stack.shape if not dim):
+                raise ValueError(f'Unable to load tomography stack {i+1} for reconstruction')
+            tomo_stack = np.swapaxes(tomo_stack, 0, 1)
+            assert(len(thetas) == tomo_stack.shape[1])
             assert(0 <= lower_row < upper_row < tomo_stack.shape[0])
             center_offsets = [lower_center_offset-lower_row*center_slope,
                     upper_center_offset+(tomo_stack.shape[0]-1-upper_row)*center_slope]
@@ -453,7 +491,7 @@
             tomo_recon_stack = self._reconstruct_one_tomo_stack(tomo_stack, thetas,
                     center_offsets=center_offsets, num_core=self.num_core, algorithm='gridrec')
             logger.debug(f'... done in {time()-t0:.2f} seconds')
-            logger.info(f'Reconstruction of stack {i} took {time()-t0:.2f} seconds')
+            logger.info(f'Reconstruction of stack {i+1} took {time()-t0:.2f} seconds')
 
             # Combine stacks
             tomo_recon_stacks[i] = tomo_recon_stack
@@ -497,7 +535,7 @@
         if num_tomo_stacks == 1:
             basetitle = 'recon'
         else:
-            basetitle = f'recon stack {i}'
+            basetitle = f'recon stack {i+1}'
         for i, stack in enumerate(tomo_recon_stacks):
             title = f'{basetitle} {res_title} xslice{x_slice}'
             quick_imshow(stack[z_range[0]:z_range[1],x_slice,y_range[0]:y_range[1]],
@@ -550,7 +588,7 @@
 
         return(nxroot_copy)
 
-    def combine_data(self, nxroot):
+    def combine_data(self, nxroot, x_bounds=None, y_bounds=None):
         """Combine the reconstructed tomography stacks.
         """
         logger.info('Combine the reconstructed tomography stacks')
@@ -574,41 +612,39 @@
             raise KeyError(f'Unable to find valid reconstructed image data in {nxentry}.')
 
         # Create an NXprocess to store combined image reconstruction (meta)data
-#        if 'combined_data' in nxentry:
-#            logger.warning(f'Existing combined data in {nxentry} will be overwritten.')
-#            del nxentry['combined_data']
-#        if 'data' in nxentry 'combined_data' in nxentry.data:
-#            del nxentry.data['combined_data']
         nxprocess = NXprocess()
 
         # Get the reconstructed data
         #   reconstructed data order: stack,row(z),x,y
         #   Note: Nexus cannot follow a link if the data it points to is too big,
         #         so get the data from the actual place, not from nxentry.data
-        tomo_recon_stacks = np.asarray(nxentry.reconstructed_data.data.reconstructed_data)
-        num_tomo_stacks = tomo_recon_stacks.shape[0]
+        num_tomo_stacks = nxentry.reconstructed_data.data.reconstructed_data.shape[0]
         if num_tomo_stacks == 1:
-            return(nxroot)
+            logger.info('Only one stack available: leaving combine_data')
+            return(None)
+
+        # Combine the reconstructed stacks
+        # (load one stack at a time to reduce risk of hitting Nexus data access limit)
         t0 = time()
         logger.debug(f'Combining the reconstructed stacks ...')
-        tomo_recon_combined = tomo_recon_stacks[0,:,:,:]
+        tomo_recon_combined = np.asarray(nxentry.reconstructed_data.data.reconstructed_data[0])
         if num_tomo_stacks > 2:
             tomo_recon_combined = np.concatenate([tomo_recon_combined]+
-                    [tomo_recon_stacks[i,:,:,:] for i in range(1, num_tomo_stacks-1)])
+                    [nxentry.reconstructed_data.data.reconstructed_data[i]
+                    for i in range(1, num_tomo_stacks-1)])
         if num_tomo_stacks > 1:
             tomo_recon_combined = np.concatenate([tomo_recon_combined]+
-                    [tomo_recon_stacks[num_tomo_stacks-1,:,:,:]])
+                    [nxentry.reconstructed_data.data.reconstructed_data[num_tomo_stacks-1]])
         logger.debug(f'... done in {time()-t0:.2f} seconds')
         logger.info(f'Combining the reconstructed stacks took {time()-t0:.2f} seconds')
 
-        # Resize the combined tomography data set
+        # Resize the combined tomography data stacks
         #   combined data order: row/z,x,y
         if self.test_mode:
             x_bounds = None
             y_bounds = None
             z_bounds = self.test_config.get('z_bounds')
         elif self.galaxy_flag:
-            exit('TODO')
             if x_bounds is not None and not is_int_pair(x_bounds, ge=0,
                     lt=tomo_recon_stacks[0].shape[1]):
                 raise ValueError(f'Invalid parameter x_bounds ({x_bounds})')
@@ -699,7 +735,8 @@
             prefix = str(nxentry.instrument.detector.local_name)
             tdf_stack = dark_field.get_detector_data(prefix)
             if isinstance(tdf_stack, list):
-                exit('TODO')
+                assert(len(tdf_stack) == 1) # TODO
+                tdf_stack = tdf_stack[0]
 
         # Take median
         if tdf_stack.ndim == 2:
@@ -757,7 +794,8 @@
             prefix = str(nxentry.instrument.detector.local_name)
             tbf_stack = bright_field.get_detector_data(prefix)
             if isinstance(tbf_stack, list):
-                exit('TODO')
+                assert(len(tbf_stack) == 1) # TODO
+                tbf_stack = tbf_stack[0]
 
         # Take median if more than one image
         """Median or mean: It may be best to try the median because of some image 
@@ -818,8 +856,8 @@
             first_image = np.asarray(nxentry.instrument.detector.data[field_indices[0],:,:])
             theta = float(nxentry.sample.rotation_angle[field_indices[0]])
             z_translation_all = nxentry.sample.z_translation[field_indices]
-            z_translation_levels = sorted(list(set(z_translation_all)))
-            num_tomo_stacks = len(z_translation_levels)
+            vertical_shifts = sorted(list(set(z_translation_all)))
+            num_tomo_stacks = len(vertical_shifts)
         else:
             tomo_field_scans = nxentry.spec_scans.tomo_fields
             tomo_fields = TomoField.construct_from_nxcollection(tomo_field_scans)
@@ -1112,7 +1150,7 @@
                 if len(tomo_stacks) == 1:
                     title = f'red fullres theta {round(thetas[0], 2)+0}'
                 else:
-                    title = f'red stack {i} fullres theta {round(thetas[0], 2)+0}'
+                    title = f'red stack {i+1} fullres theta {round(thetas[0], 2)+0}'
                 quick_imshow(tomo_stack[0,:,:], title=title, path=path, save_fig=self.save_figs,
                         save_only=self.save_only, block=self.block)
 #                if not self.block:
@@ -1135,15 +1173,13 @@
 #                    if not self.block:
 #                        clear_imshow(title)
 
-            # Convert tomography stack from theta,row,column to row,theta,column
-            t0 = time()
-            tomo_stack = np.swapaxes(tomo_stack, 0, 1)
-            logger.debug(f'Converting coordinate order took {time()-t0:.2f} seconds')
-
             # Save test data to file
             if self.test_mode:
-                row_index = int(tomo_stack.shape[0]/2)
-                np.savetxt(f'{self.output_folder}/red_stack_{i+1}.txt', tomo_stack[row_index,:,:],
+#                row_index = int(tomo_stack.shape[0]/2)
+#                np.savetxt(f'{self.output_folder}/red_stack_{i+1}.txt', tomo_stack[row_index,:,:],
+#                        fmt='%.6e')
+                row_index = int(tomo_stack.shape[1]/2)
+                np.savetxt(f'{self.output_folder}/red_stack_{i+1}.txt', tomo_stack[:,row_index,:],
                         fmt='%.6e')
 
             # Combine resized stacks
@@ -1168,6 +1204,7 @@
         # Try automatic center finding routines for initial value
         # sinogram index order: theta,column
         # need column,theta for iradon, so take transpose
+        sinogram = np.asarray(sinogram)
         sinogram_T = sinogram.T
         center = sinogram.shape[1]/2
 
@@ -1499,7 +1536,7 @@
                     accept = True
             logger.debug(f'y_bounds = {y_bounds}')
 
-        # Selecting z bounds (in xy-plane) (only valid for a single image set)
+        # Selecting z bounds (in xy-plane) (only valid for a single image stack)
         if num_tomo_stacks != 1:
             z_bounds = None
         else:
@@ -1548,9 +1585,11 @@
     logger.info(f'test_mode = {test_mode}')
 
     # Check for correction modes
+    legal_modes = ['reduce_data', 'find_center', 'reconstruct_data', 'combine_data', 'all']
     if modes is None:
         modes = ['all']
-    logger.debug(f'modes {type(modes)} = {modes}')
+    if not all(True if mode in legal_modes else False for mode in modes):
+        raise ValueError(f'Invalid parameter modes ({modes})')
 
     # Instantiate Tomo object
     tomo = Tomo(num_core=num_core, output_folder=output_folder, save_figs=save_figs,
@@ -1581,9 +1620,10 @@
         data = tomo.combine_data(data)
 
     # Write output file
-    if not test_mode:
+    if data is not None and not test_mode:
         if center_data is None:
             data = tomo.write(data, output_file)
         else:
             data = tomo.write(center_data, output_file)
 
+    logger.info(f'Completed modes: {modes}')