SiLeBAT/FSK-Lab

View on GitHub
de.bund.bfr.knime.pmm.nodes/src/de/bund/bfr/knime/pmm/common/writer/WriterUtils.java

Summary

Maintainability
F
1 wk
Test Coverage
package de.bund.bfr.knime.pmm.common.writer;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.xml.stream.XMLStreamException;

import org.apache.commons.lang3.StringUtils;
import org.hsh.bfr.db.DBKernel;
import org.knime.core.node.ExecutionContext;
import org.sbml.jsbml.Annotation;
import org.sbml.jsbml.AssignmentRule;
import org.sbml.jsbml.Compartment;
import org.sbml.jsbml.ListOf;
import org.sbml.jsbml.Model;
import org.sbml.jsbml.Parameter;
import org.sbml.jsbml.SBMLDocument;
import org.sbml.jsbml.SBMLReader;
import org.sbml.jsbml.Species;
import org.sbml.jsbml.Unit;
import org.sbml.jsbml.UnitDefinition;
import org.sbml.jsbml.ext.comp.CompConstants;
import org.sbml.jsbml.ext.comp.CompModelPlugin;
import org.sbml.jsbml.ext.comp.CompSBMLDocumentPlugin;
import org.sbml.jsbml.ext.comp.CompSBasePlugin;
import org.sbml.jsbml.ext.comp.ExternalModelDefinition;
import org.sbml.jsbml.ext.comp.ModelDefinition;
import org.sbml.jsbml.ext.comp.ReplacedBy;
import org.sbml.jsbml.ext.comp.Submodel;
import org.sbml.jsbml.xml.XMLNode;
import org.sbml.jsbml.xml.XMLTriple;

import de.bund.bfr.knime.pmm.common.AgentXml;
import de.bund.bfr.knime.pmm.common.CatalogModelXml;
import de.bund.bfr.knime.pmm.common.DepXml;
import de.bund.bfr.knime.pmm.common.EstModelXml;
import de.bund.bfr.knime.pmm.common.IndepXml;
import de.bund.bfr.knime.pmm.common.LiteratureItem;
import de.bund.bfr.knime.pmm.common.MatrixXml;
import de.bund.bfr.knime.pmm.common.MiscXml;
import de.bund.bfr.knime.pmm.common.ParamXml;
import de.bund.bfr.knime.pmm.common.PmmXmlDoc;
import de.bund.bfr.knime.pmm.common.PmmXmlElementConvertable;
import de.bund.bfr.knime.pmm.common.generictablemodel.KnimeTuple;
import de.bund.bfr.knime.pmm.common.math.MathUtilities;
import de.bund.bfr.knime.pmm.common.pmmtablemodel.Model1Schema;
import de.bund.bfr.knime.pmm.common.pmmtablemodel.Model2Schema;
import de.bund.bfr.knime.pmm.common.pmmtablemodel.TimeSeriesSchema;
import de.bund.bfr.knime.pmm.common.units.UnitsFromDB;
import de.bund.bfr.knime.pmm.dbutil.DBUnits;
import de.bund.bfr.pmfml.ModelClass;
import de.bund.bfr.pmfml.ModelType;
import de.bund.bfr.pmfml.PMFUtil;
import de.bund.bfr.pmfml.file.ExperimentalDataFile;
import de.bund.bfr.pmfml.file.ManualSecondaryModelFile;
import de.bund.bfr.pmfml.file.ManualTertiaryModelFile;
import de.bund.bfr.pmfml.file.OneStepSecondaryModelFile;
import de.bund.bfr.pmfml.file.OneStepTertiaryModelFile;
import de.bund.bfr.pmfml.file.PrimaryModelWDataFile;
import de.bund.bfr.pmfml.file.PrimaryModelWODataFile;
import de.bund.bfr.pmfml.file.TwoStepSecondaryModelFile;
import de.bund.bfr.pmfml.file.TwoStepTertiaryModelFile;
import de.bund.bfr.pmfml.model.ExperimentalData;
import de.bund.bfr.pmfml.model.ManualSecondaryModel;
import de.bund.bfr.pmfml.model.ManualTertiaryModel;
import de.bund.bfr.pmfml.model.OneStepSecondaryModel;
import de.bund.bfr.pmfml.model.OneStepTertiaryModel;
import de.bund.bfr.pmfml.model.PrimaryModelWData;
import de.bund.bfr.pmfml.model.PrimaryModelWOData;
import de.bund.bfr.pmfml.model.TwoStepSecondaryModel;
import de.bund.bfr.pmfml.model.TwoStepTertiaryModel;
import de.bund.bfr.pmfml.numl.NuMLDocument;
import de.bund.bfr.pmfml.sbml.Correlation;
import de.bund.bfr.pmfml.sbml.DataSourceNode;
import de.bund.bfr.pmfml.sbml.GlobalModelIdNode;
import de.bund.bfr.pmfml.sbml.LimitsConstraint;
import de.bund.bfr.pmfml.sbml.Metadata;
import de.bund.bfr.pmfml.sbml.MetadataAnnotation;
import de.bund.bfr.pmfml.sbml.Model2Annotation;
import de.bund.bfr.pmfml.sbml.ModelRule;
import de.bund.bfr.pmfml.sbml.ModelVariable;
import de.bund.bfr.pmfml.sbml.PMFCoefficient;
import de.bund.bfr.pmfml.sbml.PMFCompartment;
import de.bund.bfr.pmfml.sbml.PMFSpecies;
import de.bund.bfr.pmfml.sbml.PMFUnit;
import de.bund.bfr.pmfml.sbml.PMFUnitDefinition;
import de.bund.bfr.pmfml.sbml.PrimaryModelNode;
import de.bund.bfr.pmfml.sbml.Reference;
import de.bund.bfr.pmfml.sbml.ReferenceSBMLNode;
import de.bund.bfr.pmfml.sbml.ReferenceType;
import de.bund.bfr.pmfml.sbml.SBMLFactory;
import de.bund.bfr.pmfml.sbml.SecDep;
import de.bund.bfr.pmfml.sbml.SecIndep;
import de.bund.bfr.pmfml.sbml.Uncertainties;

public class WriterUtils {

    private WriterUtils() {
    }

    public static PMFCoefficient paramXml2Coefficient(ParamXml paramXml) {
        String name = paramXml.name;
        double value = (paramXml.value == null) ? 0.0 : paramXml.value;
        String unit = (paramXml.unit == null) ? "dimensionless" : PMFUtil.createId(paramXml.unit);
        Double P = paramXml.P;
        Double error = paramXml.error;
        Double t = paramXml.t;
        Boolean isStart = paramXml.isStartParam;

        Map<String, Double> correlationMap = paramXml.correlations;
        Correlation[] correlations = new Correlation[correlationMap.size()];
        int i = 0;
        for (Map.Entry<String, Double> entry : correlationMap.entrySet()) {
            if (entry.getValue() == null) {
                correlations[i] = new Correlation(entry.getKey());
            } else {
                correlations[i] = new Correlation(entry.getKey(), entry.getValue());
            }
            i++;
        }

        String desc = paramXml.description;

        PMFCoefficient coefficient = SBMLFactory.createPMFCoefficient(name, value, unit, P, error, t, correlations,
                desc, isStart);
        return coefficient;
    }

    public static Uncertainties estModel2Uncertainties(EstModelXml estModel) {
        Integer id = estModel.id;
        String modelName = estModel.name;
        String comment = estModel.comment;
        Double r2 = estModel.r2;
        Double rms = estModel.rms;
        Double sse = estModel.sse;
        Double aic = estModel.aic;
        Double bic = estModel.bic;
        Integer dof = estModel.dof;
        Uncertainties uncertainties = SBMLFactory.createUncertainties(id, modelName, comment, r2, rms, sse, aic, bic,
                dof);
        return uncertainties;
    }

    public static Reference literatureItem2Reference(LiteratureItem lit) {
        String author = lit.author;
        Integer year = lit.year;
        String title = lit.title;
        String abstractText = lit.abstractText;
        String journal = lit.journal;
        String volume = lit.volume;
        String issue = lit.issue;
        Integer page = lit.page;
        Integer approvalMode = lit.approvalMode;
        String website = lit.website;
        ReferenceType type = (lit.type == null) ? null : ReferenceType.fromValue(lit.type);
        String comment = lit.comment;

        Reference ref = SBMLFactory.createReference(author, year, title, abstractText, journal, volume, issue, page,
                approvalMode, website, type, comment);
        return ref;
    }

    public static PMFCompartment matrixXml2Compartment(MatrixXml matrixXml, PmmXmlDoc miscDoc) {
        ModelVariable[] modelVariables = new ModelVariable[miscDoc.size()];
        for (int i = 0; i < miscDoc.size(); i++) {
            MiscXml miscXml = (MiscXml) miscDoc.get(i);
            modelVariables[i] = new ModelVariable(miscXml.name, miscXml.value);
        }
        String compartmentDetail = matrixXml.detail;

        String compartmentId;
        String compartmentName;

        if (matrixXml.name == null) {
            compartmentId = "MISSING_COMPARTMENT";
            compartmentName = "MISSING_COMPARTMENT";
        } else {
            compartmentId = PMFUtil.createId(matrixXml.name);
            compartmentName = matrixXml.name;
        }

        // Gets PMF code from DB
        String[] colNames = { "CodeSystem", "Basis" };
        String[] colVals = { "PMF", matrixXml.id.toString() };
        String pmfCode = (String) DBKernel.getValue(null, "Codes_Matrices", colNames, colVals, "Codes");
        PMFCompartment compartment = SBMLFactory.createPMFCompartment(compartmentId, compartmentName, pmfCode,
                compartmentDetail, modelVariables);
        return compartment;
    }

    public static PMFSpecies createSpecies(AgentXml agentXml, String unit, String compartmentId) {
        // Creates species and adds it to the model
        String speciesId = PMFUtil.createId("species" + agentXml.id);
        String speciesUnit = (unit == null) ? "dimensionless" : PMFUtil.createId(unit);
        String speciesName = (String) DBKernel.getValue("Agenzien", "ID", agentXml.id.toString(), "Agensname");
        String casNumber;
        if (speciesName == null) {
            speciesName = agentXml.name;
            casNumber = null;
        } else {
            casNumber = (String) DBKernel.getValue("Agenzien", "ID", agentXml.id.toString(), "Cas_Nummer");
        }
        String speciesDetail = agentXml.detail;

        PMFSpecies species = SBMLFactory.createPMFSpecies(compartmentId, speciesId, speciesName, speciesUnit, casNumber,
                speciesDetail, null);
        return species;
    }

    public static PMFUnitDefinition createUnitFromDB(String unit) throws XMLStreamException {
        if (!DBUnits.getDBUnits().containsKey(unit)) {
            return null;
        }

        String id = PMFUtil.createId(unit);
        String name = unit;
        String transformation = null;
        PMFUnit[] units = null;

        UnitsFromDB dbUnit = DBUnits.getDBUnits().get(unit);

        String mathml = dbUnit.mathMlString;
        if (mathml != null && !mathml.isEmpty()) {

            // Parse transformation name if present
            int transformationIndex = mathml.indexOf("<transformation");
            if (transformationIndex == -1) {
                transformation = null;
            } else {
                int endTransformationIndex = mathml.indexOf("<", transformationIndex + 1);
                int firstQuotePos = mathml.indexOf("\"", transformationIndex);
                int secQuotePos = mathml.indexOf("\"", firstQuotePos + 1);
                transformation = mathml.substring(firstQuotePos + 1, secQuotePos);

                // Removes the transformation annotation (this
                // annotation has a name which is not prefixed and then
                // cannot be parsed properly)
                mathml = mathml.substring(0, transformationIndex) + mathml.substring(endTransformationIndex);
            }

            // Remove namespace (all the DB units have this namespace
            // which is not necessary)
            mathml = mathml
                    .replaceAll("xmlns=\"http://sourceforge.net/projects/microbialmodelingexchange/files/Units\"", "");

            String preXml = "<?xml version='1.0' encoding='UTF-8' standalone='no'?>"
                    + "<sbml xmlns=\"http://www.sbml.org/sbml/level3/version1/core\"" + " level=\"3\" version=\"1\""
                    + " xmlns:pmf=\"http://sourceforge.net/projects/microbialmodelingexchange/files/PMF-ML\""
                    + "><model id=\"ID\">" + "<listOfUnitDefinitions>";

            String postXml = "</listOfUnitDefinitions></model></sbml>";
            String totalXml = preXml + mathml + postXml;

            // Create a new UnitDefinition from XML
            UnitDefinition xmlUnitDef = SBMLReader.read(totalXml).getModel().getUnitDefinition(0);

            ListOf<Unit> listOfUnits = xmlUnitDef.getListOfUnits();
            int numOfUnits = listOfUnits.size();
            units = new PMFUnit[numOfUnits];
            for (int i = 0; i < numOfUnits; i++) {
                Unit sbmlUnit = listOfUnits.get(i);
                Unit.Kind kind = sbmlUnit.getKind();
                double multiplier = sbmlUnit.isSetMultiplier() ? sbmlUnit.getMultiplier() : PMFUnit.DEFAULT_MULTIPLIER;
                int scale = sbmlUnit.isSetScale() ? sbmlUnit.getScale() : PMFUnit.DEFAULT_SCALE;
                double exponent = sbmlUnit.isSetExponent() ? sbmlUnit.getExponent() : PMFUnit.DEFAULT_EXPONENT;

                units[i] = new PMFUnit(multiplier, scale, kind, exponent);
            }
        }

        PMFUnitDefinition unitDefinition = new PMFUnitDefinition(id, name, transformation, units);
        return unitDefinition;
    }

    public static ModelRule createM1Rule(CatalogModelXml catModel, String variable, Reference[] references) {

        // Trims out the "Value=" from the formula
        int pos = catModel.formula.indexOf("=");
        String formula = catModel.formula.substring(pos + 1);

        // Removes boundary conditions
        formula = MathUtilities.getAllButBoundaryCondition(formula);
        // csymbol time needs a lower case t
        formula = formula.replace("Time", "time");

        ModelClass modelClass;
        if (catModel.modelClass == null) {
            modelClass = ModelClass.UNKNOWN;
        } else {
            modelClass = ModelClass.fromValue(catModel.modelClass);
        }

        String formulaName = StringUtils.defaultIfEmpty(catModel.name, "Missing formula name");

        int catModelId = catModel.id;

        ModelRule rule = new ModelRule(variable, formula, formulaName, modelClass, catModelId, references);
        return rule;
    }

    public static ModelRule createM2Rule(CatalogModelXml catModel, Reference[] references) {
        // Parses variable from the formula
        int pos = catModel.formula.indexOf("=");
        String variable = catModel.formula.substring(0, pos);

        // The remaining chunk contains the actual formula
        String formula = catModel.formula.substring(pos + 1);

        // Removes boundary conditions
        formula = MathUtilities.getAllButBoundaryCondition(formula);

        ModelClass modelClass;
        if (catModel.modelClass == null) {
            modelClass = ModelClass.UNKNOWN;
        } else {
            modelClass = ModelClass.fromValue(catModel.modelClass);
        }

        String formulaName = StringUtils.defaultIfEmpty("Missing formula name", catModel.name);

        int catModelId = catModel.id;

        ModelRule rule = new ModelRule(variable, formula, formulaName, modelClass, catModelId, references);
        return rule;
    }
    
    /** @return true if any tuple has a time series. */
    public static boolean hasData(List<KnimeTuple> tuples) {
        for (KnimeTuple tuple : tuples) {
            PmmXmlDoc mdData = tuple.getPmmXml(TimeSeriesSchema.ATT_TIMESERIES);
            if (mdData != null && mdData.size() > 0) {
                return true;
            }
        }
        return false;
    }

    public static void write(List<KnimeTuple> tuples, boolean isPMFX, String dir, String mdName, Metadata metadata,
            boolean splitModels, String notes, ExecutionContext exec, ModelType modelType) throws Exception {

        Parser parser;

        switch (modelType) {
        case EXPERIMENTAL_DATA:
            parser = new ExperimentalDataParser();
            break;
        case PRIMARY_MODEL_WDATA:
            parser = new PrimaryModelWDataParser();
            break;
        case PRIMARY_MODEL_WODATA:
            parser = new PrimaryModelWODataParser();
            break;
        case TWO_STEP_SECONDARY_MODEL:
            parser = new TwoStepSecondaryModelParser();
            break;
        case ONE_STEP_SECONDARY_MODEL:
            parser = new OneStepSecondaryModelParser();
            break;
        case MANUAL_SECONDARY_MODEL:
            parser = new ManualSecondaryModelParser();
            break;
        case TWO_STEP_TERTIARY_MODEL:
            parser = new TwoStepTertiaryModelParser();
            break;
        case ONE_STEP_TERTIARY_MODEL:
            parser = new OneStepTertiaryModelParser();
            break;
        case MANUAL_TERTIARY_MODEL:
            parser = new ManualTertiaryModelParser();
            break;
        default:
            throw new IllegalArgumentException("Invalid model type: " + modelType);
        }

        parser.write(tuples, isPMFX, dir, mdName, metadata, splitModels, notes, exec);
    }

    private interface Parser {
        public void write(List<KnimeTuple> tuples, boolean isPMFX, String dir, String mdName, Metadata metadata,
                boolean splitModels, String notes, ExecutionContext exec) throws Exception;
    }

    /**
     * Parse tuples from a table with timeseries.
     */
    private static class ExperimentalDataParser implements Parser {

        @Override
        public void write(List<KnimeTuple> tuples, boolean isPMFX, String dir, String mdName, Metadata metadata,
                boolean splitModels, String notes, ExecutionContext exec) throws Exception {

            List<ExperimentalData> eds = new LinkedList<>();
            for (int i = 0; i < tuples.size(); i++) {
                KnimeTuple tuple = tuples.get(i);

                String docName = String.format("%s_%d.numl", mdName, i);
                NuMLDocument doc = new DataParser(tuple, metadata, notes).getDocument();

                ExperimentalData ed = new ExperimentalData(docName, doc);
                eds.add(ed);
            }

            Path path = Paths.get(dir, mdName + (isPMFX ? ".pmfx" : ".pmf"));
            ExperimentalDataFile.write(path, eds);
        }
    }

    /**
     * Parse tuples from a table with primary models with data.
     */
    private static class PrimaryModelWDataParser implements Parser {

        @Override
        public void write(List<KnimeTuple> tuples, boolean isPMFX, String dir, String mdName, Metadata metadata,
                boolean splitModels, String notes, ExecutionContext exec) throws Exception {

            final String modelExtension = isPMFX ? "pmf" : "sbml";

            List<PrimaryModelWData> pms = new LinkedList<>();

            for (KnimeTuple tuple : tuples) {
                PrimaryModelWData pm;

                Model1Parser m1Parser = new Model1Parser(tuple, metadata, notes);
                SBMLDocument sbmlDoc = m1Parser.getDocument();
                String sbmlDocName = String.format("%s_%d.%s", mdName, pms.size(), modelExtension);

                if (tuple.getPmmXml(TimeSeriesSchema.ATT_TIMESERIES).size() > 0) {
                    DataParser dataParser = new DataParser(tuple, metadata, notes);
                    NuMLDocument numlDoc = dataParser.getDocument();
                    String numlDocName = String.format("%s_%d.numl", mdName, pms.size());

                    // Adds DataSourceNode to the model
                    XMLNode dsn = new DataSourceNode(numlDocName).getNode();
                    sbmlDoc.getModel().getAnnotation().getNonRDFannotation().getChildElement("metadata", "")
                            .addChild(dsn);

                    pm = new PrimaryModelWData(sbmlDocName, sbmlDoc, numlDocName, numlDoc);
                } else {
                    pm = new PrimaryModelWData(sbmlDocName, sbmlDoc, null, null);
                }
                pms.add(pm);
            }

            Path path = Paths.get(dir, mdName + (isPMFX ? ".pmfx" : ".pmf"));
            PrimaryModelWDataFile.write(path, pms);
        }
    }

    /**
     * Parse tuples from a table with primary models without data.
     */
    private static class PrimaryModelWODataParser implements Parser {

        @Override
        public void write(List<KnimeTuple> tuples, boolean isPMFX, String dir, String mdName, Metadata metadata,
                boolean splitModels, String notes, ExecutionContext exec) throws Exception {

            final String modelExtension = isPMFX ? "pmf" : "sbml";

            List<PrimaryModelWOData> pms = new LinkedList<>();
            for (KnimeTuple tuple : tuples) {
                Model1Parser m1Parser = new Model1Parser(tuple, metadata, notes);

                SBMLDocument sbmlDoc = m1Parser.getDocument();
                String sbmlDocName = String.format("%s_%d.%s", mdName, pms.size(), modelExtension);

                PrimaryModelWOData pm = new PrimaryModelWOData(sbmlDocName, sbmlDoc);
                pms.add(pm);
            }

            Path path = Paths.get(dir, mdName + (isPMFX ? ".pmfx" : ".pmf"));
            PrimaryModelWODataFile.write(path, pms);
        }
    }

    /**
     * Parse tuples from a table with primary models without data.
     */
    private static class TwoStepSecondaryModelParser implements Parser {

        @Override
        public void write(List<KnimeTuple> tuples, boolean isPMFX, String dir, String mdName, Metadata metadata,
                boolean splitModels, String notes, ExecutionContext exec) throws Exception {

            // Group tuples according to its secondary model
            Map<Integer, List<KnimeTuple>> secModelMap = new HashMap<>();
            for (KnimeTuple tuple : tuples) {
                int secModelId = ((EstModelXml) tuple.getPmmXml(Model2Schema.ATT_ESTMODEL).get(0)).id;
                if (secModelMap.containsKey(secModelId)) {
                    secModelMap.get(secModelId).add(tuple);
                } else {
                    List<KnimeTuple> tlist = new LinkedList<>();
                    tlist.add(tuple);
                    secModelMap.put(secModelId, tlist);
                }
            }

            // For the tuples of every secondary model
            List<TwoStepSecondaryModel> sms = new LinkedList<>();
            for (List<KnimeTuple> tupleList : secModelMap.values()) {
                int modelCounter = sms.size();
                sms.add(parse(tupleList, isPMFX, modelCounter, mdName, metadata, notes));
            }

            if (splitModels) {
                for (int numModel = 0; numModel < sms.size(); numModel++) {
                    String modelName = mdName + Integer.toString(numModel);
                    Path path = Paths.get(dir, modelName + (isPMFX ? ".pmfx" : ".pmf"));
                    TwoStepSecondaryModelFile.write(path, sms.subList(numModel, numModel + 1));
                }
            } else {
                Path path = Paths.get(dir, mdName + (isPMFX ? ".pmfx" : ".pmf"));
                TwoStepSecondaryModelFile.write(path, sms);
            }
        }

        private static TwoStepSecondaryModel parse(List<KnimeTuple> tuples, boolean isPMFX, int modelNum, String mdName,
                Metadata metadata, String notes) {
            /**
             * <ol>
             * <li>Create n SBMLDocument for primary models</li>
             * <li>Parse data and create n NuMLDocument</li>
             * <li>Create SBMLDocument for secondary model</li>
             * </ol>
             */
            final String modelExtension = isPMFX ? "pmf" : "sbml";

            List<PrimaryModelWData> primModels = new LinkedList<>();
            for (int i = 0; i < tuples.size(); i++) {
                KnimeTuple tuple = tuples.get(i);
                PrimaryModelWData pm;

                Model1Parser m1Parser = new Model1Parser(tuple, metadata, notes);

                SBMLDocument sbmlDoc = m1Parser.getDocument();
                String sbmlDocName = String.format("%s.%s", sbmlDoc.getModel().getId(), modelExtension);

                XMLNode metadataNode = sbmlDoc.getModel().getAnnotation().getNonRDFannotation()
                        .getChildElement("metadata", "");
                if (tuple.getPmmXml(TimeSeriesSchema.ATT_TIMESERIES).size() > 0) {
                    DataParser dataParser = new DataParser(tuple, metadata, notes);

                    NuMLDocument numlDoc = dataParser.getDocument();
                    String numlDocName = String.format("%s.numl", sbmlDoc.getModel().getId());

                    // Adds DataSourceNode to the model
                    DataSourceNode dsn = new DataSourceNode(numlDocName);
                    metadataNode.addChild(dsn.getNode());

                    pm = new PrimaryModelWData(sbmlDocName, sbmlDoc, numlDocName, numlDoc);
                } else {
                    pm = new PrimaryModelWData(sbmlDocName, sbmlDoc, null, null);
                }

                primModels.add(pm);
            }

            // We get the first tuple to query the Model2 columns which are the
            // same
            // for all the tuples of the secondary model
            KnimeTuple firstTuple = tuples.get(0);
            Model2Parser m2Parser = new Model2Parser(firstTuple, metadata, notes);

            SBMLDocument secDoc = m2Parser.getDocument();
            String secDocName = String.format("%s_%d.%s", mdName, modelNum, modelExtension);
            // Adds annotation for the primary models
            XMLNode metadataNode = secDoc.getModel().getAnnotation().getNonRDFannotation().getChildElement("metadata",
                    "");
            for (PrimaryModelWData pmwd : primModels) {
                PrimaryModelNode node = new PrimaryModelNode(pmwd.getModelDocName());
                metadataNode.addChild(node.getNode());
            }

            // Creates and return TwoStepSecondaryModel
            return new TwoStepSecondaryModel(secDocName, secDoc, primModels);
        }
    }

    /**
     * Parse tuples from a table with primary models without data.
     */
    private static class OneStepSecondaryModelParser implements Parser {

        @Override
        public void write(List<KnimeTuple> tuples, boolean isPMFX, String dir, String mdName, Metadata metadata,
                boolean splitModels, String notes, ExecutionContext exec) throws Exception {

            // Group tuples according to its secondary model
            Map<Integer, List<KnimeTuple>> secModelMap = new HashMap<>();
            for (KnimeTuple tuple : tuples) {
                int secModelId = ((EstModelXml) tuple.getPmmXml(Model2Schema.ATT_ESTMODEL).get(0)).id;
                if (secModelMap.containsKey(secModelId)) {
                    secModelMap.get(secModelId).add(tuple);
                } else {
                    List<KnimeTuple> tlist = new LinkedList<>();
                    tlist.add(tuple);
                    secModelMap.put(secModelId, tlist);
                }
            }

            // For the tuples of every secondary model
            List<OneStepSecondaryModel> sms = new LinkedList<>();
            for (List<KnimeTuple> tupleList : secModelMap.values()) {
                int modelCounter = sms.size();
                sms.add(parse(tupleList, isPMFX, mdName, modelCounter, metadata, notes));
            }

            if (splitModels) {
                for (int numModel = 0; numModel < sms.size(); numModel++) {
                    String modelName = mdName + Integer.toString(numModel);
                    Path path = Paths.get(dir, modelName + (isPMFX ? ".pmfx" : ".pmf"));
                    OneStepSecondaryModelFile.write(path, sms.subList(numModel, numModel + 1));
                }
            } else {
                Path path = Paths.get(dir, mdName + (isPMFX ? ".pmfx" : ".pmf"));
                OneStepSecondaryModelFile.write(path, sms);
            }
        }

        private static OneStepSecondaryModel parse(List<KnimeTuple> tuples, boolean isPMFX, String mdName, int modelNum,
                Metadata metadata, String notes) {

            final String modelExtension = isPMFX ? ".pmf" : ".sbml";
            KnimeTuple firstTuple = tuples.get(0);

            // Retrieve Model2Schema cells
            EstModelXml secEstModel = (EstModelXml) firstTuple.getPmmXml(Model2Schema.ATT_ESTMODEL).get(0);

            Model1Parser m1Parser = new Model1Parser(firstTuple, metadata, notes);
            SBMLDocument doc = m1Parser.getDocument();
            String docName = String.format("%s_%d.%s", mdName, modelNum, modelExtension);

            Model model = doc.getModel();
            model.setId(PMFUtil.createId("model" + secEstModel.id));
            CompSBMLDocumentPlugin compDocPlugin = (CompSBMLDocumentPlugin) doc.getPlugin(CompConstants.shortLabel);
            CompModelPlugin compModelPlugin = (CompModelPlugin) model.getPlugin(CompConstants.shortLabel);

            // Create secondary model
            Model secModel = new Model2Parser(firstTuple, metadata, notes).getDocument().getModel();
            ModelDefinition md = new ModelDefinition(secModel);
            compDocPlugin.addModelDefinition(md);

            Submodel submodel = compModelPlugin.createSubmodel("submodel");
            submodel.setModelRef(secModel.getId());

            // Parse data sets and create NuML documents
            XMLNode metadataNode = md.getAnnotation().getNonRDFannotation().getChildElement("metadata", "");
            List<NuMLDocument> numlDocs = new LinkedList<>();
            List<String> numlDocNames = new LinkedList<>();
            for (KnimeTuple tuple : tuples) {
                String numlDocName = String.format("data%d.numl", numlDocs.size());
                numlDocNames.add(numlDocName);

                DataParser dataParser = new DataParser(tuple, metadata, notes);
                NuMLDocument numlDoc = dataParser.getDocument();
                numlDocs.add(numlDoc);

                // Adds DataSourceNode to the model
                metadataNode.addChild(new DataSourceNode(numlDocName).getNode());
            }

            OneStepSecondaryModel ossm = new OneStepSecondaryModel(docName, doc, numlDocNames, numlDocs);
            return ossm;
        }
    }

    /**
     * Parse tuples from a table with primary models without data.
     */
    private static class ManualSecondaryModelParser implements Parser {

        @Override
        public void write(List<KnimeTuple> tuples, boolean isPMFX, String dir, String mdName, Metadata metadata,
                boolean splitModels, String notes, ExecutionContext exec) throws Exception {

            List<ManualSecondaryModel> sms = new LinkedList<>();
            for (KnimeTuple tuple : tuples) {
                int mdNum = sms.size();
                sms.add(parse(tuple, isPMFX, mdName, mdNum, metadata, notes));
            }

            if (splitModels) {
                for (int numModel = 0; numModel < sms.size(); numModel++) {
                    String modelName = mdName + Integer.toString(numModel);
                    Path path = Paths.get(dir, modelName + (isPMFX ? ".pmfx" : ".pmf"));
                    ManualSecondaryModelFile.write(path, sms.subList(numModel, numModel + 1));
                }
            } else {
                Path path = Paths.get(dir, mdName + (isPMFX ? ".pmfx" : ".pmf"));
                ManualSecondaryModelFile.write(path, sms);
            }
        }

        private static ManualSecondaryModel parse(KnimeTuple tuple, boolean isPMFX, String mdName, int mdNum,
                Metadata metadata, String notes) {

            final String modelExtension = isPMFX ? "pmf" : "sbml";

            // Retrieve Model2Schema cells
            CatalogModelXml catModel = (CatalogModelXml) tuple.getPmmXml(Model2Schema.ATT_MODELCATALOG).get(0);
            EstModelXml estModel = (EstModelXml) tuple.getPmmXml(Model2Schema.ATT_ESTMODEL).get(0);
            DepXml dep = (DepXml) tuple.getPmmXml(Model2Schema.ATT_DEPENDENT).get(0);
            PmmXmlDoc indepDoc = tuple.getPmmXml(Model2Schema.ATT_INDEPENDENT);
            PmmXmlDoc paramsDoc = tuple.getPmmXml(Model2Schema.ATT_PARAMETER);
            PmmXmlDoc mLitDoc = tuple.getPmmXml(Model2Schema.ATT_MLIT);
            PmmXmlDoc emLitDoc = tuple.getPmmXml(Model2Schema.ATT_EMLIT);
            int globalModelID = tuple.getInt(Model2Schema.ATT_GLOBAL_MODEL_ID);

            // Gets independent parameters
            List<IndepXml> indepXmls = new LinkedList<>();
            for (PmmXmlElementConvertable item : indepDoc.getElementSet()) {
                indepXmls.add((IndepXml) item);
            }

            // Gets constant parameters
            List<ParamXml> constXmls = new LinkedList<>();
            for (PmmXmlElementConvertable item : paramsDoc.getElementSet()) {
                constXmls.add((ParamXml) item);
            }

            String docName = String.format("%s_%d.%s", mdName, mdNum, modelExtension);
            SBMLDocument doc = new SBMLDocument(TableReader.LEVEL, TableReader.VERSION);
            // Enables Hierarchical Composition package
            doc.enablePackage(CompConstants.shortLabel);

            // Adds document annotation
            doc.setAnnotation(new MetadataAnnotation(metadata).getAnnotation());

            TableReader.addNamespaces(doc);

            // Create model definition
            String modelId = "model_" + dep.name;
            Model model = doc.createModel(modelId);
            if (estModel.name != null) {
                model.setName(estModel.name);
            }

            if (notes != null) {
                try {
                    model.setNotes(notes);
                } catch (XMLStreamException e) {
                    e.printStackTrace();
                }
            }

            try {
                TableReader.addUnitDefinitions(model, dep, indepXmls, constXmls);
            } catch (XMLStreamException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            // Adds dep
            Parameter depParam = new SecDep(dep.name, dep.description, dep.unit).getParam();
            // Adds dep constraint
            LimitsConstraint depLc = new LimitsConstraint(dep.name, dep.min, dep.max);
            if (depLc.getConstraint() != null) {
                model.addConstraint(depLc.getConstraint());
            }
            model.addParameter(depParam);

            // Add independent parameters
            for (IndepXml indepXml : indepXmls) {
                // Creates SBML parameter
                // model.addParameter(new SecIndep(indepXml).getParam());
                SecIndep secIndep = new SecIndep(indepXml.name, indepXml.description, indepXml.unit);
                model.addParameter(secIndep.getParam());
                // Adds constraint
                LimitsConstraint lc = new LimitsConstraint(indepXml.name, indepXml.min, indepXml.max);
                if (lc.getConstraint() != null) {
                    model.addConstraint(lc.getConstraint());
                }
            }

            // Adds constant parameters
            for (ParamXml paramXml : constXmls) {
                // Creates SBML parameter
                PMFCoefficient coefficient = WriterUtils.paramXml2Coefficient(paramXml);
                model.addParameter(coefficient.getParameter());

                // Adds constraint
                LimitsConstraint lc = new LimitsConstraint(paramXml.name, paramXml.min, paramXml.max);
                if (lc.getConstraint() != null) {
                    model.addConstraint(lc.getConstraint());
                }
            }

            // Gets model literature
            Reference[] mLits = new Reference[mLitDoc.size()];
            for (int i = 0; i < mLitDoc.size(); i++) {
                mLits[i] = WriterUtils.literatureItem2Reference((LiteratureItem) mLitDoc.get(i));
            }

            // Gets estimated model literature
            Reference[] emLits = new Reference[emLitDoc.size()];
            for (int i = 0; i < emLitDoc.size(); i++) {
                emLits[i] = WriterUtils.literatureItem2Reference((LiteratureItem) emLitDoc.get(i));
            }

            ModelRule rule2 = WriterUtils.createM2Rule(catModel, mLits);
            model.addRule(rule2.getRule());

            // Add annotation
            Uncertainties uncertainties = WriterUtils.estModel2Uncertainties(estModel);
            model.setAnnotation(new Model2Annotation(globalModelID, uncertainties, emLits).getAnnotation());

            return new ManualSecondaryModel(docName, doc);
        }
    }

    private static class TwoStepTertiaryModelParser implements Parser {

        @Override
        public void write(List<KnimeTuple> tuples, boolean isPMFX, String dir, String mdName, Metadata metadata,
                boolean splitModels, String notes, ExecutionContext exec) throws Exception {

            List<TwoStepTertiaryModel> tms = new LinkedList<>();

            // Sort global models
            Map<Integer, Map<Integer, List<KnimeTuple>>> gms = TableReader.sortGlobalModels(tuples);

            for (Map<Integer, List<KnimeTuple>> tertiaryInstances : gms.values()) {
                List<List<KnimeTuple>> tuplesList = new LinkedList<>();
                for (List<KnimeTuple> tertiaryInstance : tertiaryInstances.values()) {
                    tuplesList.add(tertiaryInstance);
                }
                // We have a list of tertiary instances. Each instance has the
                // same
                // microbial data yet different data. Then we'll create a
                // TwoTertiaryModel from the first instance and create the data
                // from
                // every instance.
                int modelNum = tms.size();
                TwoStepTertiaryModel tm = parse(tuplesList, isPMFX, modelNum, mdName, metadata, notes);
                tms.add(tm);
            }

            if (splitModels) {
                for (int numModel = 0; numModel < tms.size(); numModel++) {
                    String modelName = mdName + Integer.toString(numModel);
                    Path path = Paths.get(dir, modelName + (isPMFX ? ".pmfx" : ".pmf"));
                    TwoStepTertiaryModelFile.write(path, tms.subList(numModel, numModel + 1));
                }
            } else {
                Path path = Paths.get(dir, mdName + (isPMFX ? ".pmfx" : ".pmf"));
                TwoStepTertiaryModelFile.write(path, tms);
            }
        }

        private static TwoStepTertiaryModel parse(List<List<KnimeTuple>> tupleList, boolean isPMFX, int modelNum,
                String mdName, Metadata metadata, String notes) {

            final String modelExtension = isPMFX ? "pmf" : "sbml";

            List<PrimaryModelWData> primModels = new LinkedList<>();
            List<SBMLDocument> secDocs = new LinkedList<>();

            // Parse primary models and their data from every instance. Each
            // instance has an unique primary model and data set
            for (List<KnimeTuple> instance : tupleList) {
                // Get first tuple: All the tuples of an instance have the same
                // primary model
                KnimeTuple tuple = instance.get(0);
                int instanceNum = primModels.size();
                PrimaryModelWData pm;

                Model1Parser m1Parser = new Model1Parser(tuple, metadata, notes);

                SBMLDocument sbmlDoc = m1Parser.getDocument();
                String sbmlDocName = String.format("%s_%d_%d.%s", mdName, modelNum, instanceNum, modelExtension);
                XMLNode metadataNode = sbmlDoc.getModel().getAnnotation().getNonRDFannotation()
                        .getChildElement("metadata", "");

                if (tuple.getPmmXml(TimeSeriesSchema.ATT_TIMESERIES).size() > 0) {
                    NuMLDocument numlDoc = new DataParser(tuple, metadata, notes).getDocument();
                    String numlDocName = String.format("%s_%d_%d.numl", mdName, modelNum, instanceNum);

                    // Adds DataSourceNode to the model
                    metadataNode.addChild(new DataSourceNode(numlDocName).getNode());

                    primModels.add(new PrimaryModelWData(sbmlDocName, sbmlDoc, numlDocName, numlDoc));

                } else {
                    pm = new PrimaryModelWData(sbmlDocName, sbmlDoc, null, null);
                    primModels.add(pm);
                }
            }

            // Parse secondary models from the first instance (all the instance
            // have
            // the same secondary models)
            List<KnimeTuple> firstInstance = tupleList.get(0);
            for (KnimeTuple tuple : firstInstance) {
                SBMLDocument secDoc = new Model2Parser(tuple, metadata, notes).getDocument();

                // Adds annotations for the primary models
                XMLNode metadataNode = secDoc.getModel().getAnnotation().getNonRDFannotation()
                        .getChildElement("metadata", "");
                for (PrimaryModelWData pm : primModels) {
                    if (pm.getDataDocName() != null) {
                        metadataNode.addChild(new PrimaryModelNode(pm.getModelDocName()).getNode());
                    }
                }

                secDocs.add(secDoc);
            }

            // Creates tertiary model
            String tertDocName = String.format("%s_%s.%s", mdName, modelNum, modelExtension);
            SBMLDocument tertDoc = new SBMLDocument(TableReader.LEVEL, TableReader.VERSION);
            // Enable Hierarchical Compositon package
            tertDoc.enablePackage(CompConstants.shortLabel);
            CompSBMLDocumentPlugin compDocPlugin = (CompSBMLDocumentPlugin) tertDoc.getPlugin(CompConstants.shortLabel);
            TableReader.addNamespaces(tertDoc);

            // Adds document annotation
            tertDoc.setAnnotation(new MetadataAnnotation(metadata).getAnnotation());

            Model model = tertDoc.createModel("model");
            KnimeTuple aTuple = tupleList.get(0).get(0);

            // Builds metadata node
            XMLTriple metadataTriple = new XMLTriple("metadata", null, "pmf");
            XMLNode metadataNode = new XMLNode(metadataTriple);
            model.getAnnotation().setNonRDFAnnotation(metadataNode);

            // Builds global model id node
            int gmId = aTuple.getInt(Model2Schema.ATT_GLOBAL_MODEL_ID);
            metadataNode.addChild(new GlobalModelIdNode(gmId).getNode());

            // Get literature references
            PmmXmlDoc litDoc = aTuple.getPmmXml(Model1Schema.ATT_EMLIT);
            List<LiteratureItem> lits = new LinkedList<>();
            for (PmmXmlElementConvertable item : litDoc.getElementSet()) {
                lits.add((LiteratureItem) item);
            }

            // Builds reference nodes
            for (LiteratureItem lit : lits) {
                Reference ref = WriterUtils.literatureItem2Reference(lit);
                metadataNode.addChild(new ReferenceSBMLNode(ref).getNode());
            }

            // Gets a primary model
            Model primModel = primModels.get(0).getModelDoc().getModel();

            // Adds species
            Species species = primModel.getSpecies(0);
            model.addSpecies(new Species(species));

            // Adds compartment
            Compartment compartment = primModel.getCompartment(0);
            model.addCompartment(new Compartment(compartment));

            // Adds rule
            AssignmentRule rule = (AssignmentRule) primModel.getRule(0);
            model.addRule(new AssignmentRule(rule));

            // Assigns parameters of the primary model
            for (Parameter p : primModel.getListOfParameters()) {
                Parameter p2 = new Parameter(p);
                if (p2.isSetAnnotation()) {
                    p2.setAnnotation(new Annotation());
                }
                model.addParameter(p2);
            }

            CompModelPlugin modelPlugin = (CompModelPlugin) model.getPlugin(CompConstants.shortLabel);

            // Creates ExternalModelDefinition
            List<String> secDocNames = new LinkedList<>();
            for (SBMLDocument secDoc : secDocs) {
                // Gets model definition id from secDoc
                String mdId = secDoc.getModel().getId();

                String secDocName = String.format("%s.%s", secDoc.getModel().getId(), modelExtension);
                secDocNames.add(secDocName);

                // Creates and adds an ExternalModelDefinition to the tertiary
                // model
                ExternalModelDefinition emd = compDocPlugin.createExternalModelDefinition(mdId);
                emd.setSource(secDocName);
                emd.setModelRef(mdId);

                String depId = ((AssignmentRule) secDoc.getModel().getRule(0)).getVariable();

                // Creates submodel
                Submodel submodel = modelPlugin.createSubmodel("submodel_" + depId);
                submodel.setModelRef(mdId);

                Parameter parameter = model.getParameter(depId);

                CompSBasePlugin plugin = (CompSBasePlugin) parameter.getPlugin(CompConstants.shortLabel);
                ReplacedBy replacedBy = plugin.createReplacedBy();
                replacedBy.setIdRef(depId);
                replacedBy.setSubmodelRef(submodel.getId());
            }

            // Assigns unit definitions of the primary model
            model.setListOfUnitDefinitions(new ListOf<UnitDefinition>(primModel.getListOfUnitDefinitions()));

            TwoStepTertiaryModel tstm = new TwoStepTertiaryModel(tertDocName, tertDoc, primModels, secDocNames,
                    secDocs);
            return tstm;
        }
    }

    /**
     * One Step Fit Tertiary Model
     * 
     * @author Miguel Alba
     */
    private static class OneStepTertiaryModelParser implements Parser {

        @Override
        public void write(List<KnimeTuple> tuples, boolean isPMFX, String dir, String mdName, Metadata metadata,
                boolean splitModels, String notes, ExecutionContext exec) throws Exception {

            List<OneStepTertiaryModel> tms = new LinkedList<>();

            // Sort global models
            Map<Integer, Map<Integer, List<KnimeTuple>>> gms = TableReader.sortGlobalModels(tuples);

            // Parse tertiary models
            for (Map<Integer, List<KnimeTuple>> tertiaryInstances : gms.values()) {
//                List<List<KnimeTuple>> tuplesList = new LinkedList<>();
//                for (List<KnimeTuple> tertiaryInstance : tertiaryInstances.values()) {
//                    tuplesList.add(tertiaryInstance);
//                }
                List<List<KnimeTuple>> tuplesList = new LinkedList<>(tertiaryInstances.values());
                /**
                 * We have a list of tertiary instances. Each instance has the
                 * same microbial data yet different data. Then we'll create a
                 * TwoTertiaryModel from the first instance and create the data
                 * from every instance.
                 */
                int mdNum = tms.size();
                OneStepTertiaryModel tm = parse(tuplesList, isPMFX, mdName, mdNum, metadata, notes);
                tms.add(tm);
            }

            if (splitModels) {
                for (int numModel = 0; numModel < tms.size(); numModel++) {
                    String modelName = mdName + Integer.toString(numModel);
                    Path path = Paths.get(dir, modelName + (isPMFX ? ".pmfx" : ".pmf"));
                    OneStepTertiaryModelFile.write(path, tms.subList(numModel, numModel + 1));
                }
            } else {
                Path path = Paths.get(dir, mdName + (isPMFX ? ".pmfx" : ".pmf"));
                OneStepTertiaryModelFile.write(path, tms);
            }
        }

        private static OneStepTertiaryModel parse(List<List<KnimeTuple>> tupleList, boolean isPMFX, String mdName,
                int mdNum, Metadata metadata, String notes) {

            final String modelExtension = isPMFX ? "pmf" : "sbml";

            List<String> numlDocNames = new LinkedList<>();
            List<NuMLDocument> numlDocs = new LinkedList<>();
            for (List<KnimeTuple> instance : tupleList) {
                // Get first tuple: All the tuples of an instance have the same
                // data
                KnimeTuple tuple = instance.get(0);
                if (tuple.getPmmXml(TimeSeriesSchema.ATT_TIMESERIES).size() > 0) {
                    int dataCounter = numlDocs.size();
                    String numlDocName = String.format("data_%d_%d.numl", mdNum, dataCounter);
                    numlDocNames.add(numlDocName);

                    DataParser dataParser = new DataParser(tuple, metadata, notes);
                    NuMLDocument numlDoc = dataParser.getDocument();
                    numlDocs.add(numlDoc);
                }
            }

            // We'll get microbial data from the first instance
            List<KnimeTuple> firstInstance = tupleList.get(0);
            // and the primary model from the first tuple
            KnimeTuple firstTuple = firstInstance.get(0);

            Model1Parser m1Parser = new Model1Parser(firstTuple, metadata, notes);
            SBMLDocument tertDoc = m1Parser.getDocument();
            String tertDocName = String.format("%s_%s.%s", mdName, mdNum, modelExtension);
            CompSBMLDocumentPlugin compDocPlugin = (CompSBMLDocumentPlugin) tertDoc.getPlugin(CompConstants.shortLabel);

            // Adds DataSourceNode to the tertiary model
            XMLNode tertMetadataNode = tertDoc.getModel().getAnnotation().getNonRDFannotation()
                    .getChildElement("metadata", "");
            for (String numlDocName : numlDocNames) {
                tertMetadataNode.addChild(new DataSourceNode(numlDocName).getNode());
            }

            CompModelPlugin modelPlugin = (CompModelPlugin) tertDoc.getModel().getPlugin(CompConstants.shortLabel);

            // Add submodels and model definitions
            List<String> secDocNames = new LinkedList<>();
            List<SBMLDocument> secDocs = new LinkedList<>();
            for (KnimeTuple tuple : firstInstance) {

                SBMLDocument secDoc = new Model2Parser(tuple, metadata, notes).getDocument();

                String secModelId = secDoc.getModel().getId();
                String secDocName = String.format("%s.%s", secModelId, modelExtension);

                secDocNames.add(secDocName);
                secDocs.add(secDoc);

                // Creates and adds an ExternalModelDefinition
                ExternalModelDefinition emd = compDocPlugin.createExternalModelDefinition(secModelId);
                emd.setSource(secDocName);
                emd.setModelRef(secModelId);

                String depId = ((AssignmentRule) secDoc.getModel().getRule(0)).getVariable();

                Submodel submodel = modelPlugin.createSubmodel("submodel_" + depId);
                submodel.setModelRef(emd.getId());

                Parameter parameter = tertDoc.getModel().getParameter(depId);

                CompSBasePlugin plugin = (CompSBasePlugin) parameter.getPlugin(CompConstants.shortLabel);
                ReplacedBy replacedBy = plugin.createReplacedBy();
                replacedBy.setIdRef(depId);
                replacedBy.setSubmodelRef(submodel.getId());

                // Add annotation for the primary model
                XMLNode secMetadataNode = secDoc.getModel().getAnnotation().getNonRDFannotation()
                        .getChildElement("metadata", "");
                secMetadataNode.addChild(new PrimaryModelNode(tertDocName).getNode());

                // Adds DataSourceNodes to the sec model
                for (String numlDocName : numlDocNames) {
                    secMetadataNode.addChild(new DataSourceNode(numlDocName).getNode());
                }
            }

            OneStepTertiaryModel tstm = new OneStepTertiaryModel(tertDocName, tertDoc, secDocNames, secDocs,
                    numlDocNames, numlDocs);
            return tstm;
        }
    }

    private static class ManualTertiaryModelParser implements Parser {

        @Override
        public void write(List<KnimeTuple> tuples, boolean isPMFX, String dir, String mdName, Metadata metadata,
                boolean splitModels, String notes, ExecutionContext exec) throws Exception {

            List<ManualTertiaryModel> tms = new LinkedList<>();

            // Sort tertiary models
            Map<Integer, List<KnimeTuple>> tertiaryModelMap = new HashMap<>();
            for (KnimeTuple tuple : tuples) {
                int primModelId = ((EstModelXml) tuple.getPmmXml(Model1Schema.ATT_ESTMODEL).get(0)).id;

                // primary model in tuple is already in tertiaryModelMap
                if (tertiaryModelMap.containsKey(primModelId)) {
                    tertiaryModelMap.get(primModelId).add(tuple);
                } else {
                    LinkedList<KnimeTuple> tupleList = new LinkedList<>();
                    tupleList.add(tuple);
                    tertiaryModelMap.put(primModelId, tupleList);
                }
            }

            for (List<KnimeTuple> tupleList : tertiaryModelMap.values()) {
                int modelCounter = tms.size();
                tms.add(parse(tupleList, isPMFX, mdName, modelCounter, metadata, notes));
            }

            if (splitModels) {
                for (int numModel = 0; numModel < tms.size(); numModel++) {
                    String modelName = mdName + Integer.toString(numModel);
                    Path path = Paths.get(dir, modelName + (isPMFX ? ".pmfx" : ".pmf"));
                    ManualTertiaryModelFile.write(path, tms.subList(numModel, numModel + 1));
                }
            } else {
                Path path = Paths.get(dir, mdName + (isPMFX ? ".pmfx" : ".pmf"));
                ManualTertiaryModelFile.write(path, tms);
            }
        }

        private static ManualTertiaryModel parse(List<KnimeTuple> tupleList, boolean isPMFX, String mdName,
                int modelNum, Metadata metadata, String notes) {

            final String modelExtension = isPMFX ? "pmf" : "sbml";

            // We'll get microbial data from the primary of the first tuple
            KnimeTuple firstTuple = tupleList.get(0);

            // Creates SBMLDocument for the tertiary model
            Model1Parser m1Parser = new Model1Parser(firstTuple, metadata, notes);
            SBMLDocument tertDoc = m1Parser.getDocument();
            String tertDocName = String.format("%s_%s.%s", mdName, modelNum, modelExtension);

            CompSBMLDocumentPlugin compDocPlugin = (CompSBMLDocumentPlugin) tertDoc.getPlugin(CompConstants.shortLabel);
            CompModelPlugin compModelPlugin = (CompModelPlugin) tertDoc.getModel().getPlugin(CompConstants.shortLabel);

            // Add submodels and model definitions
            List<String> secDocNames = new LinkedList<>();
            List<SBMLDocument> secDocs = new LinkedList<>();

            for (KnimeTuple tuple : tupleList) {

                Model2Parser m2Parser = new Model2Parser(tuple, metadata, notes);
                SBMLDocument secDoc = m2Parser.getDocument();

                String emdId = secDoc.getModel().getId();
                String secDocName = String.format("%s_%s_%s.%s", mdName, modelNum, emdId, modelExtension);

                secDocNames.add(secDocName);
                secDocs.add(secDoc);

                // Creates ExternalModelDefinition
                ExternalModelDefinition emd = new ExternalModelDefinition(emdId, TableReader.LEVEL,
                        TableReader.VERSION);
                emd.setSource(secDocName);
                emd.setModelRef(emdId);

                compDocPlugin.addExternalModelDefinition(emd);

                Submodel submodel = compModelPlugin.createSubmodel(emdId);
                submodel.setModelRef(emdId);

                String depId = ((AssignmentRule) secDoc.getModel().getRule(0)).getVariable();
                Parameter parameter = tertDoc.getModel().getParameter(depId);

                CompSBasePlugin plugin = (CompSBasePlugin) parameter.getPlugin(CompConstants.shortLabel);
                ReplacedBy replacedBy = plugin.createReplacedBy();
                replacedBy.setIdRef(depId);
                replacedBy.setSubmodelRef(emdId);
            }

            ManualTertiaryModel mtm = new ManualTertiaryModel(tertDocName, tertDoc, secDocNames, secDocs);
            return mtm;
        }
    }

}