com.selventa.whistle.cli.Rcr.java Source code

Java tutorial

Introduction

Here is the source code for com.selventa.whistle.cli.Rcr.java

Source

package com.selventa.whistle.cli;

import static java.lang.String.format;
import static java.lang.System.err;
import static java.lang.System.getenv;
import static org.openbel.framework.common.BELUtilities.asPath;
import static org.openbel.framework.common.BELUtilities.hasLength;
import static org.openbel.framework.common.BELUtilities.noLength;
import static org.openbel.framework.common.BELUtilities.readable;
import static org.openbel.framework.common.cfg.SystemConfiguration.createSystemConfiguration;
import static org.openbel.framework.common.enums.RelationshipType.DECREASES;
import static org.openbel.framework.common.enums.RelationshipType.INCREASES;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.openbel.framework.api.DefaultSpeciesDialect;
import org.openbel.framework.api.Dialect;
import org.openbel.framework.api.Kam;
import org.openbel.framework.api.Kam.KamNode;
import org.openbel.framework.api.KamDialect;
import org.openbel.framework.api.KamSpecies;
import org.openbel.framework.api.KAMStore;
import org.openbel.framework.api.KAMStoreImpl;
import org.openbel.framework.common.bel.parser.BELParser;
import org.openbel.framework.common.cfg.SystemConfiguration;
import org.openbel.framework.common.model.Namespace;
import org.openbel.framework.common.model.Term;
import org.openbel.framework.core.df.DBConnection;
import org.openbel.framework.core.df.DatabaseService;
import org.openbel.framework.core.df.DatabaseServiceImpl;
import org.openbel.framework.core.df.beldata.namespace.NamespaceHeader;
import org.openbel.framework.core.df.beldata.namespace.NamespaceHeaderParser;
import org.openbel.framework.core.df.cache.CacheableResourceService;
import org.openbel.framework.core.df.cache.DefaultCacheableResourceService;
import org.openbel.framework.core.df.cache.ResolvedResource;
import org.openbel.framework.core.df.cache.ResourceType;
import org.openbel.framework.api.internal.KAMStoreDaoImpl.BelTerm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import au.com.bytecode.opencsv.CSVWriter;

import com.selventa.whistle.cli.license.LicenseAgreement;
import com.selventa.whistle.cli.license.LicenseCallback;
import com.selventa.whistle.cli.license.LicensePromptOptions;
import com.selventa.whistle.data.enums.DirectionType;
import com.selventa.whistle.data.model.Comparison;
import com.selventa.whistle.data.model.Measurement;
import com.selventa.whistle.data.service.DataFileService;
import com.selventa.whistle.data.service.DefaultCollapsingStrategy;
import com.selventa.whistle.data.service.DefaultIdAMPDataFileService;
import com.selventa.whistle.score.model.Cutoffs;
import com.selventa.whistle.score.model.Downstream;
import com.selventa.whistle.score.model.Hypothesis;
import com.selventa.whistle.score.model.MappedMeasurement;
import com.selventa.whistle.score.model.ScoredHypothesis;
import com.selventa.whistle.score.service.BasicHypothesisFinder;
import com.selventa.whistle.score.service.DefaultMeasurementMappingService;
import com.selventa.whistle.score.service.MeasurementMappingService;
import com.selventa.whistle.score.service.MeasurementMappingService.MappingResult;
import com.selventa.whistle.score.service.Scorer;
import com.selventa.whistle.score.service.Scorer.Prediction;

/**
 * Command line application to perform Reverse Causal Reasoning of a data set
 * against a given Kam.
 *
 * @author Steve Ungerer
 * @author Anthony Bargnesi
 */
public class Rcr {
    private static final Logger logger = LoggerFactory.getLogger(Rcr.class);

    // cli constants
    private static final String KAM_SHORT_OPT = "k";
    private static final String KAM_LONG_OPT = "kam";
    private static final String DATA_SHORT_OPT = "f";
    private static final String DATA_LONG_OPT = "input-file";
    private static final String RUN_NAME_SHORT_OPT = "r";
    private static final String RUN_NAME_LONG_OPT = "run-name";
    private static final String FOLD_CHANGE_SHORT_OPT = "m";
    private static final String FOLD_CHANGE_LONG_OPT = "fold-change-cutoff";
    private static final String PVAL_SHORT_OPT = "p";
    private static final String PVAL_LONG_OPT = "p-value-cutoff";
    private static final String ABUN_SHORT_OPT = "a";
    private static final String ABUN_LONG_OPT = "abundance-cutoff";
    private static final String ANLST_SHORT_OPT = "s";
    private static final String ANLST_LONG_OPT = "use-analyst-selection";
    private static final String POP_SIZE_SHORT_OPT = "n";
    private static final String POP_SIZE_LONG_OPT = "population-size";
    private static final String NS_URL_SHORT_OPT = "u";
    private static final String NS_URL_LONG_OPT = "namespace-url";
    private static final String DETAIL_LONG_OPT = "detail";
    private static final String SPECIES_TAXID_SHORT_OPT = "t";
    private static final String SPECIES_TAXID_LONG_OPT = "taxid";
    private static final String CSV = ".csv";
    private static final String RESULT_FILE_SUFFIX = "_result" + CSV;
    private static final String MAPPING_FILE_SUFFIX = "_mapping" + CSV;
    private static final String DETAIL_FILE_SUFFIX = "_detail" + CSV;

    // reporting constants
    private static final String NOT_SIGNIFICANT = "Not significant";
    private static final String AMBIGUOUS = "Ambiguous";
    private static final String CONTRA = "Contra";
    private static final String CORRECT = "Correct";
    private static final String COLLAPSED_STATUS = "Collapsed to: %s";
    private static final String NOT_PRESENT_IN_POPULATION_STATUS = "Not present in population: %s";
    private static final String NOT_MAPPED_TO_KAM_STATUS = "Not mapped to KAM";

    // header constants
    private static final String ID_HEADER = "Id";
    private static final String DIRECTION_HEADER = "Direction";
    private static final String CORRECT_HEADER = "Correct";
    private static final String RICHNESS_HEADER = "Richness";
    private static final String CONCORDANCE_HEADER = "Concordance";
    private static final String AMBIGUOUS_HEADER = "Ambiguous";
    private static final String CONTRA_HEADER = "Contra";
    private static final String POSSIBLE_HEADER = "Possible";
    private static final String OBSERVED_HEADER = "Observed";
    private static final String STATUS_HEADER = "STATUS";
    private static final String KAM_NODE_HEADER = "KAM_NODE";
    private static final String SOURCE_HEADER = "Source";
    private static final String RELATIONSHIP_HEADER = "Relationship";
    private static final String TARGET_HEADER = "Target";
    private static final String TYPE_HEADER = "Type";

    // license constants
    private static final String APPLICATION = "Whistle";
    private static final String LICENSE_PATH = "LICENSE";
    private static final String LICENSE_PROMPT = "Do you accept the license agreement above?";
    private static final String LICENSE_ACCEPT = "Yes";
    private static final String LICENSE_REJECT = "No";
    private static final String LICENSE_REJECTED_EXIT = "License Agreement not accepted.  Whistle will now exit.";

    // environment constants
    private static final String ENV_BELFRAMEWORK_HOME = "BELFRAMEWORK_HOME";
    private static final String WHISTLE_HOME = "WHISTLE_HOME";
    private static final String CONFIG_DIRECTORY = "config";
    private static final String BELFRAMEWORK_CONFIG = "belframework.cfg";

    // service objects, for custom subclassing override the applicable getters.
    private SystemConfiguration sysCfg;
    private DatabaseService dbService;
    private KAMStore kamStore;
    private CacheableResourceService cacheService;
    private DataFileService dataFileService;
    private MeasurementMappingService mappingService;
    private Dialect dialect;

    // run state
    protected final CommandLine commandLine;

    /**
     * Entry point from CLI execution. Simply constructs a new instance and runs it.
     *
     * @param args
     * @throws Exception
     */
    public static void main(final String[] args) throws Exception {
        LicensePromptOptions options = new LicensePromptOptions(LICENSE_PROMPT, LICENSE_ACCEPT, LICENSE_REJECT);
        LicenseCallback callback = new LicenseCallback() {

            @Override
            public void onAcceptance() {
                try {
                    Rcr rcr = new Rcr(args);
                    rcr.run();
                } catch (Exception e) {
                    err.println(format("Error running %s: %s", APPLICATION, e.getMessage()));
                    e.printStackTrace();
                }
                System.exit(0);
            }

            @Override
            public void onRejection() {
                err.println(LICENSE_REJECTED_EXIT);
                System.exit(1);
            }
        };

        LicenseAgreement license;

        final InputStream is = Rcr.class.getResourceAsStream("/" + LICENSE_PATH);
        if (is != null) {
            license = new LicenseAgreement(APPLICATION, is, options, callback);
        } else {
            File licenseFile = new File(LICENSE_PATH);
            if (!readable(licenseFile)) {
                err.println(format("Cannot find %s file.", LICENSE_PATH));
            }

            license = new LicenseAgreement(APPLICATION, licenseFile, options, callback);
        }

        license.promptLicense();
    }

    /**
     * Constructs the Rcr instance and validates the provided arguments
     * @param args
     * @throws Exception
     */
    public Rcr(String[] args) throws Exception {
        // Are they asking for help?
        if (args.length == 0 || ArrayUtils.contains(args, "-h") || ArrayUtils.contains(args, "--help")
                || ArrayUtils.contains(args, "-?")) {
            printHelp();
            System.exit(0);
        }

        // parse options
        GnuParser parser = new GnuParser();
        this.commandLine = parser.parse(getCommandLineOptions(), args);

        // setup
        try {
            setup();
        } catch (Exception e) {
            System.out.println("ERROR: Failed to set up RCR application: " + e.getMessage());
            System.exit(1);
        }

        // validate
        if (!validateOptions()) {
            System.exit(1);
        }
    }

    /**
     * Sets up the services, etc needed for Rcr execution.<br>
     * Note to subclasses: overriding specific getters is preferred to
     * overriding the entire setup() method.
     *
     * @throws Exception any exception that occurs during setup will terminate
     *             the application.
     */
    protected void setup() throws Exception {
        this.sysCfg = getSystemConfiguration();
        this.dbService = getDatabaseService();
        this.kamStore = getKamStore();
        this.cacheService = getCacheableResourceService();
        this.dataFileService = getDataFileService();
        this.mappingService = getMappingService();
        this.dialect = getDialect();

        if (sysCfg == null || dbService == null || kamStore == null || cacheService == null
                || dataFileService == null || mappingService == null || dialect == null) {
            throw new IllegalStateException("null service created");
        }
    }

    /**
     * Prints the help/usage for Rcr.
     */
    private void printHelp() {
        HelpFormatter hf = new HelpFormatter();
        hf.printHelp(" whistle [-r <run_name> -f <data_file> -k <kam_name> -u <namespace_url>]",
                getCommandLineOptions());
    }

    /**
     * Validate the provided arguments.
     *
     * @return
     */
    protected boolean validateOptions() {
        boolean valid = true;
        if (commandLine.hasOption(ANLST_SHORT_OPT)) {
            if (commandLine.hasOption(FOLD_CHANGE_SHORT_OPT)) {
                System.out.println(
                        "WARNING: fold change cutoff specified when using analyst selection. Cutoff will be ignored.");
            }
            if (commandLine.hasOption(PVAL_SHORT_OPT)) {
                System.out.println(
                        "WARNING: p-value cutoff specified when using analyst selection. Cutoff will be ignored.");
            }
            if (commandLine.hasOption(ABUN_SHORT_OPT)) {
                System.out.println(
                        "WARNING: abundance cutoff specified when using analyst selection. Cutoff will be ignored.");
            }
        } else {
            if (!commandLine.hasOption(FOLD_CHANGE_SHORT_OPT) || !commandLine.hasOption(PVAL_SHORT_OPT)
                    || !commandLine.hasOption(ABUN_SHORT_OPT)) {
                System.err.println(
                        "ERROR: Fold change, p-value and abundance cutoffs must be specified if not using analyst selection");
                valid = false;
            }
        }
        if (commandLine.hasOption(FOLD_CHANGE_SHORT_OPT)
                && !isDouble(commandLine.getOptionValue(FOLD_CHANGE_SHORT_OPT))) {
            System.err.println("ERROR: Invalid fold change cutoff. Value must be a decimal.");
            valid = false;
        }
        if (commandLine.hasOption(PVAL_SHORT_OPT) && !isDouble(commandLine.getOptionValue(PVAL_SHORT_OPT))) {
            System.err.println("ERROR: Invalid p-value cutoff. Value must be a decimal.");
            valid = false;
        }
        if (commandLine.hasOption(ABUN_SHORT_OPT) && !isDouble(commandLine.getOptionValue(ABUN_SHORT_OPT))) {
            System.err.println("ERROR: Invalid abundance cutoff. Value must be a decimal.");
            valid = false;
        }

        try {
            parseNamespace(cacheService, commandLine.getOptionValue(NS_URL_SHORT_OPT));
        } catch (Exception e) {
            System.err
                    .println("ERROR: Could not validate namespace URL. Confirm the URL is correct and accessible.");
            valid = false;
        }

        File f = new File(commandLine.getOptionValue(DATA_SHORT_OPT));
        if (!f.exists() || !f.canRead()) {
            System.err.println("ERROR: Could not open data file for reading. Confirm data file path");
            valid = false;
        }

        String runName = commandLine.getOptionValue(RUN_NAME_SHORT_OPT);
        valid = touchFile(valid, new File(runName + RESULT_FILE_SUFFIX));

        boolean isDetailedOutput = commandLine.hasOption(DETAIL_LONG_OPT);
        if (isDetailedOutput) {
            valid = touchFile(valid, new File(runName + MAPPING_FILE_SUFFIX));
            valid = touchFile(valid, new File(runName + DETAIL_FILE_SUFFIX));
        }

        if (commandLine.hasOption(POP_SIZE_SHORT_OPT)) {
            try {
                Integer.valueOf(commandLine.getOptionValue(POP_SIZE_SHORT_OPT));
            } catch (NumberFormatException e) {
                System.err.println("ERROR: Invalid population size. Value must be a positive integer.");
            }
        }

        if (commandLine.hasOption(SPECIES_TAXID_LONG_OPT)) {
            try {
                Integer.valueOf(commandLine.getOptionValue(SPECIES_TAXID_LONG_OPT));
            } catch (NumberFormatException e) {
                System.err.println("ERROR: Invalid species taxonomy id. Value must be a positive integer.");
            }
        }

        return valid;
    }

    protected void run() throws Exception {
        logger.debug("Parsing Namespace");
        Namespace ns = parseNamespace(cacheService, commandLine.getOptionValue(NS_URL_SHORT_OPT));
        File input = new File(commandLine.getOptionValue(DATA_SHORT_OPT));
        logger.debug("Parsing input file");
        Collection<Comparison> comparisons = dataFileService.process(input, ns);

        if (comparisons.isEmpty()) {
            System.err.println("No comparisons were found in input file.");
            System.exit(1);
        }
        logger.info("Parsed {} comparisons from file", comparisons.size());

        // If > 1 comparison parsed, prompt the user for the comparison they want to use
        Comparison comparison;
        if (comparisons.size() > 1) {
            List<Comparison> compList = new ArrayList<Comparison>(comparisons);
            System.out.println("Select the comparison to use:");
            int idx = 0;
            for (Comparison c : compList) {
                System.out.println(++idx + ": " + c.getName());
            }
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            int selection = -1;
            while (selection == -1) {
                String in = br.readLine();
                if (StringUtils.isNumeric(in)) {
                    int tmp = Integer.parseInt(in);
                    if (tmp > 0 && tmp <= compList.size()) {
                        selection = tmp;
                    }
                } else {
                    System.err.println("Invalid selection. Please select a comparison");
                }
            }
            comparison = compList.get(--selection);
        } else {
            comparison = comparisons.iterator().next();
        }

        // if details is enabled, write additional files
        boolean showDetail = commandLine.hasOption(DETAIL_LONG_OPT);
        Map<Measurement, String> debugInfo = new HashMap<Measurement, String>();

        Collection<Measurement> measurements = comparison.getMeasurements();
        logger.info("Comparison {} contains {} measurements", comparison.getName(), measurements.size());

        Cutoffs cutoffs;
        if (commandLine.hasOption(ANLST_SHORT_OPT)) {
            cutoffs = new Cutoffs(true);
        } else {

            cutoffs = new Cutoffs(Double.valueOf(commandLine.getOptionValue(FOLD_CHANGE_SHORT_OPT)),
                    Double.valueOf(commandLine.getOptionValue(PVAL_SHORT_OPT)),
                    Double.valueOf(commandLine.getOptionValue(ABUN_SHORT_OPT)));
        }

        String kamName = commandLine.getOptionValue(KAM_SHORT_OPT);
        Kam kam;
        logger.debug("Retrieving KAM '{}' from KamStore", kamName);
        if (commandLine.hasOption(SPECIES_TAXID_LONG_OPT)) {
            final String taxId = commandLine.getOptionValue(SPECIES_TAXID_LONG_OPT);

            // taxId string has already been validated as a numeric
            final int speciesTaxId = Integer.parseInt(taxId);

            logger.debug("Collapsing KAM '{}' to tax id {}.", kamName, String.valueOf(speciesTaxId));
            kam = kamStore.getKam(kamName);
            kam = new KamSpecies(new KamDialect(kam, dialect),
                    new DefaultSpeciesDialect(kam.getKamInfo(), kamStore, speciesTaxId, false), kamStore);
        } else {
            kam = new KamDialect(kamStore.getKam(kamName), dialect);
        }
        logger.info("Completed KAM retrieval");

        logger.debug("Finding mechanisms");
        BasicHypothesisFinder hypFinder = new BasicHypothesisFinder();
        List<Hypothesis> hyps = hypFinder.findAll(kam, 2);
        logger.info("Found {} mechanisms in KAM", hyps.size());

        logger.debug("Mapping measurements to Mechanisms");
        MappingResult mappingResult = mappingService.map(kam, hyps, measurements);
        logger.info("Mapped {} measurements to Mechanisms", mappingResult.getMappedMeasurements().size());

        if (showDetail) {
            // if detail option is enabled the debug service is required
            assert mappingService instanceof DebugMeasurementMappingService;
            DebugMeasurementMappingService msvc = (DebugMeasurementMappingService) mappingService;

            Collection<Measurement> unmapped = msvc.getUnmapped();
            String status = NOT_MAPPED_TO_KAM_STATUS;
            for (Measurement m : unmapped) {
                debugInfo.put(m, status);
            }

            Map<KamNode, Set<Measurement>> notInPopulation = msvc.getNotInPopulation();
            for (Map.Entry<KamNode, Set<Measurement>> entry : notInPopulation.entrySet()) {
                status = format(NOT_PRESENT_IN_POPULATION_STATUS, entry.getKey().getLabel());
                for (Measurement m : entry.getValue()) {
                    debugInfo.put(m, status);
                }
            }

            Map<KamNode, Set<Measurement>> collapsed = msvc.getCollapsed();
            for (Map.Entry<KamNode, Set<Measurement>> entry : collapsed.entrySet()) {
                status = format(COLLAPSED_STATUS, entry.getKey().getLabel());
                for (Measurement m : entry.getValue()) {
                    debugInfo.put(m, status);
                }
            }
        }

        int popSize = commandLine.hasOption(POP_SIZE_SHORT_OPT)
                ? Integer.parseInt(commandLine.getOptionValue(POP_SIZE_SHORT_OPT))
                : mappingResult.getPopulationSize();

        logger.info("Using population size: {}", popSize);

        logger.debug("Computing scores");
        Scorer scorer = showDetail ? new DebugScorer(debugInfo) : new Scorer();
        Collection<ScoredHypothesis> scores = scorer.score(hyps, mappingResult.getMappedMeasurements(), cutoffs,
                popSize);
        logger.info("Found {} scores", scores.size());

        String runName = commandLine.getOptionValue(RUN_NAME_SHORT_OPT);
        File resultFile = new File(runName + RESULT_FILE_SUFFIX);

        // write scored hypothesis file
        FileWriter out = new FileWriter(resultFile);
        writeOutput(out, scores);
        logger.info("Complete: scores have been saved to {}", resultFile.getAbsolutePath());

        // write detail files if detailed output requested
        if (showDetail) {
            // if detail option is enabled the debug service is required
            assert mappingService instanceof DebugMeasurementMappingService;
            assert scorer instanceof DebugScorer;
            DebugMeasurementMappingService msvc = (DebugMeasurementMappingService) mappingService;
            DebugScorer debugScorer = (DebugScorer) scorer;

            // write mapping file
            File mappingFile = new File(runName + MAPPING_FILE_SUFFIX);
            logger.info("Saving measurement information");
            writeMeasurementDebug(new FileWriter(mappingFile), mappingResult.getMappedMeasurements(), debugInfo);
            logger.info("Mapping output saved to {}", mappingFile);

            // map hyps to all downstreams in the population
            Map<KamNode, Set<Downstream>> hypMap = computeHypMap(hyps, msvc.getInPopulation());

            Map<KamNode, MappedMeasurement> stateChanges = debugScorer.getStateChangeMap();

            // write mechanism detail file
            File detailFile = new File(runName + DETAIL_FILE_SUFFIX);
            FileWriter mechout = new FileWriter(detailFile);
            writeMechanismDetail(mechout, hypMap, scores, stateChanges);
            logger.info("Mechanism detail saved to {}", detailFile);
        }
    }

    /**
     * Compute the {@link KamNode hypothesis node} to
     * {@link Downstream downstreams} that are in the population measured.
     *
     * @param hyps the hypothesis {@link List list}
     * @param population the {@link Set set} of measurements in the
     * population
     * @return a {@link Map map} of {@link KamNode hypothesis node} to
     * {@link Downstream downstreams} in the measured population
     */
    private static Map<KamNode, Set<Downstream>> computeHypMap(List<Hypothesis> hyps,
            Map<Integer, KamNode> population) {
        Map<KamNode, Set<Downstream>> hypDownstreams = new HashMap<Kam.KamNode, Set<Downstream>>();
        for (final Hypothesis hyp : hyps) {
            // new set of downstreams
            Set<Downstream> newset = new HashSet<Downstream>();

            // remove all downstreams not in population
            Set<Downstream> oldset = hyp.getDownstreams();
            Iterator<Downstream> it = oldset.iterator();
            while (it.hasNext()) {
                Downstream down = it.next();
                KamNode dn = down.getKamNode();

                KamNode pn = population.get(dn.getId());
                if (pn != null) {
                    newset.add(new Downstream(pn, down.getDirectionType()));
                }
            }

            // store mechanism to downstreams in population
            hypDownstreams.put(hyp.getKamNode(), newset);
        }
        return hypDownstreams;
    }

    private boolean touchFile(boolean valid, File f) {
        boolean touchOk = true;
        try {
            FileUtils.touch(f);
        } catch (IOException e) {
            touchOk = false;
        }
        if (!touchOk || !f.canWrite()) {

            System.err.println(
                    "ERROR: Could not open file for " + "writing. Do you have permission to write the file here?");
            valid = false;
        }
        return valid;
    }

    /**
     * Parse a namespace for a given URL
     *
     * @param cacheResource
     * @param namespaceUrl
     * @return
     * @throws Exception
     */
    private Namespace parseNamespace(CacheableResourceService cacheResource, String namespaceUrl) throws Exception {
        NamespaceHeaderParser nhp = new NamespaceHeaderParser();
        ResolvedResource nsResource = cacheResource.resolveResource(ResourceType.NAMESPACES, namespaceUrl);
        NamespaceHeader header = nhp.parseNamespace(namespaceUrl, nsResource.getCacheResourceCopy());
        return new Namespace(header.getNamespaceBlock().getKeyword(), namespaceUrl);
    }

    /**
     * Write the score output
     *
     * @param out
     * @param scores
     * @throws IOException
     */
    protected void writeOutput(FileWriter out, Collection<ScoredHypothesis> scores) throws IOException {
        CSVWriter writer = new CSVWriter(out);
        String[] line = new String[] { ID_HEADER, DIRECTION_HEADER, CORRECT_HEADER, RICHNESS_HEADER,
                CONCORDANCE_HEADER, AMBIGUOUS_HEADER, CONTRA_HEADER, POSSIBLE_HEADER, OBSERVED_HEADER };
        writer.writeNext(line);
        for (ScoredHypothesis score : scores) {
            int idx = -1;
            line[++idx] = score.getKamNode().getLabel();
            // line[++idx] = String.valueOf(score.getDepth());
            line[++idx] = valueOf(score.getDirectionType().getValue());
            line[++idx] = valueOf(score.getNumberCorrect());
            line[++idx] = valueOf(score.getRichness());
            line[++idx] = valueOf(score.getConcordance());
            line[++idx] = valueOf(score.getNumberAmbiguous());
            line[++idx] = valueOf(score.getNumberContra());
            line[++idx] = valueOf(score.getPossible());
            line[++idx] = valueOf(score.getObserved());
            writer.writeNext(line);
        }
        writer.flush();
        writer.close();
    }

    protected void writeMeasurementDebug(FileWriter out, Collection<MappedMeasurement> mappedMeasurements,
            Map<Measurement, String> debugInfo) throws IOException {

        Map<Measurement, KamNode> nodeMap = new HashMap<Measurement, Kam.KamNode>();
        for (MappedMeasurement mm : mappedMeasurements) {
            nodeMap.put(mm.getMeasurement(), mm.getKamNode());
        }

        CSVWriter writer = new CSVWriter(out);
        String[] line = new String[] { ID_HEADER, KAM_NODE_HEADER, STATUS_HEADER };
        writer.writeNext(line);
        Map<Measurement, String> idMap = ((DebugIdAmpDataFileService) dataFileService).getIdMap();

        for (Map.Entry<Measurement, String> entry : idMap.entrySet()) {
            Measurement m = entry.getKey();
            int idx = -1;
            line[++idx] = entry.getValue();
            line[++idx] = nodeMap.get(m) == null ? "" : nodeMap.get(m).getLabel();
            line[++idx] = debugInfo.get(m);
            writer.writeNext(line);
        }
        writer.flush();
        writer.close();
    }

    protected void writeMechanismDetail(FileWriter out, Map<KamNode, Set<Downstream>> hypDownstreams,
            Collection<ScoredHypothesis> scores, Map<KamNode, MappedMeasurement> stateChanges) throws IOException {

        CSVWriter csv = new CSVWriter(out);
        String[] line = new String[] { SOURCE_HEADER, RELATIONSHIP_HEADER, TARGET_HEADER, TYPE_HEADER,
                DIRECTION_HEADER };
        csv.writeNext(line);

        for (final ScoredHypothesis score : scores) {
            Prediction pred = score.getPrediction();

            line[0] = score.getKamNode().getLabel();

            Set<Downstream> allDownstreams = hypDownstreams.get(score.getKamNode());
            for (final Downstream down : allDownstreams) {
                final KamNode downNode = down.getKamNode();
                final MappedMeasurement mm = stateChanges.get(downNode);
                final String scoreType = getScoreType(pred, down);

                line[1] = translateDirection(down.getDirectionType());
                line[2] = downNode.getLabel();
                line[3] = scoreType;

                if (mm == null) {
                    line[4] = NOT_SIGNIFICANT;
                } else {
                    line[4] = mm.getMeasurement().getDirection().getDisplayValue();
                }

                csv.writeNext(line);
            }
            csv.flush();
        }
        csv.close();
    }

    protected String getScoreType(final Prediction pred, final Downstream down) {
        if (pred == null) {
            return NOT_SIGNIFICANT;
        }

        if (pred.getCorrect().contains(down)) {
            return CORRECT;
        } else if (pred.getContra().contains(down)) {
            return CONTRA;
        } else if (pred.getAmbiguous().contains(down)) {
            return AMBIGUOUS;
        }

        return NOT_SIGNIFICANT;
    }

    protected String translateDirection(final DirectionType dir) {
        if (dir == null) {
            return null;
        }

        switch (dir) {
        case UP:
            return INCREASES.getDisplayValue();
        case DOWN:
            return DECREASES.getDisplayValue();
        default:
            return AMBIGUOUS;
        }
    }

    /**
     * Retrieve the {@link Options} applicable to Rcr.
     *
     * @return
     */
    protected Options getCommandLineOptions() {
        Options ret = new Options();
        Option o = new Option(KAM_SHORT_OPT, KAM_LONG_OPT, true,
                "KAM name. Must be present in the local KAM catalog.");
        o.setRequired(true);
        ret.addOption(o);

        o = new Option(DATA_SHORT_OPT, DATA_LONG_OPT, true,
                "Data set input file. The file should be in valid IdAMP format.  "
                        + "If more than one comparison is represented you will be prompted to choose one.");
        o.setRequired(true);
        ret.addOption(o);

        o = new Option(RUN_NAME_SHORT_OPT, RUN_NAME_LONG_OPT, true,
                "Name of the whistle run used as a file prefix.");
        o.setRequired(true);
        ret.addOption(o);

        o = new Option(NS_URL_SHORT_OPT, NS_URL_LONG_OPT, true,
                "Resource location URL of the namespace containing all values of the data set input file.");
        o.setRequired(true);
        ret.addOption(o);

        ret.addOption(new Option(ANLST_SHORT_OPT, ANLST_LONG_OPT, false,
                "Use analyst selection to filter Measurements to state changes."));
        ret.addOption(new Option(FOLD_CHANGE_SHORT_OPT, FOLD_CHANGE_LONG_OPT, true,
                "Fold change cutoff to apply to Measurements for state change generation. Applicable only if not using analyst selection."));
        ret.addOption(new Option(PVAL_SHORT_OPT, PVAL_LONG_OPT, true,
                "P-value cutoff to apply to Measurements for state change generation. Applicable only if not using analyst selection."));
        ret.addOption(new Option(ABUN_SHORT_OPT, ABUN_LONG_OPT, true,
                "Abundance cutoff to apply to Measurements for state change generation. Applicable only if not using analyst selection."));
        ret.addOption(new Option(POP_SIZE_SHORT_OPT, POP_SIZE_LONG_OPT, true,
                "Population size. The default is to calculate population based on the data set measurements that exist in the KAM."));

        ret.addOption(new Option(DETAIL_LONG_OPT, false,
                "Output a mapping and mechanism detail file that shows additional information"));

        ret.addOption(new Option(SPECIES_TAXID_SHORT_OPT, SPECIES_TAXID_LONG_OPT, true,
                "The species taxonomy id used to collapse orthologous nodes."));

        return ret;
    }

    /**
     * Obtain the {@link SystemConfiguration}.<br>
     * Defaults to obtaining via the BELFRAMEWORK_HOME environment variable.
     *
     * @return
     * @throws Exception
     */
    protected SystemConfiguration getSystemConfiguration() throws Exception {
        final String bfhome = getenv(ENV_BELFRAMEWORK_HOME);
        if (hasLength(bfhome)) {
            sysCfg = createSystemConfiguration();
            return sysCfg;
        }

        String whistleHome = getenv(WHISTLE_HOME);

        // assert that WHISTLE_HOME is set, alert the user
        assert noLength(whistleHome);
        if (noLength(whistleHome)) {
            throw new IllegalStateException("WHISTLE_HOME needs to be set.");
        }

        String cfgPath = asPath(getenv(WHISTLE_HOME), CONFIG_DIRECTORY, BELFRAMEWORK_CONFIG);

        sysCfg = createSystemConfiguration(new File(cfgPath));
        return sysCfg;
    }

    /**
     * Obtain the {@link DatabaseService} for use in Rcr.<br>
     * Defaults to {@link DatabaseServiceImpl}
     *
     * @return
     * @throws Exception
     */
    protected DatabaseService getDatabaseService() throws Exception {
        return dbService == null ? new DatabaseServiceImpl() : dbService;
    }

    /**
     * Obtain the {@link CacheableResourceService} for use in Rcr.<br>
     * Defaults to {@link DefaultCacheableResourceService}
     *
     * @return
     * @throws Exception
     */
    protected CacheableResourceService getCacheableResourceService() throws Exception {
        return cacheService == null ? new DefaultCacheableResourceService() : cacheService;
    }

    /**
     * Obtain the {@link KamStore} for use in Rcr.<br>
     * Defaults to {@link KamStoreImpl}
     *
     * @return
     * @throws Exception
     */
    protected KAMStore getKamStore() throws Exception {
        if (kamStore != null) {
            return kamStore;
        }
        if (sysCfg == null) {
            throw new IllegalStateException("System Configuration must be valid for KamStore creation");
        }
        if (dbService == null) {
            throw new IllegalStateException("DBService must be valid for KamStore creation");
        }
        DBConnection dbc = dbService.dbConnection(sysCfg.getKamURL(), sysCfg.getKamUser(), sysCfg.getKamPassword());
        return new KAMStoreImpl(dbc);
    }

    /**
     * Obtain the {@link DataFileService} for use in processing the input data
     * file.
     *
     * @return
     * @throws Exception
     */
    protected DataFileService getDataFileService() throws Exception {

        return dataFileService == null ? (commandLine.hasOption(DETAIL_LONG_OPT) ? new DebugIdAmpDataFileService()
                : new DefaultIdAMPDataFileService()) : dataFileService;
    }

    /**
     * Obtain the {@link MeasurementMappingService} for use in mapping
     * {@link Measurement}s to a {@link Kam}
     * @return
     * @throws Exception
     */
    protected MeasurementMappingService getMappingService() throws Exception {
        if (mappingService != null) {
            return mappingService;
        }
        if (getKamStore() == null) {
            throw new IllegalStateException("KamStore must be valid for MeasurementMappingService creation");
        }
        // if debug is enabled, use the debug mappingService
        DefaultMeasurementMappingService mappingService;
        if (commandLine.hasOption(DETAIL_LONG_OPT)) {
            mappingService = new DebugMeasurementMappingService();
        } else {
            mappingService = new DefaultMeasurementMappingService();
        }

        mappingService.setCollapsingStrategy(new DefaultCollapsingStrategy(commandLine.hasOption(ANLST_SHORT_OPT)));
        mappingService.setKamStore(getKamStore());
        return mappingService;
    }

    /**
     * Obtain the {@link Dialect} for use in Rcr.<br>
     * Defaults to {@link RcrDialect}
     *
     * @return
     */
    protected Dialect getDialect() {
        if (dialect != null) {
            return dialect;
        }
        if (kamStore == null) {
            throw new IllegalStateException("KamStore must be valid for Dialect creation");
        }
        return new RcrDialect(kamStore);
    }

    /**
     * Verify a {@link Double} can be parsed from the provided {@link String}.
     *
     * @param d
     * @return
     */
    protected static boolean isDouble(String d) {
        try {
            Double.valueOf(d);
        } catch (NumberFormatException e) {
            return false;
        }
        return true;
    }

    /**
     * Get a string for a number.<br>
     * This implementation returns "NA" if the number is null; useful for R processing
     * @param number
     * @return
     */
    protected String valueOf(Number number) {
        return number == null ? "NA" : String.valueOf(number);
    }

    /**
     * Simple {@link Dialect} for usage in RCR:<br>
     * Uses the label from the first (arbitrary) supporting term found for the
     * node.
     *
     * @author Steve Ungerer
     */
    private class RcrDialect implements Dialect {
        private KAMStore kamStore;

        public RcrDialect(KAMStore kamStore) {
            this.kamStore = kamStore;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String getLabel(KamNode kamNode) {
            String label = kamNode.getLabel();
            try {
                List<BelTerm> terms = kamStore.getSupportingTerms(kamNode);
                if (!terms.isEmpty()) {
                    BelTerm bt = terms.get(0);
                    Term t = BELParser.parseTerm(bt.getLabel());
                    label = t.toBELShortForm();
                }
            } catch (Exception e) {
                throw new RuntimeException("Failed to retrieve label for kamNode", e);
            }
            return label;
        }
    }
}