changeset 0:2e827836f0ad draft

planemo upload for repository https://github.com/MaterialsGalaxy/larch-tools/tree/main/larch_select_paths commit 5be486890442dedfb327289d597e1c8110240735
author muon-spectroscopy-computational-project
date Tue, 14 Nov 2023 15:35:52 +0000
parents
children 7fdca938d90c
files larch_select_paths.py larch_select_paths.xml test-data/FEFF_paths.zip test-data/[CSV_summary_of_1564889.cif].csv test-data/gds_altered_defaults.csv test-data/gds_default.csv test-data/gds_include_path_3_custom_name.csv test-data/gds_include_path_3_custom_name_value.csv test-data/gds_merge_custom.csv test-data/sp_default.csv test-data/sp_include_path_3.csv test-data/sp_include_path_3_custom_name.csv test-data/sp_merge_custom.csv test-data/sp_merge_default.csv test-data/sp_select_all_false.csv
diffstat 15 files changed, 671 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/larch_select_paths.py	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,295 @@
+import csv
+import json
+import os
+import re
+import sys
+from zipfile import ZIP_DEFLATED, ZipFile
+
+
+class GDSWriter:
+    def __init__(self, default_variables: "dict[str, dict]"):
+        self.default_properties = {
+            "s02": {"name": "s02"},
+            "e0": {"name": "e0"},
+            "deltar": {"name": "alpha*reff"},
+            "sigma2": {"name": "sigma2"},
+        }
+        self.rows = [
+            f"{'id':>4s}, {'name':>24s}, {'value':>5s}, {'expr':>4s}, "
+            f"{'vary':>4s}\n"
+        ]
+        self.names = set()
+
+        for property in self.default_properties:
+            name = self.default_properties[property]["name"]
+            value = default_variables[property]["value"]
+            vary = default_variables[property]["vary"]
+            is_common = default_variables[property]["is_common"]
+
+            self.default_properties[property]["value"] = value
+            self.default_properties[property]["vary"] = vary
+            self.default_properties[property]["is_common"] = is_common
+
+            if is_common:
+                self.append_gds(name=name, value=value, vary=vary)
+
+    def append_gds(
+        self,
+        name: str,
+        value: float = 0.,
+        expr: str = None,
+        vary: bool = True,
+        label: str = "",
+    ):
+        """Append a single GDS variable to the list of rows, later to be
+        written to file.
+
+        Args:
+            name (str): Name of the GDS variable.
+            value (float, optional): Starting value for variable.
+                Defaults to 0.
+            expr (str, optional): Expression for setting the variable.
+                Defaults to None.
+            vary (bool, optional): Whether the variable is optimised during the
+                fit. Defaults to True.
+            label (str, optional): Label to keep variables for different FEFF
+                directories distinct. Defaults to "".
+        """
+        formatted_name = name if (label is None) else label + name
+        formatted_name = formatted_name.replace("*reff", "")
+        if not expr:
+            expr = "    "
+
+        if formatted_name in self.names:
+            raise ValueError(f"{formatted_name} already used as variable name")
+        self.names.add(formatted_name)
+
+        self.rows.append(
+            f"{len(self.rows):4d}, {formatted_name:>24s}, {str(value):>5s}, "
+            f"{expr:>4s}, {str(vary):>4s}\n"
+        )
+
+    def parse_gds(
+        self,
+        property_name: str,
+        variable_name: str = None,
+        path_variable: dict = None,
+        directory_label: str = None,
+        path_label: str = None,
+    ) -> str:
+        """Parse and append a row defining a GDS variable for a particular
+        path.
+
+        Args:
+            property_name (str): The property to which the variable
+                corresponds. Should be a key in `self.default_properties`.
+            variable_name (str, optional): Custom name for this variable.
+                Defaults to None.
+            path_variable (dict, optional): Dictionary defining the GDS
+                settings for this path's variable. Defaults to None.
+            directory_label (str, optional): Label to indicate paths from a
+                separate directory. Defaults to None.
+            path_label (str, optional): Label indicating the atoms involved in
+                this path. Defaults to None.
+
+        Returns:
+            str: Either `variable_name`, the name used as a default globally
+                for this `property_name`, or an automatically generated unique
+                name.
+        """
+        if variable_name:
+            self.append_gds(
+                name=variable_name,
+                value=path_variable["value"],
+                expr=path_variable["expr"],
+                vary=path_variable["vary"],
+            )
+            return variable_name
+        elif self.default_properties[property_name]["is_common"]:
+            return self.default_properties[property_name]["name"]
+        else:
+            auto_name = self.default_properties[property_name]["name"]
+            if directory_label:
+                auto_name += f"_{directory_label}"
+            if path_label:
+                auto_name += f"_{path_label.lower().replace('.', '')}"
+
+            self.append_gds(
+                name=auto_name,
+                value=self.default_properties[property_name]["value"],
+                vary=self.default_properties[property_name]["vary"],
+            )
+            return auto_name
+
+    def write(self):
+        """Write GDS rows to file.
+        """
+        with open("gds.csv", "w") as out:
+            out.writelines(self.rows)
+
+
+class PathsWriter:
+    def __init__(self, default_variables: "dict[str, dict]"):
+        self.rows = [
+            f"{'id':>4s}, {'filename':>24s}, {'label':>24s}, {'s02':>3s}, "
+            f"{'e0':>4s}, {'sigma2':>24s}, {'deltar':>10s}\n"
+        ]
+        self.gds_writer = GDSWriter(default_variables=default_variables)
+
+    def parse_feff_output(
+        self,
+        paths_file: str,
+        selection: "dict[str, str|list]",
+        directory_label: str = "",
+    ):
+        """Parse selected paths from CSV summary and define GDS variables.
+
+        Args:
+            paths_file (str): CSV summary filename.
+            selection (dict[str, str|list]): Dictionary indicating which paths
+                to select, and how to define their variables.
+            directory_label (str, optional): Label to indicate paths from a
+                separate directory. Defaults to "".
+        """
+        paths = selection["paths"]
+        path_values_ids = [path_value["id"] for path_value in paths]
+
+        with open(paths_file) as file:
+            reader = csv.reader(file)
+            for row in reader:
+                id_match = re.search(r"\d+", row[0])
+                if id_match:
+                    path_id = int(id_match.group())
+                    filename = row[0].strip()
+                    path_label = row[-2].strip()
+                    variables = {}
+
+                    if path_id in path_values_ids:
+                        path_value = paths[path_values_ids.index(path_id)]
+                        for property in self.gds_writer.default_properties:
+                            variables[property] = self.gds_writer.parse_gds(
+                                property_name=property,
+                                variable_name=path_value[property]["name"],
+                                path_variable=path_value[property],
+                                directory_label=directory_label,
+                                path_label=path_label,
+                            )
+                        self.parse_selected_path(
+                            filename=filename,
+                            path_label=path_label,
+                            directory_label=directory_label,
+                            **variables,
+                        )
+                    elif selection["selection"] == "all" or int(row[-1]):
+                        path_value = None
+                        for property in self.gds_writer.default_properties:
+                            variables[property] = self.gds_writer.parse_gds(
+                                property_name=property,
+                                directory_label=directory_label,
+                                path_label=path_label,
+                            )
+                        self.parse_selected_path(
+                            filename=filename,
+                            path_label=path_label,
+                            directory_label=directory_label,
+                            **variables,
+                        )
+
+    def parse_selected_path(
+        self,
+        filename: str,
+        path_label: str,
+        directory_label: str = "",
+        s02: str = "s02",
+        e0: str = "e0",
+        sigma2: str = "sigma2",
+        deltar: str = "alpha*reff",
+    ):
+        """Format and append row representing a selected FEFF path.
+
+        Args:
+            filename (str): Name of the underlying FEFF path file, without
+                parent directory.
+            path_label (str): Label indicating the atoms involved in this path.
+            directory_label (str, optional): Label to indicate paths from a
+                separate directory. Defaults to "".
+            s02 (str, optional): Electron screening factor variable name.
+                Defaults to "s02".
+            e0 (str, optional): Energy shift variable name. Defaults to "e0".
+            sigma2 (str, optional): Mean squared displacement variable name.
+                Defaults to "sigma2".
+            deltar (str, optional): Change in path length variable.
+                Defaults to "alpha*reff".
+        """
+        if directory_label:
+            filename = os.path.join(directory_label, filename)
+            label = f"{directory_label}.{path_label}"
+        else:
+            filename = os.path.join("feff", filename)
+            label = path_label
+
+        self.rows.append(
+            f"{len(self.rows):>4d}, {filename:>24s}, {label:>24s}, "
+            f"{s02:>3s}, {e0:>4s}, {sigma2:>24s}, {deltar:>10s}\n"
+        )
+
+    def write(self):
+        """Write selected path and GDS rows to file.
+        """
+        self.gds_writer.write()
+        with open("sp.csv", "w") as out:
+            out.writelines(self.rows)
+
+
+def main(input_values: dict):
+    """Select paths and define GDS parameters.
+
+    Args:
+        input_values (dict): All input values from the Galaxy tool UI.
+
+    Raises:
+        ValueError: If a FEFF label is not unique.
+    """
+    default_variables = input_values["variables"]
+
+    writer = PathsWriter(default_variables=default_variables)
+
+    if len(input_values["feff_outputs"]) == 1:
+        feff_output = input_values["feff_outputs"][0]
+        writer.parse_feff_output(
+            paths_file=feff_output["paths_file"],
+            selection=feff_output["selection"],
+        )
+    else:
+        zfill_length = len(str(len(input_values["feff_outputs"])))
+        labels = set()
+        with ZipFile("merged.zip", "x", ZIP_DEFLATED) as zipfile_out:
+            for i, feff_output in enumerate(input_values["feff_outputs"]):
+                label = feff_output.pop("label") or str(i + 1).zfill(
+                    zfill_length
+                )
+                if label in labels:
+                    raise ValueError(f"Label '{label}' is not unique")
+                labels.add(label)
+
+                writer.parse_feff_output(
+                    directory_label=label,
+                    paths_file=feff_output["paths_file"],
+                    selection=feff_output["selection"],
+                )
+
+                with ZipFile(feff_output["paths_zip"]) as z:
+                    for zipinfo in z.infolist():
+                        if zipinfo.filename != "feff/":
+                            zipinfo.filename = zipinfo.filename[5:]
+                            z.extract(member=zipinfo, path=label)
+                            zipfile_out.write(
+                                os.path.join(label, zipinfo.filename)
+                            )
+
+    writer.write()
+
+
+if __name__ == "__main__":
+    input_values = json.load(open(sys.argv[1], "r", encoding="utf-8"))
+    main(input_values)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/larch_select_paths.xml	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,294 @@
+<tool id="larch_select_paths" name="Larch Select Paths" version="@TOOL_VERSION@+galaxy@WRAPPER_VERSION@" python_template_version="3.5" profile="22.05" license="MIT">
+    <description>select FEFF paths for XAFS data</description>
+    <macros>
+        <!-- version of underlying tool (PEP 440) -->
+        <token name="@TOOL_VERSION@">0.9.71</token>
+        <!-- version of this tool wrapper (integer) -->
+        <token name="@WRAPPER_VERSION@">0</token>
+        <!-- citation should be updated with every underlying tool version -->
+        <!-- typical fields to update are version, month, year, and doi -->
+        <token name="@TOOL_CITATION@">10.1088/1742-6596/430/1/012007</token>
+        <xml name="name">
+            <param name="name" type="text" optional="true" label="Name" help="The name of the variable should be unique, and can be used in expressions for other paths. If name is set, will overwrite the default bevaviour for this variable."/>
+        </xml>
+        <xml name="expr">
+            <param name="expr" type="text" optional="true" label="Expression" help="If set, the variable will be 'Defined' by the expression. This can include other variable name, for example in order to set two paths to use the same variable."/>
+        </xml>
+        <xml name="vary">
+            <param name="vary" type="boolean" checked="true" label="Vary" help="If True, the initial 'Guess' will be optimised in the fitting. If False, the value will be 'Set' instead and not optimised."/>
+        </xml>
+    </macros>
+    <creator>
+        <person givenName="Patrick" familyName="Austin" url="https://github.com/patrick-austin" identifier="https://orcid.org/0000-0002-6279-7823"/>
+    </creator>
+    <requirements>
+        <requirement type="package" version="@TOOL_VERSION@">xraylarch</requirement>
+        <requirement type="package" version="3.5.2">matplotlib</requirement>
+    </requirements>
+    <required_files>
+        <include type="literal" path="larch_select_paths.py"/>
+    </required_files>
+    <command detect_errors="exit_code"><![CDATA[
+        python '${__tool_directory__}/larch_select_paths.py' '$inputs'
+    ]]></command>
+    <configfiles>
+        <inputs name="inputs" data_style="paths"/>
+    </configfiles>
+    <inputs>
+        <section name="variables" expanded="false" title="GDS variable defaults" help="Define default values for variables in the EXAFS equation to use for the paths selected below.">
+            <section name="s02" expanded="false" title="S02: passive electron reduction factor">
+                <param name="is_common" type="boolean" checked="true" label="Common to all paths" help="If set, a single variable 's02' will be used for all paths. Otherwise, each path has a distinct variable."/>
+                <param name="value" type="float" value="1.0" min="0.0" max="1.0" label="Value" help="The initial value for 's02'. This is typically between 0.7 and 1.0."/>
+                <expand macro="vary"/>
+            </section>
+            <section name="e0" expanded="false" title="E0: energy shift">
+                <param name="is_common" type="boolean" checked="true" label="Common to all paths" help="If set, a single variable 'e0' will be used for all paths. Otherwise, each path has a distinct variable."/>
+                <param name="value" type="float" value="0.0" label="Value" help="The initial value for 'e0'. This should be close to zero, as it represents the difference in the absorption edge positions between simulation and experiment."/>
+                <expand macro="vary"/>
+            </section>
+            <section name="deltar" expanded="false" title="Delta R: change in path length">
+                <param name="is_common" type="boolean" checked="true" label="Common to all paths" help="If set, a single variable 'alpha*reff' will be used for all paths (where 'reff' is the effective path length). Otherwise, each path has a distinct variable."/>
+                <param name="value" type="float" value="0.0" label="Value" help="The initial value for 'alpha'. This should be close to zero, as it represents the difference in the FEFF path and fitted (experimental) path length."/>
+                <expand macro="vary"/>
+            </section>
+            <section name="sigma2" expanded="false" title="Sigma2: mean squared displacement">
+                <param name="is_common" type="boolean" checked="true" label="Common to all paths" help="If set, a single variable 'sigma2' will be used for all paths. Otherwise, each path has a distinct variable."/>
+                <param name="value" type="float" value="0.003" label="Value" help="The initial value for 'sigma2'. This should be small, but non-zero, as atomic vibrations will result in variation in exact positions."/>
+                <expand macro="vary"/>
+            </section>
+        </section>
+        <repeat name="feff_outputs" title="Distinct FEFF outputs" min="1" help="If FEFF has been used on multiple structures, the paths from each can be merged into one input to provide to Larch Artemis. If only one entry is provided, then the zipped directory of paths will be unchanged.">
+            <param name="label" type="text" optional="true" label="Label" help="Short label to use when merging different FEFF outputs to ensure they remain distinct. Not required if only providing a single output, and will default to the index in this list."/>
+            <param name="paths_zip" type="data" format="zip" label="Zipped paths directory" help="Zipped directory containing the actual path files output by FEFF."/>
+            <param name="paths_file" type="data" format="feff" label="Paths file" help="CSV file detailing the possible scattering paths. Note that rows with '1' in the 'select' column will be selected with default values for their parameters automatically."/>
+            <conditional name="selection">
+                <param name="selection" type="select" label="Selection method">
+                    <option value="all" selected="true">All paths</option>
+                    <!-- <option value="number">Fixed number</option>
+                    <option value="combinations">Combinations</option> -->
+                    <option value="manual">Manual</option>
+                </param>
+                <when value="all">
+                    <repeat name="paths" title="Define path variables" help="Overwrite the default variables defined above for chosen paths.">
+                        <param name="id" type="integer" value="1" min="1" label="Path ID" help="Numerical id of a path to select, this appears at the end of the label and filename in the path summary CSV."/>
+                        <section name="s02" expanded="false" title="S02: passive electron reduction factor">
+                            <expand macro="name"/>
+                            <param name="value" type="float" value="1.0" min="0.0" max="1.0" label="Value" help="The initial value for 's02'. This is typically between 0.7 and 1.0."/>
+                            <expand macro="expr"/>
+                            <expand macro="vary"/>
+                        </section>
+                        <section name="e0" expanded="false" title="E0: energy shift">
+                            <expand macro="name"/>
+                            <param name="value" type="float" value="0.0" label="Value" help="The initial value for 'e0'. This should be close to zero, as it represents the difference in the absorption edge positions between simulation and experiment."/>
+                            <expand macro="expr"/>
+                            <expand macro="vary"/>
+                        </section>
+                        <section name="deltar" expanded="false" title="Delta R: change in path length">
+                            <expand macro="name"/>
+                            <param name="value" type="float" value="0.0" label="Value" help="The initial value for 'alpha'. This should be close to zero, as it represents the difference in the FEFF path and fitted (experimental) path length."/>
+                            <expand macro="expr"/>
+                            <expand macro="vary"/>
+                        </section>
+                        <section name="sigma2" expanded="false" title="Sigma2: mean squared displacement">
+                            <expand macro="name"/>
+                            <param name="value" type="float" value="0.003" label="Value" help="The initial value for 'sigma2'. This should be small, but non-zero, as atomic vibrations will result in variation in exact positions."/>
+                            <expand macro="expr"/>
+                            <expand macro="vary"/>
+                        </section>
+                    </repeat>
+                </when>
+                <!-- <when value="number">
+                </when>
+                <when value="combinations">
+                </when> -->
+                <when value="manual">
+                    <repeat name="paths" title="Select paths" help="Identify paths to use in the fitting by their id, and optionally define their variables. This will overwrite and defaults set above.">
+                        <param name="id" type="integer" value="1" min="1" label="Path ID" help="Numerical id of a path to select, this appears at the end of the label and filename in the path summary CSV."/>
+                        <section name="s02" expanded="false" title="S02: passive electron reduction factor">
+                            <expand macro="name"/>
+                            <param name="value" type="float" value="1.0" min="0.0" max="1.0" label="Value" help="The initial value for 's02'. This is typically between 0.7 and 1.0."/>
+                            <expand macro="expr"/>
+                            <expand macro="vary"/>
+                        </section>
+                        <section name="e0" expanded="false" title="E0: energy shift">
+                            <expand macro="name"/>
+                            <param name="value" type="float" value="0.0" label="Value" help="The initial value for 'e0'. This should be close to zero, as it represents the difference in the absorption edge positions between simulation and experiment."/>
+                            <expand macro="expr"/>
+                            <expand macro="vary"/>
+                        </section>
+                        <section name="deltar" expanded="false" title="Delta R: change in path length">
+                            <expand macro="name"/>
+                            <param name="value" type="float" value="0.0" label="Value" help="The initial value for 'alpha'. This should be close to zero, as it represents the difference in the FEFF path and fitted (experimental) path length."/>
+                            <expand macro="expr"/>
+                            <expand macro="vary"/>
+                        </section>
+                        <section name="sigma2" expanded="false" title="Sigma2: mean squared displacement">
+                            <expand macro="name"/>
+                            <param name="value" type="float" value="0.003" label="Value" help="The initial value for 'sigma2'. This should be small, but non-zero, as atomic vibrations will result in variation in exact positions."/>
+                            <expand macro="expr"/>
+                            <expand macro="vary"/>
+                        </section>
+                    </repeat>
+                </when>
+            </conditional>
+        </repeat>
+    </inputs>
+    <outputs>
+        <data name="merged_directories" format="zip" from_work_dir="merged.zip" label="Merged directories from ${on_string}">
+            <filter>len(feff_outputs) > 1</filter>
+        </data>
+        <data name="gds_csv" format="gds" from_work_dir="gds.csv" label="GDS values for ${on_string}"/>
+        <data name="sp_csv" format="sp" from_work_dir="sp.csv" label="Selected paths for ${on_string}"/>
+    </outputs>
+    <tests>
+        <!-- Test defaults for CSV with select_all -->
+        <test expect_num_outputs="2">
+            <param name="paths_zip" value="FEFF_paths.zip"/>
+            <param name="paths_file" value="[CSV_summary_of_1564889.cif].csv"/>
+            <output name="gds_csv" file="gds_default.csv"/>
+            <output name="sp_csv" file="sp_default.csv"/>
+        </test>
+        <!-- Test defaults for CSV with some selected rows -->
+        <test expect_num_outputs="2">
+            <param name="paths_zip" value="FEFF_paths.zip"/>
+            <param name="paths_file" value="[CSV_summary_of_1564889.cif].csv"/>
+            <param name="selection" value="manual"/>
+            <output name="gds_csv" file="gds_default.csv"/>
+            <output name="sp_csv" file="sp_select_all_false.csv"/>
+        </test>
+        <!-- Test selected paths without custom GDS -->
+        <test expect_num_outputs="2">
+            <param name="paths_zip" value="FEFF_paths.zip"/>
+            <param name="paths_file" value="[CSV_summary_of_1564889.cif].csv"/>
+            <param name="selection" value="manual"/>
+            <param name="id" value="3"/>
+            <output name="gds_csv" file="gds_default.csv"/>
+            <output name="sp_csv" file="sp_include_path_3.csv"/>
+        </test>
+        <!-- Test selected paths with custom name but no GDS entry -->
+        <test expect_num_outputs="2">
+            <param name="paths_zip" value="FEFF_paths.zip"/>
+            <param name="paths_file" value="[CSV_summary_of_1564889.cif].csv"/>
+            <param name="selection" value="manual"/>
+            <param name="id" value="3"/>
+            <section name="sigma2">
+                <param name="name" value="custom_name"/>
+            </section>
+            <output name="gds_csv" file="gds_include_path_3_custom_name.csv"/>
+            <output name="sp_csv" file="sp_include_path_3_custom_name.csv"/>
+        </test>
+        <!-- Test selected paths with custom GDS -->
+        <test expect_num_outputs="2">
+            <param name="paths_zip" value="FEFF_paths.zip"/>
+            <param name="paths_file" value="[CSV_summary_of_1564889.cif].csv"/>
+            <param name="selection" value="manual"/>
+            <param name="id" value="3"/>
+            <repeat name="paths">
+                <section name="sigma2">
+                    <param name="name" value="custom_name"/>
+                    <param name="value" value="0.005"/>
+                    <param name="expr" value=""/>
+                    <param name="vary" value="false"/>
+                </section>
+            </repeat>
+            <output name="gds_csv" file="gds_include_path_3_custom_name_value.csv"/>
+            <output name="sp_csv" file="sp_include_path_3_custom_name.csv"/>
+        </test>
+        <!-- Test changing default GDS values -->
+        <test expect_num_outputs="2">
+            <section name="variables">
+                <section name="s02">
+                    <param name="value" value="0.1"/>
+                    <param name="vary" value="false"/>
+                </section>
+                <section name="e0">
+                    <param name="value" value="0.1"/>
+                    <param name="vary" value="true"/>
+                </section>
+                <section name="deltar">
+                    <param name="value" value="10"/>
+                    <param name="vary" value="false"/>
+                </section>
+            </section>
+            <param name="paths_zip" value="FEFF_paths.zip"/>
+            <param name="paths_file" value="[CSV_summary_of_1564889.cif].csv"/>
+            <param name="selection" value="manual"/>
+            <output name="gds_csv" file="gds_altered_defaults.csv"/>
+            <output name="sp_csv" file="sp_select_all_false.csv"/>
+        </test>
+        <!-- Test merging defaults -->
+        <test expect_num_outputs="3">
+            <repeat name="feff_outputs">
+                <param name="paths_zip" value="FEFF_paths.zip"/>
+                <param name="paths_file" value="[CSV_summary_of_1564889.cif].csv"/>
+            </repeat>
+            <repeat name="feff_outputs">
+                <param name="paths_zip" value="FEFF_paths.zip"/>
+                <param name="paths_file" value="[CSV_summary_of_1564889.cif].csv"/>
+            </repeat>
+            <output name="merged_directories">
+                <assert_contents>
+                    <has_size value="206000" delta="100"/>
+                </assert_contents>
+            </output>
+            <output name="gds_csv" file="gds_default.csv"/>
+            <output name="sp_csv" file="sp_merge_default.csv"/>
+        </test>
+        <!-- Test merging custom arguments -->
+        <test expect_num_outputs="3">
+            <repeat name="feff_outputs">
+                <param name="label" value="primary"/>
+                <param name="paths_zip" value="FEFF_paths.zip"/>
+                <param name="paths_file" value="[CSV_summary_of_1564889.cif].csv"/>
+                <conditional name="selection">
+                    <param name="selection" value="manual"/>
+                    <repeat name="paths">
+                        <param name="id" value="3"/>
+                        <section name="sigma2">
+                            <param name="name" value="custom_name_1"/>
+                        </section>
+                    </repeat>
+                </conditional>
+            </repeat>
+            <repeat name="feff_outputs">
+                <param name="label" value="secondary"/>
+                <param name="paths_zip" value="FEFF_paths.zip"/>
+                <param name="paths_file" value="[CSV_summary_of_1564889.cif].csv"/>
+                <conditional name="selection">
+                    <param name="selection" value="manual"/>
+                    <repeat name="paths">
+                        <param name="id" value="3"/>
+                        <section name="sigma2">
+                            <param name="name" value="custom_name_2"/>
+                        </section>
+                    </repeat>
+                </conditional>
+            </repeat>
+            <output name="merged_directories">
+                <assert_contents>
+                    <has_size value="206500" delta="100"/>
+                </assert_contents>
+            </output>
+            <output name="gds_csv" file="gds_merge_custom.csv"/>
+            <output name="sp_csv" file="sp_merge_custom.csv"/>
+        </test>
+    </tests>
+    <help><![CDATA[
+        Select FEFF scattering paths to use in the fitting process.
+
+        If paths from multiple different FEFF outputs are of interest (for example, corresponding to different structural files), then additional FEFF outputs can be added.
+        Each requires its own zip directory and path summary CSV, and any custom GDS parameters will be uniquely labelled with the label provided or a numerical default.
+        In this case the zipped directories will also be merged into one output containing all paths and associated files.
+
+        If only one set of FEFF outputs is provided, labelling is not required and the existing zip file can be used as the input to Larch Artemis.
+
+        If the selection method "All paths" is chosen, or an individual row in the CSV with ``select`` is set to ``1``, it will be automatically used, with the default values defined.
+        This can be useful when many paths are needed and using the UI can be cumbersome.
+
+        It is also possible to manually check and further modify the GDS and SP output CSVs to ensure the values are suitable, as an alternative to re-running this tool.
+    ]]></help>
+    <citations>
+        <citation type="doi">@TOOL_CITATION@</citation>
+        <citation type="doi">10.1107/S0909049505012719</citation>
+    </citations>
+</tool>
\ No newline at end of file
Binary file test-data/FEFF_paths.zip has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/[CSV_summary_of_1564889.cif].csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,9 @@
+    file     ,   sig2 ,  amp ratio,    deg  ,  nlegs,  r effective, label                         , select
+ feff0001.dat, 0.00000,   100.000 ,    2.000,     2 ,  2.2366     , S.Fe.1                        ,      1
+ feff0002.dat, 0.00000,   100.000 ,    4.000,     2 ,  2.2549     , S.Fe.2                        ,      1
+ feff0003.dat, 0.00000,    17.561 ,    2.000,     2 ,  3.3852     , Fe.Fe.3                       ,      0
+ feff0004.dat, 0.00000,    16.957 ,    2.000,     2 ,  3.4915     , S.Fe.4                        ,      0
+ feff0005.dat, 0.00000,    31.275 ,    4.000,     2 ,  3.6045     , S.Fe.5                        ,      0
+ feff0006.dat, 0.00000,     7.146 ,    8.000,     3 ,  3.8064     , S.S.Fe.6                      ,      0
+ feff0007.dat, 0.00000,     7.214 ,    8.000,     3 ,  3.8606     , S.S.Fe.7                      ,      0
+ feff0008.dat, 0.00000,    50.180 ,    8.000,     2 ,  3.8958     , Fe.Fe.8                       ,      0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/gds_altered_defaults.csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,5 @@
+  id,                     name, value, expr, vary
+   1,                      s02,   0.1,     , False
+   2,                       e0,   0.1,     , True
+   3,                    alpha,  10.0,     , False
+   4,                   sigma2, 0.003,     , True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/gds_default.csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,5 @@
+  id,                     name, value, expr, vary
+   1,                      s02,   1.0,     , True
+   2,                       e0,   0.0,     , True
+   3,                    alpha,   0.0,     , True
+   4,                   sigma2, 0.003,     , True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/gds_include_path_3_custom_name.csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,6 @@
+  id,                     name, value, expr, vary
+   1,                      s02,   1.0,     , True
+   2,                       e0,   0.0,     , True
+   3,                    alpha,   0.0,     , True
+   4,                   sigma2, 0.003,     , True
+   5,              custom_name, 0.003,     , True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/gds_include_path_3_custom_name_value.csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,6 @@
+  id,                     name, value, expr, vary
+   1,                      s02,   1.0,     , True
+   2,                       e0,   0.0,     , True
+   3,                    alpha,   0.0,     , True
+   4,                   sigma2, 0.003,     , True
+   5,              custom_name, 0.005,     , False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/gds_merge_custom.csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,7 @@
+  id,                     name, value, expr, vary
+   1,                      s02,   1.0,     , True
+   2,                       e0,   0.0,     , True
+   3,                    alpha,   0.0,     , True
+   4,                   sigma2, 0.003,     , True
+   5,            custom_name_1, 0.003,     , True
+   6,            custom_name_2, 0.003,     , True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/sp_default.csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,9 @@
+  id,                 filename,                    label, s02,   e0,                   sigma2,     deltar
+   1,        feff/feff0001.dat,                   S.Fe.1, s02,   e0,                   sigma2, alpha*reff
+   2,        feff/feff0002.dat,                   S.Fe.2, s02,   e0,                   sigma2, alpha*reff
+   3,        feff/feff0003.dat,                  Fe.Fe.3, s02,   e0,                   sigma2, alpha*reff
+   4,        feff/feff0004.dat,                   S.Fe.4, s02,   e0,                   sigma2, alpha*reff
+   5,        feff/feff0005.dat,                   S.Fe.5, s02,   e0,                   sigma2, alpha*reff
+   6,        feff/feff0006.dat,                 S.S.Fe.6, s02,   e0,                   sigma2, alpha*reff
+   7,        feff/feff0007.dat,                 S.S.Fe.7, s02,   e0,                   sigma2, alpha*reff
+   8,        feff/feff0008.dat,                  Fe.Fe.8, s02,   e0,                   sigma2, alpha*reff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/sp_include_path_3.csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,4 @@
+  id,                 filename,                    label, s02,   e0,                   sigma2,     deltar
+   1,        feff/feff0001.dat,                   S.Fe.1, s02,   e0,                   sigma2, alpha*reff
+   2,        feff/feff0002.dat,                   S.Fe.2, s02,   e0,                   sigma2, alpha*reff
+   3,        feff/feff0003.dat,                  Fe.Fe.3, s02,   e0,                   sigma2, alpha*reff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/sp_include_path_3_custom_name.csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,4 @@
+  id,                 filename,                    label, s02,   e0,                   sigma2,     deltar
+   1,        feff/feff0001.dat,                   S.Fe.1, s02,   e0,                   sigma2, alpha*reff
+   2,        feff/feff0002.dat,                   S.Fe.2, s02,   e0,                   sigma2, alpha*reff
+   3,        feff/feff0003.dat,                  Fe.Fe.3, s02,   e0,              custom_name, alpha*reff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/sp_merge_custom.csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,7 @@
+  id,                 filename,                    label, s02,   e0,                   sigma2,     deltar
+   1,     primary/feff0001.dat,           primary.S.Fe.1, s02,   e0,                   sigma2, alpha*reff
+   2,     primary/feff0002.dat,           primary.S.Fe.2, s02,   e0,                   sigma2, alpha*reff
+   3,     primary/feff0003.dat,          primary.Fe.Fe.3, s02,   e0,            custom_name_1, alpha*reff
+   4,   secondary/feff0001.dat,         secondary.S.Fe.1, s02,   e0,                   sigma2, alpha*reff
+   5,   secondary/feff0002.dat,         secondary.S.Fe.2, s02,   e0,                   sigma2, alpha*reff
+   6,   secondary/feff0003.dat,        secondary.Fe.Fe.3, s02,   e0,            custom_name_2, alpha*reff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/sp_merge_default.csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,17 @@
+  id,                 filename,                    label, s02,   e0,                   sigma2,     deltar
+   1,           1/feff0001.dat,                 1.S.Fe.1, s02,   e0,                   sigma2, alpha*reff
+   2,           1/feff0002.dat,                 1.S.Fe.2, s02,   e0,                   sigma2, alpha*reff
+   3,           1/feff0003.dat,                1.Fe.Fe.3, s02,   e0,                   sigma2, alpha*reff
+   4,           1/feff0004.dat,                 1.S.Fe.4, s02,   e0,                   sigma2, alpha*reff
+   5,           1/feff0005.dat,                 1.S.Fe.5, s02,   e0,                   sigma2, alpha*reff
+   6,           1/feff0006.dat,               1.S.S.Fe.6, s02,   e0,                   sigma2, alpha*reff
+   7,           1/feff0007.dat,               1.S.S.Fe.7, s02,   e0,                   sigma2, alpha*reff
+   8,           1/feff0008.dat,                1.Fe.Fe.8, s02,   e0,                   sigma2, alpha*reff
+   9,           2/feff0001.dat,                 2.S.Fe.1, s02,   e0,                   sigma2, alpha*reff
+  10,           2/feff0002.dat,                 2.S.Fe.2, s02,   e0,                   sigma2, alpha*reff
+  11,           2/feff0003.dat,                2.Fe.Fe.3, s02,   e0,                   sigma2, alpha*reff
+  12,           2/feff0004.dat,                 2.S.Fe.4, s02,   e0,                   sigma2, alpha*reff
+  13,           2/feff0005.dat,                 2.S.Fe.5, s02,   e0,                   sigma2, alpha*reff
+  14,           2/feff0006.dat,               2.S.S.Fe.6, s02,   e0,                   sigma2, alpha*reff
+  15,           2/feff0007.dat,               2.S.S.Fe.7, s02,   e0,                   sigma2, alpha*reff
+  16,           2/feff0008.dat,                2.Fe.Fe.8, s02,   e0,                   sigma2, alpha*reff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/sp_select_all_false.csv	Tue Nov 14 15:35:52 2023 +0000
@@ -0,0 +1,3 @@
+  id,                 filename,                    label, s02,   e0,                   sigma2,     deltar
+   1,        feff/feff0001.dat,                   S.Fe.1, s02,   e0,                   sigma2, alpha*reff
+   2,        feff/feff0002.dat,                   S.Fe.2, s02,   e0,                   sigma2, alpha*reff