diff --git a/core/src/org/sbml/jsbml/validator/OverdeterminationValidator.java b/core/src/org/sbml/jsbml/validator/OverdeterminationValidator.java index 833842af2..8938a5874 100644 --- a/core/src/org/sbml/jsbml/validator/OverdeterminationValidator.java +++ b/core/src/org/sbml/jsbml/validator/OverdeterminationValidator.java @@ -813,6 +813,46 @@ public Map getMatching() { return matching; } + /** + * Resolve an identifier from a MathML {@link ASTNode} to the + * corresponding {@link SBase} object in the model. + * + * This is in particular needed for SBML Level 3 Version 2 + * {@code rateOf} csymbols, where the {@link ASTNode#getVariable()} + * may not yet be initialized. + * + * @param id the SBML id of the object + * @return the corresponding {@link SBase}, or {@code null} if none + * can be found. + */ + private SBase resolveSBaseFromId(String id) { + if (model == null || id == null) { + return null; + } + + Species species = model.getSpecies(id); + if (species != null) { + return species; + } + + Compartment compartment = model.getCompartment(id); + if (compartment != null) { + return compartment; + } + + Parameter parameter = model.getParameter(id); + if (parameter != null) { + return parameter; + } + + Reaction reaction = model.getReaction(id); + if (reaction != null) { + return reaction; + } + + return null; + } + /** * Returns the variables in a MathML object without local parameter * @@ -824,46 +864,59 @@ public Map getMatching() { private void getVariables(ListOf param, ASTNode node, List variables, int level) { - if (node == null) - { + if (node == null) { return; } - // found node with species - if ((node.getChildCount() == 0) && (node.isString()) && - (node.getType() != Type.NAME_TIME) && - (node.getType() != Type.NAME_AVOGADRO)) { // TODO - deal with csymbol rateOf as well ? + // found node with species / compartment / parameter / reaction id + if ((node.getChildCount() == 0) && node.isString() + && (node.getType() != Type.NAME_TIME) + && (node.getType() != Type.NAME_AVOGADRO)) { + if (!node.isConstant()) { - if (param == null) { - SBase variable=node.getVariable(); - if (level==1) { - int insertingPosition = 0; - for (SBase element:variables) { - if (!(element instanceof Parameter) || (!((Parameter)element).isSetValue())) { - insertingPosition++; - } - } - variables.add(insertingPosition, variable); - } - else { - variables.add(variable); + + // Try to get the referenced SBase. For identifiers used in + // csymbol rateOf (L3V2), getVariable() may not yet be set, + // so we fall back to resolving the id in the model. + SBase variable = node.getVariable(); + if (variable == null) { + String id = node.getName(); + if (id != null) { + variable = resolveSBaseFromId(id); } - } else { - if (!param.contains(node.getVariable())) { - SBase variable=node.getVariable(); - if (level==1) { - int insertingPosition=0; - for (SBase element:variables) { - if (!(element instanceof Parameter) || - (!((Parameter) element).isSetValue())) { + } + + if (variable != null) { + if (param == null) { + // Global identifier + if (level == 1) { + int insertingPosition = 0; + for (SBase element : variables) { + if (!(element instanceof Parameter) + || (!((Parameter) element).isSetValue())) { insertingPosition++; } } variables.add(insertingPosition, variable); - } - else { + } else { variables.add(variable); } + } else { + // Exclude local parameters + if (!param.contains(variable)) { + if (level == 1) { + int insertingPosition = 0; + for (SBase element : variables) { + if (!(element instanceof Parameter) + || (!((Parameter) element).isSetValue())) { + insertingPosition++; + } + } + variables.add(insertingPosition, variable); + } else { + variables.add(variable); + } + } } } } @@ -933,4 +986,4 @@ private void updateMatching(List> path) { } } -} +} \ No newline at end of file diff --git a/core/test/org/sbml/jsbml/validator/OverdeterminationValidatorTest.java b/core/test/org/sbml/jsbml/validator/OverdeterminationValidatorTest.java new file mode 100644 index 000000000..bacd0d24b --- /dev/null +++ b/core/test/org/sbml/jsbml/validator/OverdeterminationValidatorTest.java @@ -0,0 +1,62 @@ +package org.sbml.jsbml.validator; + +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.sbml.jsbml.ASTNode; +import org.sbml.jsbml.Model; +import org.sbml.jsbml.Reaction; +import org.sbml.jsbml.SBase; + +/** + * Tests for {@link OverdeterminationValidator}. + */ +public class OverdeterminationValidatorTest { + + /** + * In SBML Level 3 Version 2, the csymbol {@code rateOf} can target + * a reaction identifier. The {@link OverdeterminationValidator} + * must treat that identifier as a referenced variable in the MathML + * expression. + * + * This test checks that when an {@link ASTNode} refers to a reaction + * by its id, and {@code getVariable()} is not set, the validator still + * resolves the identifier via the model and collects the reaction as + * a variable. + */ + @Test + public void testGetVariablesResolvesReactionIdWhenVariableNotSet() throws Exception { + // Create minimal L3V2 model with one reaction + Model model = new Model(3, 2); + model.setId("m"); + Reaction r1 = model.createReaction(); + r1.setId("R1"); + + // Create an AST node that refers to the reaction by its id. + // In some L3V2 rateOf constructs, getVariable() may not be set + // on such nodes; the validator must then resolve the id via the model. + ASTNode nameNode = new ASTNode("R1"); + + OverdeterminationValidator validator = new OverdeterminationValidator(model); + + // Prepare list to receive variables + List vars = new ArrayList(); + + // Call the private getVariables(..) method via reflection + Method m = OverdeterminationValidator.class.getDeclaredMethod( + "getVariables", + org.sbml.jsbml.ListOf.class, + ASTNode.class, + List.class, + int.class); + m.setAccessible(true); + m.invoke(validator, null, nameNode, vars, model.getLevel()); + + assertTrue("The reaction referenced by id must be collected as a variable", + vars.contains(r1)); + } +} \ No newline at end of file diff --git a/extensions/comp/src/org/sbml/jsbml/ext/comp/util/CompFlatteningConverter.java b/extensions/comp/src/org/sbml/jsbml/ext/comp/util/CompFlatteningConverter.java index 95321d7c0..81dafc319 100644 --- a/extensions/comp/src/org/sbml/jsbml/ext/comp/util/CompFlatteningConverter.java +++ b/extensions/comp/src/org/sbml/jsbml/ext/comp/util/CompFlatteningConverter.java @@ -3,7 +3,7 @@ * This file is part of JSBML. Please visit * for the latest version of JSBML and more information about SBML. * - * Copyright (C) 2009-2022 jointly by the following organizations: + * Copyright (C) 2009-2018 jointly by the following organizations: * 1. The University of Tuebingen, Germany * 2. EMBL European Bioinformatics Institute (EBML-EBI), Hinxton, UK * 3. The California Institute of Technology, Pasadena, CA, USA @@ -19,39 +19,12 @@ */ package org.sbml.jsbml.ext.comp.util; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Logger; +import org.sbml.jsbml.*; +import org.sbml.jsbml.ext.comp.*; +import org.sbml.jsbml.util.Pair; -import javax.xml.stream.XMLStreamException; - -import org.sbml.jsbml.Compartment; -import org.sbml.jsbml.Constraint; -import org.sbml.jsbml.Event; -import org.sbml.jsbml.FunctionDefinition; -import org.sbml.jsbml.InitialAssignment; -import org.sbml.jsbml.ListOf; -import org.sbml.jsbml.Model; -import org.sbml.jsbml.Parameter; -import org.sbml.jsbml.Reaction; -import org.sbml.jsbml.Rule; -import org.sbml.jsbml.SBMLDocument; -import org.sbml.jsbml.SBMLReader; -import org.sbml.jsbml.SBase; -import org.sbml.jsbml.Species; -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.Deletion; -import org.sbml.jsbml.ext.comp.ExternalModelDefinition; -import org.sbml.jsbml.ext.comp.ModelDefinition; -import org.sbml.jsbml.ext.comp.Port; -import org.sbml.jsbml.ext.comp.ReplacedElement; -import org.sbml.jsbml.ext.comp.Submodel; +import java.util.*; +import java.util.logging.Logger; /** * The {@link CompFlatteningConverter} object translates a hierarchical model defined with the SBML Level 3 @@ -59,32 +32,62 @@ * structure is dissolved and all objects are built into a single model that does no longer require the comp package. * * @author Christoph Blessing + * @author Eike Pertuch * @since 1.0 */ public class CompFlatteningConverter { private final static Logger LOGGER = Logger.getLogger(CompFlatteningConverter.class.getName()); - private List previousModelIDs; - private List previousModelMetaIDs; private ListOf modelDefinitionListOf; - private ListOf externalModelDefinitionListOf; + + // HashMap containing prefix for each submodel + private HashMap, String> subModelPrefixes; private List listOfSubmodelsToFlatten; + // Hashmap of Replaced Elements in the form pathToModel -> idType (id, metaID, ...) -> replacedId -> replacedElementInfo + private HashMap, HashMap>> replacedAndDeletedElementsHashMap; + + // Contains newly created (generally copied) initial assignments + private HashMap, List> newInitAssignHashMap; + + // Contains newly created (generally copied) rules + private HashMap, List> newRulesHashMap; + + // Contains replaced units (their references can only be changed at the end) + private HashMap newUnitDefMaps; + + // Final flattened model that is merged into bottom-up in the submodel tree private Model flattenedModel; + // Path to currently visited submodel (node) + private List curPath; + + // Previously visited models + private HashMap visitedModels; + public CompFlatteningConverter() { this.listOfSubmodelsToFlatten = new ArrayList<>(); - this.previousModelIDs = new ArrayList<>(); - this.previousModelMetaIDs = new ArrayList<>(); - this.modelDefinitionListOf = new ListOf<>(); - this.externalModelDefinitionListOf = new ListOf<>(); + + this.replacedAndDeletedElementsHashMap = new HashMap<>(); + + this.newInitAssignHashMap = new HashMap<>(); + + this.newRulesHashMap = new HashMap<>(); + + this.newUnitDefMaps = new HashMap<>(); this.flattenedModel = new Model(); + this.subModelPrefixes = new HashMap, String>(); + + this.curPath = new ArrayList<>(); + + this.visitedModels = new HashMap<>(); + } @@ -103,211 +106,1187 @@ public SBMLDocument flatten(SBMLDocument document) { CompSBMLDocumentPlugin compSBMLDocumentPlugin = (CompSBMLDocumentPlugin) document.getExtension(CompConstants.shortLabel); this.modelDefinitionListOf = compSBMLDocumentPlugin.getListOfModelDefinitions(); - this.externalModelDefinitionListOf = compSBMLDocumentPlugin.getListOfExternalModelDefinitions(); if (document.isSetModel() && document.getModel().getExtension(CompConstants.shortLabel) != null) { - // TODO: the model itself has to be flattened (can hold a list of replacements etc.) - CompModelPlugin compModelPlugin = (CompModelPlugin) document.getModel().getExtension(CompConstants.shortLabel); - handlePorts(compModelPlugin, compModelPlugin.getListOfPorts()); - replaceElementsInModelDefinition(compModelPlugin, null); //TODO: why is null given here? + //this.flattenedModel = document.getModel(); + this.flattenedModel.setLevel(document.getLevel()); + this.flattenedModel.setVersion(document.getVersion()); + // Perform flattening of model + performFlattening(document.getModel(), null, new ArrayList(), new ArrayList()); + // Set model units of flattened model + setModelUnits(document.getModel()); + // replace Units in flattened model that were replaced + replaceUnits(); - this.flattenedModel = instantiateSubModels(compModelPlugin); } else { - LOGGER.warning("No comp package found in Model. Can not flatten."); + LOGGER.warning("No comp package found in Model. Cannot flatten."); } } else { LOGGER.warning("No comp package found in Document. Cannot flatten."); } + // Remove lists that are now empty from final model + for(ListOf modelList: getAllListsOfModel(this.flattenedModel)) { + if(modelList.isEmpty()) { + modelList.removeFromParent(); + } + } + this.flattenedModel.unsetExtension(CompConstants.shortLabel); this.flattenedModel.unsetPlugin(CompConstants.shortLabel); + this.flattenedModel.setId(document.getModel().getId()); + this.flattenedModel.setName(document.getModel().getName()); + this.flattenedModel.unsetExtension(CompConstants.shortLabel); - document.setModel(this.flattenedModel); document.unsetExtension(CompConstants.shortLabel); document.disablePackage(CompConstants.shortLabel); + document.setModel(this.flattenedModel); return document; - } - /** - * Initiates every Submodel in the CompModelPlugin recursively + * Method that performs the flattening of the model + * Recursively calls itself to flatten models in submodel tree + * depth-first postorder. * - * @param compModelPlugin + * @param model currentModel (to be flattened) + * @param subModelID Id of submodel (as defined in submodel list of parent model) + * @param timeConvFactors Time Conversion Factors applied to current and all parent models + * @param extentConvFactors Extent Conversion Factors applied to current and all parent models + * @return */ - private Model instantiateSubModels(CompModelPlugin compModelPlugin) { + private void performFlattening(Model model, String subModelID, + List timeConvFactors, List extentConvFactors) { + - // TODO: the first model is not always flat. + // Model needs to be cloned to be accessible after recursion call + Model modelCopy = model.clone(); - Model model = compModelPlugin.getExtendedSBase().getModel(); + this.visitedModels.put(modelCopy.getId(), modelCopy.clone()); - handlePorts(compModelPlugin, compModelPlugin.getListOfPorts()); - replaceElementsInModelDefinition(compModelPlugin, null); - this.flattenedModel = mergeModels(flattenModel(model), this.flattenedModel); + // Initialize time and extent conversion factors list to save current factors for + List curTimeConvFactors = new ArrayList<>(timeConvFactors); + List curExtentConvFactors = new ArrayList<>(extentConvFactors); + + this.curPath.add(subModelID == null ? (model.getId().isEmpty()? "mainModel": model.getId()) : subModelID); + CompModelPlugin compModelPlugin = (CompModelPlugin) modelCopy.getExtension(CompConstants.shortLabel); + + // STEP 1: If current model contains submodels, flatten those submodels first + // Also collect all replaced elements in submodels of current model in this.replacedElementsHashMap + parseAllListsOfReplacedElements(modelCopy, curPath); + + String subModelPrefix; + // Perform steps 2-6 only for submodels (not for the main model) + // STEP 2: Find prefix unique prefix containing Submodel_id + if (subModelID != null) { + + CompFlatteningIDExpanderTS idExpander = new CompFlatteningIDExpanderTS(); + subModelPrefix = idExpander.expandID(modelCopy, this.curPath); - if (compModelPlugin.getSubmodelCount() > 0) { - // check if submodel has submodel - this.flattenedModel = initSubModels(compModelPlugin); } else { - LOGGER.info("No more submodels"); + subModelPrefix = ""; + } + this.subModelPrefixes.put(new ArrayList(this.curPath), subModelPrefix); + + // If current model contains compModelPlugin and has submodels -> go into submodels + if (compModelPlugin != null && compModelPlugin.getSubmodelCount() > 0) { + for (Submodel submodel : compModelPlugin.getListOfSubmodels()) { + // ParseListOfDeletions in current submodel and add to replacedAndDeletedElementsHashMap + parseListOfDeletions(submodel.getListOfDeletions(), curPath, submodel.getId()); + Model submodelModel = this.modelDefinitionListOf.get(submodel.getModelRef()).getModel().clone(); + + // Add time and extent conversion factors of model to list of active conversion factors + if (submodel.isSetTimeConversionFactor()) { + String timeConvFactorName = submodel.getTimeConversionFactor(); + if (timeConvFactorName != null) { + String timeConvFactor = model.getParameter(timeConvFactorName).getId(); + timeConvFactors.add(subModelPrefix + timeConvFactor); + } + } + if (submodel.isSetExtentConversionFactor()) { + String extentConvFactorName = submodel.getExtentConversionFactor(); + if (extentConvFactorName != null) { + String extentConvFactor = model.getParameter(extentConvFactorName).getId(); + extentConvFactors.add(subModelPrefix + extentConvFactor); + } + } + + // Recursively call flattening for each submodel + performFlattening(submodelModel, submodel.getId(), timeConvFactors, extentConvFactors); + } } - return this.flattenedModel; + // Now parse ReplacedBy Elements and add to replacedAndDeletedElementsHashMap + parseAllReplacedBy(modelCopy, this.curPath); + + // STEP 3: Remove all objects which were either deleted or replaced + HashMap replacedElements = removeReplacedAndDeletedElements(modelCopy, compModelPlugin); + + // STEP 4: Add newly created InitialAssignments and Rules + addNewElementsWithVariables(modelCopy); + + // STEP 5: Add prefix to all objects + addPrefixesToModelObjects(modelCopy, subModelPrefix); + + // STEP 6: Add prefix to all references and adjust them according to replacements + ASTNode timeConvFactorNode = createNewConvNode(modelCopy, curTimeConvFactors); + ASTNode extentConvFactorNode = createNewConvNode(modelCopy, curExtentConvFactors); + addPrefixesAndReplacementsToModelReferences(modelCopy, subModelPrefix, replacedElements, timeConvFactorNode, + extentConvFactorNode); + + // STEP 7: Merge model into flattened model + this.flattenedModel = mergeModels(this.flattenedModel, modelCopy); + + // Submodel flattenening finished -> go up in submodel tree; + this.curPath.remove(this.curPath.size() - 1); } + + ////////////////////////////////////////// + /// Replacements/Deletions parsers ////// + //////////////////////////////////////// + /** - * Actualizes replacement provided by comp extension: The "replaced elements" referenced by {@link ReplacedElement} - * instances are here actually removed, along with their respective {@link Port}, and thus replaced by the holder - * of the {@link ReplacedElement} - * @param compModelPlugin plugin holding the {@link ReplacedElement}s, may be null -- not used in that case - * @param compSBasePlugin plugin holding {@link ReplacedElement}s, may be null - not used in that case + * + * Parse deletions in current model + * + * @param deletions + * @param curPath + * @param submodelID */ - private void replaceElementsInModelDefinition(CompModelPlugin compModelPlugin, CompSBasePlugin compSBasePlugin) { - - if (compModelPlugin != null || compSBasePlugin != null) { + private void parseListOfDeletions(List deletions, List curPath, String submodelID) { - ListOf listOfReplacedElements = new ListOf<>(); + HashMap replacedIDElementsInModel = null; + if(this.replacedAndDeletedElementsHashMap.containsKey(curPath)) { + replacedIDElementsInModel = this.replacedAndDeletedElementsHashMap.get(curPath).get(IdType.ID); + } - if (compModelPlugin != null) { - listOfReplacedElements = compModelPlugin.getListOfReplacedElements(); - } else if (compSBasePlugin != null) { - listOfReplacedElements = compSBasePlugin.getListOfReplacedElements(); + for (Deletion deletion : deletions) { + // If deleted Element was added previously -> skip + if(replacedIDElementsInModel != null && replacedIDElementsInModel.containsKey(deletion.getId())) { + continue; } + List deletionPath = new ArrayList<>(curPath); + deletionPath.add(submodelID); + IdType deletedElementType; + String deletedElementRef; + Pair returnValue; + // If deletion is nested (sBaseRef set) get final sBaseRef and extend deletionPath accordingly + if (deletion.isSetSBaseRef()) { + deletionPath.add(deletion.getIdRef()); + SBaseRef finalSBaseRef = determineSubmodelPath(deletion.getSBaseRef(), deletionPath); + returnValue = selectCorrectRef(finalSBaseRef); + } else { + returnValue = selectCorrectRef(deletion); + } + deletedElementRef = returnValue.getKey(); + deletedElementType = returnValue.getValue(); + + // Add deleted element to replacedAndDeletedElementsHashMap + if (deletedElementRef != null) { + HashMap> deletedElementsForModel = this.replacedAndDeletedElementsHashMap.get(deletionPath); + if (deletedElementsForModel == null) { + HashMap innerHashMap = new HashMap<>(); + innerHashMap.put(deletedElementRef, null); + HashMap> idHashMap = new HashMap<>(); + idHashMap.put(deletedElementType, innerHashMap); + this.replacedAndDeletedElementsHashMap.put(deletionPath, idHashMap); + } else { + HashMap idHashMap = deletedElementsForModel.get(deletedElementType); + if (idHashMap != null) { + idHashMap.put(deletedElementRef, null); + } else { + HashMap deletedIDs = new HashMap<>(); + deletedIDs.put(deletedElementRef, null); + deletedElementsForModel.put(deletedElementType, deletedIDs); + } + } + } + } + } - for (ReplacedElement replacedElement : listOfReplacedElements) { - - for (ModelDefinition modelDefinition : this.modelDefinitionListOf) { - SBase sBase = modelDefinition.findNamedSBase(replacedElement.getIdRef()); - if (sBase != null) { - sBase.removeFromParent(); + /** + * + * Parses all lists of replacedElements in model and add them to replacedAndDeletedElementsHashMap + * + * @param model + * @param curPath + */ + private void parseAllListsOfReplacedElements(Model model, List curPath) { + + List> listOfListsOfSBases = getListOfListsOfSBases(model); + + // Iterate over every SBase list in model + for (List listOfSBases : listOfListsOfSBases) { + for (AbstractSBase sBase : listOfSBases) { + String sBaseID = sBase.getId(); + CompSBasePlugin curCompSBasePlugin = (CompSBasePlugin) sBase.getExtension(CompConstants.shortLabel); + + // Parse list of elements that are replaced by this element + if (curCompSBasePlugin != null) { + ListOf listOfReplacedElements = curCompSBasePlugin.getListOfReplacedElements(); + for (ReplacedElement replacedElement : listOfReplacedElements) { + List submodelPath = new ArrayList<>(curPath); + + ReplacedElementInfo replacedElementInfo = new ReplacedElementInfo( + sBaseID, + model.getId(), + IdType.ID, + replacedElement.isSetConversionFactor()? replacedElement.getConversionFactor(): null, + new ArrayList(submodelPath) + ); + + String replacedElementRef = null; + IdType replacedElementType = null; + submodelPath.add(replacedElement.getSubmodelRef()); + // If replacedElement is nested (sBaseRef set) get final sBaseRef and extend submodelPath accordingly + if (replacedElement.getSBaseRef() != null) { + submodelPath.add(replacedElement.getIdRef()); + // Get last sBaseRef in nested sBaseRef and extend current submodelPath accordingly + SBaseRef finalSBaseRef = determineSubmodelPath(replacedElement.getSBaseRef(), submodelPath); + Pair returnValue = selectCorrectRef(finalSBaseRef); + replacedElementRef = returnValue.getKey(); + replacedElementType = returnValue.getValue(); + } + else { + Pair returnValue = selectCorrectRef(replacedElement); + replacedElementRef = returnValue.getKey(); + replacedElementType = returnValue.getValue(); + } + // If replaceElement is not null add to replacedAndDeletedElementsHashMap + if (replacedElementRef != null) { + HashMap> hashMapOfModel = this.replacedAndDeletedElementsHashMap.get(submodelPath); + if (hashMapOfModel == null) { + HashMap> typeHashMap = new HashMap>(); + HashMap replacementHashMap = new HashMap(); + replacementHashMap.put(replacedElementRef, replacedElementInfo); + typeHashMap.put(replacedElementType, replacementHashMap); + this.replacedAndDeletedElementsHashMap.put( + submodelPath, + typeHashMap + ); + } else { + HashMap hashMapOfRefType = hashMapOfModel.get(replacedElementType); + if (hashMapOfRefType == null) { + HashMap replacementHashMap = new HashMap(); + replacementHashMap.put(replacedElementRef, replacedElementInfo); + hashMapOfModel.put(replacedElementType, replacementHashMap); + } else { + hashMapOfRefType.put(replacedElementRef, replacedElementInfo); + } + } + } + } + if(!curCompSBasePlugin.isSetReplacedBy()) { + sBase.unsetExtension(CompConstants.shortLabel); + sBase.disablePackage(CompConstants.shortLabel); } } + } + } + } - if (compModelPlugin != null) { - for(Port port : compModelPlugin.getListOfPorts()){ - if(port.getId().equals(replacedElement.getPortRef())){ - port.removeFromParent(); + /** + * + * Parse all replacedBy elements and add them to replacedAndDeletedElementsHashMap + * + * @param model + * @param submodelPath + */ + private void parseAllReplacedBy(Model model, List submodelPath) { + + List submodelPathOg = new ArrayList<>(submodelPath); + List submodelPathCopy = new ArrayList<>(submodelPath); + + List> listOfListsOfSBases = getListOfListsOfSBases(model); + + for (List listOfSBases : listOfListsOfSBases) { + for (AbstractSBase sBase : listOfSBases) { + submodelPathCopy = new ArrayList<>(submodelPath); + String sBaseID = sBase.getId(); + CompSBasePlugin curCompSBasePlugin = (CompSBasePlugin) sBase.getExtension(CompConstants.shortLabel); + // Parse list of elements that are replaced by this element + if (curCompSBasePlugin != null) { + // Get ReplacedBy and add + ReplacedBy replacedBy = curCompSBasePlugin.getReplacedBy(); + if (replacedBy != null) { + if (!submodelPathCopy.isEmpty()) { + HashMap> hashMapOfModel = this.replacedAndDeletedElementsHashMap.get(submodelPathOg); + String replacedByRef = null; + IdType replacedByType = null; + submodelPathCopy.add(replacedBy.getSubmodelRef()); + + //If replacedBy is nested (sBaseRef set) get final sBaseRef and extend submodelPath accordingly + if (replacedBy.getSBaseRef() != null) { + SBaseRef finalSBaseRef = determineSubmodelPath(replacedBy.getSBaseRef(), submodelPathCopy); + if (finalSBaseRef.isSetIdRef()) { + replacedByRef = finalSBaseRef.getIdRef(); + replacedByType = IdType.ID; + } else if (finalSBaseRef.isSetMetaIdRef()) { + replacedByRef = finalSBaseRef.getMetaIdRef(); + replacedByType = IdType.META_ID; + } else if (finalSBaseRef.isSetPortRef()) { + replacedByRef = finalSBaseRef.getPortRef(); + replacedByType = IdType.PORT; + } else if (finalSBaseRef.isSetUnitRef()) { + replacedByRef = finalSBaseRef.getUnitRef(); + replacedByType = IdType.UNIT_ID; + } + } else { + if (replacedBy.isSetIdRef()) { + replacedByRef = replacedBy.getIdRef(); + replacedByType = IdType.ID; + } else if (replacedBy.isSetMetaIdRef()) { + replacedByRef = replacedBy.getMetaIdRef(); + replacedByType = IdType.META_ID; + } else if (replacedBy.isSetPortRef()) { + replacedByRef = replacedBy.getPortRef(); + replacedByType = IdType.PORT; + } else if (replacedBy.isSetUnitRef()) { + replacedByRef = replacedBy.getUnitRef(); + replacedByType = IdType.UNIT_ID; + } + } + + String modelRef = ((CompModelPlugin)model.getExtension(CompConstants.shortLabel)).getListOfSubmodels().get(replacedBy.getSubmodelRef()).getModelRef(); + ReplacedElementInfo replacedElementInfo = new ReplacedElementInfo( + replacedByRef, + modelRef, + replacedByType, + null, + submodelPathCopy + ); + + + // Add replacement to replacedAndDeletedElementsHashMap + if (hashMapOfModel == null) { + HashMap> typeHashMap = new HashMap>(); + HashMap replacementHashMap = new HashMap(); + replacementHashMap.put(sBaseID, replacedElementInfo); + typeHashMap.put(IdType.ID, replacementHashMap); + this.replacedAndDeletedElementsHashMap.put( + submodelPathOg, + typeHashMap + ); + } else { + HashMap hashMapOfRefType = hashMapOfModel.get(IdType.ID); + if (hashMapOfRefType == null) { + HashMap replacementHashMap = new HashMap(); + replacementHashMap.put(sBaseID, replacedElementInfo); + hashMapOfModel.put(IdType.ID, replacementHashMap); + } else { + hashMapOfRefType.put(sBaseID, replacedElementInfo); + } + } } + } + sBase.unsetExtension(CompConstants.shortLabel); + sBase.disablePackage(CompConstants.shortLabel); + } + } + } + } + + ////////////////////////////////////////// + /// References modifier methods ///////// + //////////////////////////////////////// + /** + * + * Add prefixes to references, replace references to elements that were replaced + * and add conversion factors appropriately (as defined in comp package docs) + * + * @param model + * @param subModelPrefix + * @param replacedElementsHashMap + * @param timeConvFactorNode + * @param extentConvFactorNode + */ + private void addPrefixesAndReplacementsToModelReferences(Model model, String subModelPrefix, + HashMap replacedElementsHashMap, + ASTNode timeConvFactorNode, + ASTNode extentConvFactorNode) { + + + // Rules + for (Rule rule : model.getListOfRules()) { + Class classRule = rule.getClass(); + String variable; + + replaceVariables(rule.getMath(), replacedElementsHashMap, subModelPrefix, timeConvFactorNode); + if (!classRule.equals(AlgebraicRule.class)) { + if (classRule.equals(RateRule.class)) { + RateRule rRule = (RateRule) rule; + variable = rRule.getVariable(); + if (replacedElementsHashMap.containsKey(variable)) { + rRule.setVariable(replacedElementsHashMap.get(variable).id); + String convFactor = replacedElementsHashMap.get(variable).conversionFactor; + if (convFactor != null) { + ASTNode convNode = new ASTNode(convFactor); + rRule.getMath().multiplyWith(convNode); + } + if (timeConvFactorNode != null) { + rRule.getMath().divideBy(timeConvFactorNode); + } + } else { + rRule.setVariable(subModelPrefix + variable); + } + } else if (classRule.equals(AssignmentRule.class)) { + AssignmentRule aRule = (AssignmentRule) rule; + variable = aRule.getVariable(); + if (replacedElementsHashMap.containsKey(variable)) { + aRule.setVariable(replacedElementsHashMap.get(variable).id); + String convFactor = replacedElementsHashMap.get(variable).conversionFactor; + if (convFactor != null) { + ASTNode convNode = new ASTNode(convFactor); + aRule.getMath().multiplyWith(convNode); + } + } else { + aRule.setVariable(subModelPrefix + variable); } } } + } + // Initial Assignments + for (InitialAssignment initAssign : model.getListOfInitialAssignments()) { + replaceVariables(initAssign.getMath(), replacedElementsHashMap, subModelPrefix, timeConvFactorNode); + String initAssignVariable = initAssign.getVariable(); + if (replacedElementsHashMap.containsKey(initAssignVariable)) { + initAssign.setVariable(replacedElementsHashMap.get(initAssignVariable).id); + String convFactor = replacedElementsHashMap.get(initAssignVariable).conversionFactor; + if (convFactor != null) { + ASTNode convNode = new ASTNode(convFactor); + initAssign.getMath().multiplyWith(convNode); + } + } else { + initAssign.setVariable(subModelPrefix + initAssignVariable); + } } - } - private void handlePorts(CompModelPlugin compModelPlugin, ListOf listOfPorts){ + for(Compartment compartment: model.getListOfCompartments()) { + updateUnits(compartment, replacedElementsHashMap, subModelPrefix, model); + } - for (Port port : listOfPorts){ + // Species compartments + for (Species species : model.getListOfSpecies()) { + String compartment = species.getCompartment(); + updateUnits(species, replacedElementsHashMap, subModelPrefix, model); + if (replacedElementsHashMap.containsKey(compartment)) { + species.setCompartment(replacedElementsHashMap.get(compartment).id); + } else { + species.setCompartment(subModelPrefix + compartment); + } + } - // Port object instance defines a port for a component in a model. + // Constraints + for (Constraint constraint : model.getListOfConstraints()) { + replaceVariables(constraint.getMath(), replacedElementsHashMap, subModelPrefix, timeConvFactorNode); + } - // A port could be created by using the metaIdRef attribute - // to identify the object for which a given Port instance is the port; - // The question 'what does this port correspond to?' would be answered by the value of the metaIdRef attribute. + // Reactions and more specifically Reactants, Products and Kinetic laws + for (Reaction reaction : model.getListOfReactions()) { - String idRef = port.getIdRef(); - String metaIDRef = port.getMetaIdRef(); + for (SpeciesReference speciesRef : reaction.getListOfReactants()) { + String species = speciesRef.getSpecies(); + if (replacedElementsHashMap.containsKey(species)) { + speciesRef.setSpecies(replacedElementsHashMap.get(species).id); + } else { + speciesRef.setSpecies(subModelPrefix + species); + } + updateIDs(speciesRef, replacedElementsHashMap, subModelPrefix); + } - if(metaIDRef != null && !metaIDRef.isEmpty()){ + for (SpeciesReference speciesRef : reaction.getListOfProducts()) { + String species = speciesRef.getSpecies(); + if (replacedElementsHashMap.containsKey(species)) { + speciesRef.setSpecies(replacedElementsHashMap.get(species).id); + } else { + speciesRef.setSpecies(subModelPrefix + species); + } + updateIDs(speciesRef, replacedElementsHashMap, subModelPrefix); + } - SBase parentOfPort = compModelPlugin.getParent(); + if (reaction.isSetKineticLaw()) { + replaceVariables(reaction.getKineticLaw().getMath(), replacedElementsHashMap, subModelPrefix, timeConvFactorNode); + ASTNode convFactorNode; + if (extentConvFactorNode != null && timeConvFactorNode != null) { + convFactorNode = extentConvFactorNode.divideBy(timeConvFactorNode); + reaction.getKineticLaw().getMath().multiplyWith(convFactorNode); + } else if (extentConvFactorNode != null) { + convFactorNode = extentConvFactorNode; + reaction.getKineticLaw().getMath().multiplyWith(convFactorNode); + } else if (timeConvFactorNode != null) { + convFactorNode = new ASTNode(1).divideBy(timeConvFactorNode); + reaction.getKineticLaw().getMath().multiplyWith(convFactorNode); + } - SBase sBase = compModelPlugin.getSBMLDocument().findSBase(idRef); - addSBaseToModel(parentOfPort.getModel(), sBase); + for (LocalParameter localParameter : reaction.getKineticLaw().getListOfLocalParameters()) { + updateUnits(localParameter, replacedElementsHashMap, subModelPrefix, model); + updateIDs(localParameter, replacedElementsHashMap, subModelPrefix); + } + } + } - } else if(idRef != null && !idRef.isEmpty()){ - SBase parentOfPort = compModelPlugin.getParent(); + // Delays, triggers and event assignments + for (Event event : model.getListOfEvents()) { + Delay delay = event.getDelay(); + if (delay != null) { + replaceVariables(delay.getMath(), replacedElementsHashMap, subModelPrefix, timeConvFactorNode); + if (timeConvFactorNode != null) { + delay.getMath().multiplyWith(timeConvFactorNode); + } + updateIDs(delay, replacedElementsHashMap, subModelPrefix); + } - for (ModelDefinition modelDefinition : this.modelDefinitionListOf) { - SBase sBase = modelDefinition.findNamedSBase(idRef); + Trigger trigger = event.getTrigger(); + if (trigger != null) { + replaceVariables(trigger.getMath(), replacedElementsHashMap, subModelPrefix, timeConvFactorNode); + updateIDs(trigger, replacedElementsHashMap, subModelPrefix); + } - if(sBase != null){ - addSBaseToModel(parentOfPort.getModel(), sBase); - break; + Priority priority = event.getPriority(); + if (priority != null) { + replaceVariables(priority.getMath(), replacedElementsHashMap, subModelPrefix, timeConvFactorNode); + updateIDs(priority, replacedElementsHashMap, subModelPrefix); + } + + for (EventAssignment eventAssignment : event.getListOfEventAssignments()) { + replaceVariables(eventAssignment.getMath(), replacedElementsHashMap, subModelPrefix, timeConvFactorNode); + if (replacedElementsHashMap.containsKey(eventAssignment.getVariable())) { + ReplacedElementInfo value = replacedElementsHashMap.get(eventAssignment.getVariable()); + eventAssignment.setVariable(value.id); + if (value.conversionFactor != null) { + eventAssignment.getMath().multiplyWith(new ASTNode(value.conversionFactor)); } } + else { + eventAssignment.setVariable(subModelPrefix + eventAssignment.getVariable()); + } + updateIDs(eventAssignment, replacedElementsHashMap, subModelPrefix); + } + } + } + + /** + * Set units of newly created model according to original model + * + * @param ogModel original model + */ + private void setModelUnits(Model ogModel) { + + if(ogModel.isSetAreaUnits()) { + this.flattenedModel.setAreaUnits(ogModel.getAreaUnits()); + } + if(ogModel.isSetExtentUnits()) { + this.flattenedModel.setExtentUnits(ogModel.getExtentUnits()); + } + if(ogModel.isSetLengthUnits()) { + this.flattenedModel.setLengthUnits(ogModel.getLengthUnits()); + } + if(ogModel.isSetTimeUnits()) { + this.flattenedModel.setTimeUnits(ogModel.getTimeUnits()); + } + if(ogModel.isSetSubstanceUnits()) { + this.flattenedModel.setSubstanceUnits(ogModel.getSubstanceUnits()); + } + if(ogModel.isSetVolumeUnits()) { + this.flattenedModel.setVolumeUnits(ogModel.getSubstanceUnits()); + } + } + + /** + * + * Replace Units that were replaced + * + */ + private void replaceUnits() { + // Replace units (references) in compartments + for(Compartment comp: this.flattenedModel.getListOfCompartments()) { + if(comp.isSetUnits() && this.newUnitDefMaps.containsKey(comp.getUnits())) { + comp.setUnits(newUnitDefMaps.get(comp.getUnits())); } + } - // If a port references an object from a namespace that is not understood by the interpreter, - // the interpreter must consider the port to be not understood as well. - // If an interpreter cannot tell whether the referenced object does not - // exist or if exists in an unparsed XML or SBML namespace, it may choose to display a warning to the user. + // Replace units (references) in species + for (Species spec : this.flattenedModel.getListOfSpecies()) { + if (spec.isSetUnits() && this.newUnitDefMaps.containsKey(spec.getUnits())) { + spec.setUnits(this.newUnitDefMaps.get(spec.getUnits())); + } + } + // Replace units (references) in local parameters of kinetic laws + for(Reaction reac: this.flattenedModel.getListOfReactions()) { + if(reac.isSetKineticLaw() && reac.getKineticLaw().isSetListOfLocalParameters()) { + for(LocalParameter lp: reac.getKineticLaw().getListOfLocalParameters()) { + if(this.newUnitDefMaps.containsKey(lp.getUnits())) { + lp.setUnits(this.newUnitDefMaps.get(lp.getUnits())); + } + } + } } + } - listOfPorts.removeFromParent(); + /** + * + * Recursively replace variables/symbols in math elements + * + * @param parent + * @param replacedElementsHashMap + * @param subModelPrefix + * @param timeConvFactorNode + */ + private void replaceVariables(ASTNode parent, HashMap replacedElementsHashMap, + String subModelPrefix, ASTNode timeConvFactorNode) { + + int i = 0; + List listOfNodes = new ArrayList<>(parent.getListOfNodes()); + for (ASTNode node : listOfNodes) { + List children = node.getChildren(); + // If leaf is reached -> apply Conversion Factor + // Otherwise recall with children of node + if (children.isEmpty()) { + applyConvFactorToNode(node, parent, replacedElementsHashMap, subModelPrefix, timeConvFactorNode, i); + } else { + replaceVariables(node, replacedElementsHashMap, subModelPrefix, timeConvFactorNode); + } + i++; + } + applyConvFactorToNode(parent, parent, replacedElementsHashMap, subModelPrefix, timeConvFactorNode, i); } - private Model initSubModels(CompModelPlugin compModelPlugin) { + /** + * + * Update the id of a given sBase by either adding a prefix or replacing it if present in replacedElementHashMap + * + * @param sBase + * @param replacedElementsHashMap + * @param subModelPrefix + */ + private void updateIDs(SBase sBase, HashMap replacedElementsHashMap, String subModelPrefix) { - ListOf subModelListOf = compModelPlugin.getListOfSubmodels().clone(); + if(sBase.isSetId()) { + if (replacedElementsHashMap.containsKey(sBase.getId())) { + String value = replacedElementsHashMap.get(sBase.getId()).id; + sBase.setId(value); + } else { + sBase.setId(subModelPrefix + sBase.getId()); + } + } + if(sBase.isSetMetaId()) { + if (replacedElementsHashMap.containsKey(sBase.getMetaId())) { + String value = replacedElementsHashMap.get(sBase.getMetaId()).id; + sBase.setMetaId(value); + } else { + sBase.setMetaId(subModelPrefix + sBase.getMetaId()); + } + } + } - // TODO: replace elements - replaceElementsInModelDefinition(compModelPlugin, null); + /** + * + * Update Units of givem SBase depending on if its present in replacedElementsHashMap + * + * @param sBase + * @param replacedElementsHashMap + * @param subModelPrefix + * @param model + */ + private void updateUnits(SBaseWithUnit sBase, HashMap replacedElementsHashMap, + String subModelPrefix, Model model) { + + if(sBase.isSetUnits()) { + if (replacedElementsHashMap.containsKey(sBase.getUnits())) { + String subPrefix = this.subModelPrefixes.get(replacedElementsHashMap.get(sBase.getUnits()).replacedElementPath); + this.newUnitDefMaps.put(sBase.getUnits(), subPrefix + replacedElementsHashMap.get(sBase.getUnits()).id); + } else if (model.getPredefinedUnitDefinition(sBase.getUnits()) == null) { + this.newUnitDefMaps.put(sBase.getUnits(), subModelPrefix + sBase.getUnits()); + } + } + } - // TODO: ports - ListOf listOfPorts = compModelPlugin.getListOfPorts(); - handlePorts(compModelPlugin, listOfPorts); + /** + * Remove elements that were either replaced or deleted in the model + * and at them to the replacedElementsHashmap to later update ID references accordingly + * + * @param modelCopy + * @param compModelPlugin + * @return Hash map of replaced elements + */ + private HashMap removeReplacedAndDeletedElements(Model modelCopy, CompModelPlugin compModelPlugin) { + + // Get all replaced and deleted Elements for current model and initialize ReplacedElements hash map + HashMap> replacedElementsInModel = this.replacedAndDeletedElementsHashMap.get(curPath); + HashMap replacedElements = new HashMap<>(); + + Model ogModelCopy = modelCopy.clone(); + + if (replacedElementsInModel != null) { + // Iterate over every entry in hash map of replaced and deleted elements + for (IdType key : replacedElementsInModel.keySet()) { + // Different replacement procedures are needed depending on idType (id, metaID, portID, unitID) of the element that is replaced + switch (key) { + case PORT: + if (compModelPlugin != null) { + for (Map.Entry entry : replacedElementsInModel.get(IdType.PORT).entrySet()) { + // Obtain id of element from port + Port port = compModelPlugin.getListOfPorts().get(entry.getKey()); + Pair result = selectCorrectRef(port); + // Remove element and port from parent + if (result.getValue().equals(IdType.META_ID)) { + modelCopy.getElementByMetaId(result.getKey()).removeFromParent(); + } else { + modelCopy.getElementBySId(result.getKey()).removeFromParent(); + } + port.removeFromParent(); + // If element was replaced (not deleted) add the element that it's replaced by to replacedElements + if (entry.getValue() != null) { + replacedElements.put(result.getKey(), entry.getValue()); + } + } + } + break; + case ID: + for (Map.Entry entry : replacedElementsInModel.get(IdType.ID).entrySet()) { + SBase replacedSBase = modelCopy.getElementBySId(entry.getKey()); + SBase ogReplacedSBase = ogModelCopy.getElementBySId(entry.getKey()); + if (replacedSBase != null) { + // Species References need special treatment because id needs to stay the same if replaced + // and new Initial Assignments might be created + if((replacedSBase.getClass().equals(SpeciesReference.class)) && (entry.getValue() != null)) { + ReplacedElementInfo repElInfo = entry.getValue().clone(); + Model replacedElementModel = this.visitedModels.get(repElInfo.modelId); + String submodelPrefix = this.subModelPrefixes.get(repElInfo.replacedElementPath); + SpeciesReference specRef = null; + + switch (repElInfo.idType) { + case PORT: + CompModelPlugin cmp = (CompModelPlugin) replacedElementModel.getPlugin(CompConstants.shortLabel); + Port port = cmp.getPort(repElInfo.id); + specRef = replacedElementModel.getModel().findSpeciesReference(port.getIdRef()).clone(); + break; + case ID: + specRef = replacedElementModel.getModel().findSpeciesReference(entry.getValue().id).clone(); + break; + case META_ID: + specRef = (SpeciesReference) replacedElementModel.getModel().getElementByMetaId(entry.getValue().id).clone(); + break; + default: + LOGGER.warning("Replacing SpeciesReference has neither a port or (meta) id!"); + break; + } + + if(specRef != null) { + // Update speciesReference with replacedElements value except ID and MetaID (as replacement would lead to duplicate IDs) + SpeciesReference replacedSpecRef = (SpeciesReference) replacedSBase; + replacedSpecRef.setSpecies(submodelPrefix + specRef.getSpecies()); + replacedSpecRef.setStoichiometry(specRef.getStoichiometry()); + replacedSpecRef.setConstant(specRef.getConstant()); + replacedSpecRef.setValue(specRef.getValue()); + replacedSpecRef.setAnnotation(specRef.getAnnotation()); + if(entry.getValue() != null) { + checkInitialAssignmentsAndRules(modelCopy, ogReplacedSBase.getId(), entry.getValue(), replacedElements); + } + } + } + else { + replacedSBase.removeFromParent(); + // If element was replaced (not deleted) add the element that it's replaced by to replacedElements + if(entry.getValue() != null) { + replacedElements.put(replacedSBase.getId(), entry.getValue()); + } + } + } + // Replacement could be UnitDefinition (can not be got by getElementBySID) + else if(modelCopy.getUnitDefinition(entry.getKey()) != null) { + UnitDefinition unitDef = modelCopy.getUnitDefinition(entry.getKey()); + unitDef.removeFromParent(); + if (entry.getValue() != null) { + HashMap hashMap = new HashMap<>(); + hashMap.put(entry.getKey(), entry.getValue().id); + replacedElements.put(entry.getKey(), entry.getValue()); + } + } + else if(ogReplacedSBase != null && entry.getValue() != null) { + checkInitialAssignmentsAndRules(modelCopy, ogReplacedSBase.getId(), entry.getValue(), replacedElements); + } + } + break; + case META_ID: + for (Map.Entry entry : replacedElementsInModel.get(IdType.META_ID).entrySet()) { + SBase replacedSBase = modelCopy.getElementByMetaId(entry.getKey()); + SBase ogReplacedSBase = ogModelCopy.getElementByMetaId(entry.getKey()); + if (replacedSBase != null) { + // Species References need special treatment because id needs to stay the same if replaced + // and new Initial Assignments might be created + if(replacedSBase.getClass().equals(SpeciesReference.class) && entry.getValue() != null) { + Model replacedElementModel = this.visitedModels.get(entry.getValue().modelId); + ReplacedElementInfo repElInfo = entry.getValue().clone(); + String submodelPrefix = this.subModelPrefixes.get(repElInfo.replacedElementPath); + SpeciesReference specRef = null; + + switch (repElInfo.idType) { + case PORT: + CompModelPlugin cmp = (CompModelPlugin) replacedElementModel.getPlugin(CompConstants.shortLabel); + Port port = cmp.getPort(repElInfo.id); + specRef = replacedElementModel.getModel().findSpeciesReference(port.getIdRef()).clone(); + break; + case ID: + specRef = replacedElementModel.getModel().findSpeciesReference(entry.getValue().id).clone(); + break; + case META_ID: + specRef = (SpeciesReference) replacedElementModel.getModel().getElementByMetaId(entry.getValue().id).clone(); + break; + default: + LOGGER.warning("Replacing SpeciesReference has neither a port or (meta) id!"); + break; + } + + + if(specRef != null) { + // Update speciesReference with replacedElements value except ID and MetaID (as replacement would lead to duplicate IDs) + SpeciesReference replacedSpecRef = (SpeciesReference) replacedSBase; + replacedSpecRef.setSpecies(submodelPrefix + specRef.getSpecies()); + replacedSpecRef.setStoichiometry(specRef.getStoichiometry()); + replacedSpecRef.setConstant(specRef.getConstant()); + replacedSpecRef.setValue(specRef.getValue()); + replacedSpecRef.setAnnotation(specRef.getAnnotation()); + if(entry.getValue() != null) { + checkInitialAssignmentsAndRules(modelCopy, replacedSBase.getId(), entry.getValue(), replacedElements); + } + } + } + else { + replacedSBase.removeFromParent(); + if (entry.getValue() != null) { + replacedElements.put(replacedSBase.getId(), entry.getValue()); + } + } + } + // Replacement could be UnitDefinition (can not be got by getElementByMetaID) + else if(modelCopy.getUnitDefinition(entry.getKey()) != null) { + UnitDefinition unitDef = modelCopy.getUnitDefinition(entry.getKey()); + unitDef.removeFromParent(); + if (entry.getValue() != null) { + HashMap hashMap = new HashMap<>(); + hashMap.put(entry.getKey(), entry.getValue().id); + replacedElements.put(entry.getKey(), entry.getValue()); + } + } + else if(ogReplacedSBase != null && entry.getValue() != null) { + checkInitialAssignmentsAndRules(modelCopy, ogReplacedSBase.getId(), entry.getValue(), replacedElements); + } + } + break; + case UNIT_ID: + for (Map.Entry entry : replacedElementsInModel.get(IdType.UNIT_ID).entrySet()) { + UnitDefinition unitDef = modelCopy.getUnitDefinition(entry.getKey()); + //modelCopy.getElementBySId(unitDef.getId()).removeFromParent(); + unitDef.removeFromParent(); + if (entry.getValue() != null) { + HashMap hashMap = new HashMap<>(); + hashMap.put(entry.getKey(), entry.getValue().id); + //newUnitDefMaps.add(hashMap); + replacedElements.put(entry.getKey(), entry.getValue()); + } + } + break; + } + } + } + + return replacedElements; + } + + + ////////////////////////////////////////// + /// Utility ///////////////////////////// + //////////////////////////////////////// + + /** + * Adds newly created initial Assignment and Rules to model + * + * @param modelCopy + */ + private void addNewElementsWithVariables(Model modelCopy) { + if(this.newInitAssignHashMap.containsKey(curPath)) { + modelCopy.getListOfInitialAssignments().addAll(this.newInitAssignHashMap.get(curPath)); + } + if(this.newRulesHashMap.containsKey(curPath)) { + modelCopy.getListOfRules().addAll(this.newRulesHashMap.get(curPath)); + } + } + + /** + * Create ASTNode for all conversion factors of current model by multiplying them + * + * @param modelCopy + * @param convFactors list of conversion factors + * @return ASTNode created by multiplying all conversion factors + */ + private ASTNode createNewConvNode(Model modelCopy, List convFactors) { + + + if (convFactors.size() > 0) { + StringBuilder convNodeName = new StringBuilder(convFactors.get(0)); + ASTNode initAssignConvNode = new ASTNode(convNodeName.toString()); + for (String convFactor : convFactors.subList(1, convFactors.size())) { + initAssignConvNode.multiplyWith(new ASTNode(convFactor)); + convNodeName.append("_times_").append(convFactor); + } - for (Submodel submodel : subModelListOf) { + if (!this.flattenedModel.containsParameter(convNodeName.toString()) && convFactors.size() > 1) { + InitialAssignment initAssignConv = new InitialAssignment(this.flattenedModel.getLevel(), this.flattenedModel.getVersion()); + initAssignConv.setVariable(convNodeName.toString()); + initAssignConv.setMath(initAssignConvNode); + this.flattenedModel.getListOfInitialAssignments().add(initAssignConv); + + Parameter parameterConv = new Parameter(this.flattenedModel.getLevel(), this.flattenedModel.getVersion()); + parameterConv.setId(convNodeName.toString()); + this.flattenedModel.getListOfParameters().add(parameterConv); + } + + return new ASTNode(convNodeName.toString()); + } + + return null; + } + + /** + * If species reference is replaced initial assignment/rule for it have to be updated. + * There are two possibilities: + * 1. Initial Assignment/Rule of replacedElement also references another element -> create new InitialAssignment that's a copy of it + * 2. Initial Assignment/Rule of replacedElement does not reference another element -> simply replace variable of assignment/rule + * + * + * @param modelCopy + * @param replacedID + * @param newElInfo + * @param replacedElements + */ + private void checkInitialAssignmentsAndRules(Model modelCopy, String replacedID, ReplacedElementInfo newElInfo, HashMap replacedElements) { - this.listOfSubmodelsToFlatten.add(submodel); - ModelDefinition modelDefinition = this.modelDefinitionListOf.get(submodel.getModelRef()); + // Initial Assignments + List listOfInitialAssignments = modelCopy.getListOfInitialAssignments(); + List newInitialAssignments = new ArrayList<>(); - // TODO: how to initialize external model definitions? - if (modelDefinition == null) { - ExternalModelDefinition externalModelDefinition = this.externalModelDefinitionListOf.get(submodel.getModelRef()); - try { - SBMLDocument externalDocument = SBMLReader.read(new File(externalModelDefinition.getSource())); - Model flattendExternalModel = flatten(externalDocument).getModel(); //external model can also contain submodels etc. - this.flattenedModel = mergeModels(this.flattenedModel, flattendExternalModel); - } catch (XMLStreamException | IOException e) { - e.printStackTrace(); + for(InitialAssignment initAssign: listOfInitialAssignments) { + if(replacedID.equals(initAssign.getVariable())) { + if(modelCopy.getElementBySId(replacedID) == null) { + replacedElements.put(replacedID, newElInfo); } + else { + InitialAssignment initAssignNew = initAssign.clone(); + initAssignNew.setVariable(newElInfo.id); + newInitialAssignments.add(initAssignNew); + break; + } + } + } + if(!newInitialAssignments.isEmpty()) { + if (this.newInitAssignHashMap.containsKey(newElInfo.replacedElementPath)) { + this.newInitAssignHashMap.get(newElInfo.replacedElementPath).addAll(newInitialAssignments); + } else { + this.newInitAssignHashMap.put(newElInfo.replacedElementPath, newInitialAssignments); } + } + + // Rules + List listOfRules = modelCopy.getListOfRules(); + List newRules = new ArrayList<>(); - if (modelDefinition != null && modelDefinition.getExtension(CompConstants.shortLabel) != null) { - initSubModels((CompModelPlugin) modelDefinition.getExtension(CompConstants.shortLabel)); + for(Rule rule: listOfRules) { + if(rule.getClass().equals(AssignmentRule.class)) { + AssignmentRule aRule = (AssignmentRule) rule; + if(replacedID.equals(aRule.getVariable())) { + if(modelCopy.getElementBySId(replacedID) == null) { + replacedElements.put(replacedID, newElInfo); + } + else { + AssignmentRule aRuleNew = aRule.clone(); + aRuleNew.setVariable(newElInfo.id); + newRules.add(aRuleNew); + } + } + } + else if(rule.getClass().equals(RateRule.class)) { + RateRule rRule = (RateRule) rule; + if(replacedID.equals(rRule.getVariable())) { + if(modelCopy.getElementBySId(replacedID) == null) { + replacedElements.put(replacedID, newElInfo); + } + else { + RateRule rRuleNew = rRule.clone(); + rRuleNew.setVariable(newElInfo.id); + newRules.add(rRuleNew); + } + } + } + } + if(!newRules.isEmpty()) { + if (this.newRulesHashMap.containsKey(newElInfo.replacedElementPath)) { + this.newRulesHashMap.get(newElInfo.replacedElementPath).addAll(newRules); } else { - LOGGER.info("No model definition found in " + submodel.getId() + ".") ; + this.newRulesHashMap.put(newElInfo.replacedElementPath, newRules); } + } + } + /** + * + * Given sBase reference returns its id and idType as a pair + * + * @param sBaseRef + * @return pair of id and idType + */ + private Pair selectCorrectRef(SBaseRef sBaseRef) { + + String elementRef = null; + IdType elementType = null; + + if (sBaseRef.isSetIdRef()) { + elementRef = sBaseRef.getIdRef(); + elementType = IdType.ID; + } else if (sBaseRef.isSetMetaIdRef()) { + elementRef = sBaseRef.getMetaIdRef(); + elementType = IdType.META_ID; + } else if (sBaseRef.isSetPortRef()) { + elementRef = sBaseRef.getPortRef(); + elementType = IdType.PORT; + } else if (sBaseRef.isSetUnitRef()) { + elementRef = sBaseRef.getUnitRef(); + elementType = IdType.UNIT_ID; } - return flattenAndMergeModels(this.listOfSubmodelsToFlatten); + return new Pair( + elementRef, + elementType + ); } - private Model flattenAndMergeModels(List listOfSubmodelsToFlatten) { - int sizeOfList = listOfSubmodelsToFlatten.size(); + /** + * + * Recursively iterate through nested sBaseRef until last sBaseRef and return it + * Also extends submodelPath accordingly + * + * @param sBaseRef + * @param submodelPath + * @return + */ + private SBaseRef determineSubmodelPath(SBaseRef sBaseRef, List submodelPath) { - if (sizeOfList >= 2) { - Submodel last = listOfSubmodelsToFlatten.get(sizeOfList - 1); - Submodel secondLast = listOfSubmodelsToFlatten.get(sizeOfList - 2); - this.flattenedModel = mergeModels(this.flattenedModel, mergeModels(flattenSubModel(secondLast), flattenSubModel(last))); - listOfSubmodelsToFlatten.remove(secondLast); - listOfSubmodelsToFlatten.remove(last); + if (sBaseRef.getSBaseRef() != null) { + if (!sBaseRef.getIdRef().isEmpty()) { + submodelPath.add(sBaseRef.getIdRef()); + } else if (!sBaseRef.getMetaIdRef().isEmpty()) { + submodelPath.add(sBaseRef.getMetaIdRef()); + } + return determineSubmodelPath(sBaseRef.getSBaseRef(), submodelPath); + } else { + return sBaseRef; + } - flattenAndMergeModels(listOfSubmodelsToFlatten); + } - } else if (sizeOfList == 1) { - Submodel last = listOfSubmodelsToFlatten.get(sizeOfList - 1); - this.flattenedModel = mergeModels(this.flattenedModel, flattenSubModel(last)); + /** + * + * Get all lists including the list in each reaction + * + * @param model + * @return + */ + private List> getListOfListsOfSBases(Model model) { + + List> listOfListsOfSBases = new ArrayList<>(); + + listOfListsOfSBases.addAll(Arrays.asList( + model.getListOfCompartments(), + model.getListOfParameters(), + model.getListOfEvents(), + model.getListOfSpecies(), + model.getListOfFunctionDefinitions(), + model.getListOfRules(), + model.getListOfConstraints(), + model.getListOfUnitDefinitions(), + model.getListOfReactions(), + model.getListOfInitialAssignments() + )); + + for (Reaction reaction : model.getListOfReactions()) { + listOfListsOfSBases.add(reaction.getListOfProducts()); + listOfListsOfSBases.add(reaction.getListOfReactants()); } - return this.flattenedModel; + return listOfListsOfSBases; + } + /** + * Get all lists excluding the list in each reaction + * + * @param model + * @return + */ + private List> getAllListsOfModel(Model model) { + return Arrays.asList( + model.getListOfCompartments(), + model.getListOfSpecies(), + model.getListOfFunctionDefinitions(), + model.getListOfRules(), + model.getListOfEvents(), + model.getListOfUnitDefinitions(), + model.getListOfReactions(), + model.getListOfConstraints(), + model.getListOfParameters(), + model.getListOfInitialAssignments() + ); + } + + + /** + * + * Apply the conversion factor to a node + * + * @param node + * @param parent + * @param replacedElementsHashMap + * @param subModelPrefix + * @param timeConvFactorNode + * @param i + */ + private void applyConvFactorToNode(ASTNode node, ASTNode parent, + HashMap replacedElementsHashMap, + String subModelPrefix, ASTNode timeConvFactorNode, int i) { + + if (node.getType() == ASTNode.Type.NAME) { + if (replacedElementsHashMap.containsKey(node.toString())) { + ASTNode newNode = new ASTNode(replacedElementsHashMap.get(node.toString()).id); + // Apply conversion factor to element + String convFactor = replacedElementsHashMap.get(node.toString()).conversionFactor; + if (convFactor != null) { + ASTNode convNode = new ASTNode(convFactor); + newNode.divideBy(convNode); + } + parent.replaceChild(i, newNode); + } else { + parent.replaceChild(i, new ASTNode(subModelPrefix + node)); + } + } else if (node.getType() == ASTNode.Type.NAME_TIME && timeConvFactorNode != null) { + node.divideBy(timeConvFactorNode); + } else if (node.getType() == ASTNode.Type.FUNCTION_DELAY && timeConvFactorNode != null) { + node.getChild(1).multiplyWith(timeConvFactorNode); + } } + /** * All remaining elements are placed in a single Model object * The original Model, ModelDefinition, and ExternalModelDefinition objects are all deleted @@ -324,36 +1303,33 @@ private Model mergeModels(Model previousModel, Model currentModel) { // Compartments -> Species -> Function Definitions -> Rules -> Events -> Units -> Reactions -> Parameters // If done in this order, potential conflicts are resolved incrementally along the way. + // Set level/version from whichever model is available if (previousModel != null) { - - // match versions and level mergedModel.setLevel(previousModel.getLevel()); mergedModel.setVersion(previousModel.getVersion()); - - mergeListsOfModels(previousModel.getListOfCompartments(), previousModel, mergedModel); - mergeListsOfModels(previousModel.getListOfSpecies(), previousModel, mergedModel); - mergeListsOfModels(previousModel.getListOfFunctionDefinitions(), previousModel, mergedModel); - mergeListsOfModels(previousModel.getListOfRules(), previousModel, mergedModel); - mergeListsOfModels(previousModel.getListOfEvents(), previousModel, mergedModel); - mergeListsOfModels(previousModel.getListOfUnitDefinitions(), previousModel, mergedModel); - mergeListsOfModels(previousModel.getListOfReactions(), previousModel, mergedModel); - mergeListsOfModels(previousModel.getListOfParameters(), previousModel, mergedModel); - mergeListsOfModels(previousModel.getListOfInitialAssignments(), previousModel, mergedModel); - + } else if (currentModel != null) { + mergedModel.setLevel(currentModel.getLevel()); + mergedModel.setVersion(currentModel.getVersion()); } + // IMPORTANT: merge the current (parent) model first, + // then the previously flattened submodels, so that + // original objects (like param1) appear before + // flattened submodel objects (like submod1__subparam2). if (currentModel != null) { + for (ListOf modelList : getAllListsOfModel(currentModel)) { + if (!modelList.isEmpty()) { + mergeListsOfModels(modelList, currentModel, mergedModel); + } + } + } - mergeListsOfModels(currentModel.getListOfCompartments(), currentModel, mergedModel); - mergeListsOfModels(currentModel.getListOfSpecies(), currentModel, mergedModel); - mergeListsOfModels(currentModel.getListOfFunctionDefinitions(), currentModel, mergedModel); - mergeListsOfModels(currentModel.getListOfRules(), currentModel, mergedModel); - mergeListsOfModels(currentModel.getListOfEvents(), currentModel, mergedModel); - mergeListsOfModels(currentModel.getListOfUnitDefinitions(), currentModel, mergedModel); - mergeListsOfModels(currentModel.getListOfReactions(), currentModel, mergedModel); - mergeListsOfModels(currentModel.getListOfParameters(), currentModel, mergedModel); - mergeListsOfModels(currentModel.getListOfInitialAssignments(), currentModel, mergedModel); - + if (previousModel != null) { + for (ListOf modelList : getAllListsOfModel(previousModel)) { + if (!modelList.isEmpty()) { + mergeListsOfModels(modelList, previousModel, mergedModel); + } + } } // TODO: delete original model, ModelDefinition, and ExternalModelDefinition objects @@ -366,8 +1342,17 @@ private void mergeListsOfModels(ListOf listOfObjects, Model sourceModel, Model t // TODO: generify with listOf SBase ? + if (listOfObjects.getSBaseListType() == ListOf.Type.listOfReactions) { + +/* for(Reaction reaction: targetModel.getListOfReactions()) { + for (SpeciesReference specRef : reaction.getListOfProducts()) { + System.out.println("Target SR: " + targetModel.getElementBySId("z_stoich")); + } + }*/ + + ListOf reactionListOf = sourceModel.getListOfReactions().clone(); sourceModel.getListOfReactions().removeFromParent(); @@ -444,10 +1429,6 @@ private void mergeListsOfModels(ListOf listOfObjects, Model sourceModel, Model t for (Parameter parameter : parameterListOf) { - if(parameter.isSetPlugin(CompConstants.shortLabel)){ - replaceElementsInModelDefinition(null, (CompSBasePlugin) parameter.getExtension(CompConstants.shortLabel)); - parameter.unsetPlugin(CompConstants.shortLabel); - } targetModel.addParameter(parameter.clone()); } } @@ -479,7 +1460,6 @@ private void mergeListsOfModels(ListOf listOfObjects, Model sourceModel, Model t } - //TODO: // no longer supported? there are no get methods for this // ListOf.Type.listOfCompartmentTypes @@ -492,320 +1472,224 @@ private void mergeListsOfModels(ListOf listOfObjects, Model sourceModel, Model t // maybe they are already in listOfReactions? // ListOf.Type.listOfProducts // ListOf.Type.listOfReactants - - } - /** - * Performs the routine to flatten a submodel into a model - * - * @param subModel - * @return - */ - private Model flattenSubModel(Submodel subModel) { - - Model model = new Model(); - // 2 - // Let 'M' be the identifier of a given submodel. - String subModelID = subModel.getId() + "_"; - String subModelMetaID = subModel.getMetaId() + "_"; + private void addPrefixesToSBaseList(Model modelOfSubmodel, ListOf listOfSBase, String subModelPrefix) { - // Verify that no object identifier or meta identifier of objects in that submodel - // (i.e., the id or metaid attribute values) - // begin with the character sequence "M__"; - - // if there is an existing identifier or meta identifier beginning with "M__", - // add an underscore to "M__" (i.e., to produce "M___") and again check that the sequence is unique. - // Continue adding underscores until you find a unique prefix. Let "P" stand for this final prefix. + ListOf list = (ListOf) listOfSBase; - while (this.previousModelIDs.contains(subModelID)) { - subModelID += "_"; - } + for (SBase sBase : list) { - while (this.previousModelMetaIDs.contains(subModelMetaID)) { - subModelMetaID += "_"; - } + if (!sBase.getId().equals("")) { + sBase.setId(subModelPrefix + sBase.getId()); + } - if(!this.previousModelIDs.isEmpty()){ // because libSBML does it - subModelID = this.previousModelIDs.get(this.previousModelIDs.size()-1) + subModelID; + if (!sBase.getMetaId().equals("")) { + sBase.setMetaId(subModelPrefix + sBase.getMetaId()); + } } - this.previousModelIDs.add(subModelID); - this.previousModelMetaIDs.add(subModelID); - - //subModel.setId(subModelID); - //subModel.setMetaId(subModelMetaID); - - if (subModel.getModelRef() != null) { - - // initiate a clone of the referenced model - Model modelOfSubmodel = this.modelDefinitionListOf.get(subModel.getModelRef()).clone(); - - // 3 - // Remove all objects that have been replaced or deleted in the submodel. - - // TODO Delete Objects - // each Deletion object identifies an object to "remove" from that Model instance - for (Deletion deletion : subModel.getListOfDeletions()) { - - // TODO: search for element to remove in all model definitions? - this.modelDefinitionListOf.remove(deletion.getMetaIdRef()); - subModel.removeDeletion(deletion); - - //deletion.removeFromParent(); - //for (ModelDefinition modelDefinition : this.modelDefinitionListOf){ - // modelDefinition.remove(deletion.getMetaIdRef()); - //} - // modelOfSubmodel.remove(deletion.getMetaIdRef()); + } - } - subModel.getListOfDeletions().removeFromParent(); + private Model addPrefixesToModelObjects(Model modelOfSubmodel, String subModelPrefix) { - // TODO: Replace Objects + addPrefixesToSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfReactions(), subModelPrefix); + addPrefixesToSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfCompartments(), subModelPrefix); + addPrefixesToSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfConstraints(), subModelPrefix); + addPrefixesToSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfEvents(), subModelPrefix); - // 4 - // Transform the remaining objects in the submodel as follows: - // a) - // Change every identifier (id attribute) - // to a new value obtained by prepending "P" to the original identifier. - // b) - // Change every meta identifier (metaid attribute) - // to a new value obtained by prepending "P" to the original identifier. + addPrefixesToSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfFunctionDefinitions(), subModelPrefix); + addPrefixesToSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfParameters(), subModelPrefix); + addPrefixesToSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfRules(), subModelPrefix); + addPrefixesToSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfSpecies(), subModelPrefix); + addPrefixesToSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfUnitDefinitions(), subModelPrefix); - model = flattenModel(modelOfSubmodel); + for(Reaction reaction: modelOfSubmodel.getListOfReactions()) { + //addPrefixesToSBaseList(modelOfSubmodel, reaction.getListOfProducts(), subModelPrefix); + //addPrefixesToSBaseList(modelOfSubmodel, reaction.getListOfReactants(), subModelPrefix); + //addPrefixesToSBaseList(modelOfSubmodel, reaction.getListOfModifiers(), subModelPrefix); + } - // 5 - // Transform every SIdRef and IDREF type value in the remaining objects of the submodel as follows: - // a) - // If the referenced object has been replaced by the application of a ReplacedBy or ReplacedElement construct, - // change the SIdRef value (respectively, IDREF value) to the SId value (respectively, ID value) - // of the object replacing it. - // b) - // If the referenced object has not been replaced, change the SIdRef and IDREF value by prepending "P" - // to the original value. + return modelOfSubmodel; + } - // 6 - // After performing the tasks above for all remaining objects, merge the objects of the remaining submodels - // into a single model. - // Merge the various lists (list of species, list of compartments, etc.) - // in this step, and preserve notes and annotations as well as constructs from other SBML Level 3 packages. - //model = modelOfSubmodel; - //model = mergeModels(this.previousModel, modelOfSubmodel); // initiate model (?) + /** + * Collects any {@link ExternalModelDefinition}s that might be contained in + * the given {@link SBMLDocument} and transfers them into local + * {@link ModelDefinition}s (recursively, if the external models themselves + * include external models; in that case, renaming may occur). + *
+ * The given {@link SBMLDocument} need have its locationURI set! + *
+ * Opaque URIs (URNs) will not be dealt with in any defined way, resolve them + * first (make sure all relevant externalModelDefinitions' source-attributes + * are URLs or relative paths) + * + * @param document an {@link SBMLDocument}, which might, but need not, contain + * {@link ExternalModelDefinition}s to be transferred into its local + * {@link ModelDefinition}s. The locationURI of the given document need + * be set ({@link SBMLDocument#isSetLocationURI})! + * @return a new {@link SBMLDocument} without {@link + * ExternalModelDefinition}s, but containing the same information as + * the given one + * @throws Exception if given document's locationURI is not set. Set it with + * {@link SBMLDocument#setLocationURI} + */ + public static SBMLDocument internaliseExternalModelDefinitions( + SBMLDocument document) throws Exception { + if (!document.isSetLocationURI()) { + LOGGER.warning("Location URI is not set: " + document); + throw new Exception( + "document's locationURI need be set. But it was not."); + } + SBMLDocument result = document.clone(); // no side-effects intended + ArrayList usedIds = new ArrayList(); + if (result.isSetModel()) { + usedIds.add(result.getModel().getId()); } - return model; - } - - private void flattenSBaseList(Model modelOfSubmodel, ListOf listOfSBase){ + CompSBMLDocumentPlugin compSBMLDocumentPlugin = + (CompSBMLDocumentPlugin) result.getExtension(CompConstants.shortLabel); - ListOf list = (ListOf) listOfSBase; + // There is nothing to retrieve: + if (compSBMLDocumentPlugin == null || !compSBMLDocumentPlugin.isSetListOfExternalModelDefinitions()) { + return result; + } else { + /** For name-collision-avoidance */ + for (ExternalModelDefinition emd : compSBMLDocumentPlugin.getListOfExternalModelDefinitions()) { + usedIds.add(emd.getId()); + } - for(SBase sBase : list){ + for (ExternalModelDefinition emd : compSBMLDocumentPlugin.getListOfExternalModelDefinitions()) { + // general note: Be careful when using clone/cloning-constructors, they + // do not preserve parent-child-relations + Model referenced = emd.getReferencedModel(); + SBMLDocument referencedDocument = referenced.getSBMLDocument(); + SBMLDocument flattened = internaliseExternalModelDefinitions(referencedDocument); + // Guarantee: flattened does not contain any externalModelDefinitions, only local MDs + // (and main model) + // use this, and migrate the MDs into the current compSBMLDocumentPlugin + StringBuilder prefixBuilder = new StringBuilder(emd.getModelRef()); + /** For name-collision-avoidance */ + boolean contained = false; + do { + contained = false; + prefixBuilder.append("_"); + for (String id : usedIds) { + contained |= id.startsWith(prefixBuilder.toString()); + if (contained) { + break; + } + } + } while (contained); + String newPrefix = prefixBuilder.toString(); + + CompSBMLDocumentPlugin referencedDocumentPlugin = + (CompSBMLDocumentPlugin) flattened.getExtension( + CompConstants.shortLabel); + + ListOf workingList; + if (referencedDocumentPlugin == null) { + // This may happen, if the main model of a non-comp-file is referenced + workingList = new ListOf(); + workingList.setLevel(referenced.getLevel()); + workingList.setVersion(referenced.getVersion()); + } else { + workingList = referencedDocumentPlugin.getListOfModelDefinitions().clone(); + } - if (!sBase.getId().equals("")) { - sBase.setId(modelOfSubmodel.getId() + sBase.getId()); - } + // Check whether the main model is needed; Do not internalise it, if not necessary + boolean isMainReferenced = flattened.getModel().getId().equals(emd.getModelRef()); + for (ModelDefinition md : workingList) { + if (isMainReferenced) { + break; + } + CompModelPlugin cmp = (CompModelPlugin) md.getExtension(CompConstants.shortLabel); + if (cmp != null) { + for (Submodel sm : cmp.getListOfSubmodels()) { + isMainReferenced |= flattened.getModel().getId().equals(sm.getModelRef()); + } + } + } - if (!sBase.getMetaId().equals("")) { - sBase.setMetaId(modelOfSubmodel.getId() + sBase.getMetaId()); - } + if (isMainReferenced) { + ModelDefinition localisedMain = new ModelDefinition(flattened.getModel()); + workingList.add(0, localisedMain); + } - if(sBase.isPackageEnabled(CompConstants.shortLabel)){ - CompSBasePlugin compSBasePlugin = (CompSBasePlugin) sBase.getExtension(CompConstants.shortLabel); - CompModelPlugin compModelPlugin = (CompModelPlugin) modelOfSubmodel.getExtension(CompConstants.shortLabel); + for (ModelDefinition md : workingList) { + ModelDefinition internalised = new ModelDefinition(md); + // i.e. current one is the one directly referenced => take referent's place + if (md.getId().equals(referenced.getId())) { + internalised.setId(emd.getId()); + } else { + internalised.setId(newPrefix + internalised.getId()); + } - replaceElementsInModelDefinition(compModelPlugin,compSBasePlugin); + CompModelPlugin notYetInternalisedModelPlugin = + (CompModelPlugin) internalised.getExtension(CompConstants.shortLabel); + if (notYetInternalisedModelPlugin != null && notYetInternalisedModelPlugin.isSetListOfSubmodels()) { + for (Submodel sm : notYetInternalisedModelPlugin.getListOfSubmodels()) { + sm.setModelRef(newPrefix + sm.getModelRef()); + } + } + compSBMLDocumentPlugin.addModelDefinition(internalised); + } } - //sBase.unsetPlugin(CompConstants.shortLabel); + compSBMLDocumentPlugin.unsetListOfExternalModelDefinitions(); + return result; } - } - private Model flattenModel(Model modelOfSubmodel) { - - flattenSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfReactions()); - flattenSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfCompartments()); - flattenSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfConstraints()); - flattenSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfEvents()); - - flattenSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfFunctionDefinitions()); - flattenSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfParameters()); - flattenSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfRules()); - flattenSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfSpecies()); - flattenSBaseList(modelOfSubmodel, modelOfSubmodel.getListOfUnitDefinitions()); - - return modelOfSubmodel; + /** + * Type of identifier used in replaced/deleted element references. + */ + private static enum IdType { + ID, + META_ID, + PORT, + UNIT_ID; } - private void addSBaseToModel(Model model, SBase sBase) { - - if (model != null && sBase != null) { - - sBase.removeFromParent(); - - if (sBase.getClass() == Reaction.class) { - model.addReaction((Reaction) sBase); - } else if (sBase.getClass() == Compartment.class) { - model.addCompartment((Compartment) sBase); - } else if (sBase.getClass() == Constraint.class) { - model.addConstraint((Constraint) sBase); - } else if (sBase.getClass() == Event.class) { - model.addEvent((Event) sBase); - } else if (sBase.getClass() == FunctionDefinition.class) { - model.addFunctionDefinition((FunctionDefinition) sBase); - } else if (sBase.getClass() == Parameter.class) { - model.addParameter((Parameter) sBase); - } else if (sBase.getClass() == Rule.class) { - model.addRule((Rule) sBase); - } else if (sBase.getClass() == Species.class) { - model.addSpecies((Species) sBase); - } else if (sBase.getClass() == UnitDefinition.class) { - model.addUnitDefinition((UnitDefinition) sBase); - } - + /** + * Information about a replaced element: where it lives, how it is referenced, + * and any conversion factor to apply. + */ + private static class ReplacedElementInfo implements Cloneable { + String id; // id/metaId/portId/unitId of the replacing element + String modelId; // id of the model that contains the replacing element + IdType idType; // how to interpret 'id' + String conversionFactor; // optional parameter id for conversion factor + List replacedElementPath; // path of submodel ids from root to the replaced element + + ReplacedElementInfo(String id, + String modelId, + IdType idType, + String conversionFactor, + List replacedElementPath) { + this.id = id; + this.modelId = modelId; + this.idType = idType; + this.conversionFactor = conversionFactor; + this.replacedElementPath = (replacedElementPath == null) + ? null + : new ArrayList(replacedElementPath); } + @Override + public ReplacedElementInfo clone() { + return new ReplacedElementInfo( + this.id, + this.modelId, + this.idType, + this.conversionFactor, + this.replacedElementPath == null + ? null + : new ArrayList(this.replacedElementPath)); + } } - - - /** - * Collects any {@link ExternalModelDefinition}s that might be contained in - * the given {@link SBMLDocument} and transfers them into local - * {@link ModelDefinition}s (recursively, if the external models themselves - * include external models; in that case, renaming may occur). - *
- * The given {@link SBMLDocument} need have its locationURI set! - *
- * Opaque URIs (URNs) will not be dealt with in any defined way, resolve them - * first (make sure all relevant externalModelDefinitions' source-attributes - * are URLs or relative paths) - * - * @param document - * an {@link SBMLDocument}, which might, but need not, contain - * {@link ExternalModelDefinition}s to be transferred into its local - * {@link ModelDefinition}s. The locationURI of the given document need - * be set ({@link SBMLDocument#isSetLocationURI})! - * @return a new {@link SBMLDocument} without {@link - * ExternalModelDefinition}s, but containing the same information as - * the given one - * @throws Exception - * if given document's locationURI is not set. Set it with - * {@link SBMLDocument#setLocationURI} - */ - public static SBMLDocument internaliseExternalModelDefinitions( - SBMLDocument document) throws Exception { - - if (!document.isSetLocationURI()) { - LOGGER.warning("Location URI is not set: " + document); - throw new Exception( - "document's locationURI need be set. But it was not."); - } - SBMLDocument result = document.clone(); // no side-effects intended - ArrayList usedIds = new ArrayList(); - if (result.isSetModel()) { - usedIds.add(result.getModel().getId()); - } - - CompSBMLDocumentPlugin compSBMLDocumentPlugin = - (CompSBMLDocumentPlugin) result.getExtension(CompConstants.shortLabel); - - // There is nothing to retrieve: - if (compSBMLDocumentPlugin == null || !compSBMLDocumentPlugin.isSetListOfExternalModelDefinitions()) { - return result; - } else { - /** For name-collision-avoidance */ - for (ExternalModelDefinition emd : compSBMLDocumentPlugin.getListOfExternalModelDefinitions()) { - usedIds.add(emd.getId()); - } - - for (ExternalModelDefinition emd : compSBMLDocumentPlugin.getListOfExternalModelDefinitions()) { - // general note: Be careful when using clone/cloning-constructors, they - // do not preserve parent-child-relations - Model referenced = emd.getReferencedModel(); - SBMLDocument referencedDocument = referenced.getSBMLDocument(); - SBMLDocument flattened = internaliseExternalModelDefinitions(referencedDocument); - // Guarantee: flattened does not contain any externalModelDefinitions, only local MDs - // (and main model) - // use this, and migrate the MDs into the current compSBMLDocumentPlugin - StringBuilder prefixBuilder = new StringBuilder(emd.getModelRef()); - /** For name-collision-avoidance */ - boolean contained = false; - do { - contained = false; - prefixBuilder.append("_"); - for (String id : usedIds) { - contained |= id.startsWith(prefixBuilder.toString()); - if(contained) { - break; - } - } - } while (contained); - String newPrefix = prefixBuilder.toString(); - - CompSBMLDocumentPlugin referencedDocumentPlugin = - (CompSBMLDocumentPlugin) flattened.getExtension( - CompConstants.shortLabel); - - ListOf workingList; - if(referencedDocumentPlugin == null) { - // This may happen, if the main model of a non-comp-file is referenced - workingList = new ListOf(); - workingList.setLevel(referenced.getLevel()); - workingList.setVersion(referenced.getVersion()); - } else { - workingList = referencedDocumentPlugin.getListOfModelDefinitions().clone(); - } - - // Check whether the main model is needed; Do not internalise it, if not necessary - boolean isMainReferenced = flattened.getModel().getId().equals(emd.getModelRef()); - for ( ModelDefinition md : workingList) { - if(isMainReferenced) { - break; - } - CompModelPlugin cmp = (CompModelPlugin) md.getExtension(CompConstants.shortLabel); - if (cmp != null) { - for (Submodel sm : cmp.getListOfSubmodels()) { - isMainReferenced |= flattened.getModel().getId().equals(sm.getModelRef()); - } - } - } - - if(isMainReferenced) { - ModelDefinition localisedMain = new ModelDefinition(flattened.getModel()); - workingList.add(0, localisedMain); - } - - - - - for (ModelDefinition md : workingList) { - ModelDefinition internalised = new ModelDefinition(md); - // i.e. current one is the one directly referenced => take referent's place - if(md.getId().equals(referenced.getId())) { - internalised.setId(emd.getId()); - } else { - internalised.setId(newPrefix + internalised.getId()); - } - - CompModelPlugin notYetInternalisedModelPlugin = - (CompModelPlugin) internalised.getExtension(CompConstants.shortLabel); - if(notYetInternalisedModelPlugin != null && notYetInternalisedModelPlugin.isSetListOfSubmodels()) { - for (Submodel sm : notYetInternalisedModelPlugin.getListOfSubmodels()) { - sm.setModelRef(newPrefix + sm.getModelRef()); - } - } - - compSBMLDocumentPlugin.addModelDefinition(internalised); - } - } - compSBMLDocumentPlugin.unsetListOfExternalModelDefinitions(); - return result; - } - } -} - - +} \ No newline at end of file diff --git a/extensions/comp/src/org/sbml/jsbml/ext/comp/util/CompFlatteningIDExpander.java b/extensions/comp/src/org/sbml/jsbml/ext/comp/util/CompFlatteningIDExpander.java new file mode 100644 index 000000000..d5c70ae38 --- /dev/null +++ b/extensions/comp/src/org/sbml/jsbml/ext/comp/util/CompFlatteningIDExpander.java @@ -0,0 +1,25 @@ +package org.sbml.jsbml.ext.comp.util; + +import org.sbml.jsbml.ListOf; +import org.sbml.jsbml.Model; +import org.sbml.jsbml.SBase; + +import java.util.List; + +public abstract class CompFlatteningIDExpander { + + public abstract String expandID(Model model, List curPath); + + static boolean checkIfListIDsContainPrefix(ListOf sBaseList, String prefix) { + + for (Object object : sBaseList) { + SBase sBase = (SBase) object; + if (sBase.getId().startsWith(prefix) || sBase.getMetaId().startsWith(prefix)) { + return true; + } + } + + return false; + } + +} \ No newline at end of file diff --git a/extensions/comp/src/org/sbml/jsbml/ext/comp/util/CompFlatteningIDExpanderTS.java b/extensions/comp/src/org/sbml/jsbml/ext/comp/util/CompFlatteningIDExpanderTS.java new file mode 100644 index 000000000..223463d24 --- /dev/null +++ b/extensions/comp/src/org/sbml/jsbml/ext/comp/util/CompFlatteningIDExpanderTS.java @@ -0,0 +1,37 @@ +package org.sbml.jsbml.ext.comp.util; + +import org.sbml.jsbml.Model; + +import java.util.List; + +public class CompFlatteningIDExpanderTS extends CompFlatteningIDExpander{ + + + @Override + public String expandID(Model model, List curPath) { + + StringBuilder subModelPrefix = new StringBuilder(); + for(int i=1; i < curPath.size(); i++) { + subModelPrefix.append(curPath.get(i) + "__"); + } + + // Check if any object in the submodel already contains subModelPrefix as a prefix, + // if so append an underscore and check again + while ( + checkIfListIDsContainPrefix(model.getListOfParameters(), subModelPrefix.toString()) + || checkIfListIDsContainPrefix(model.getListOfCompartments(), subModelPrefix.toString()) + || checkIfListIDsContainPrefix(model.getListOfConstraints(), subModelPrefix.toString()) + || checkIfListIDsContainPrefix(model.getListOfEvents(), subModelPrefix.toString()) + || checkIfListIDsContainPrefix(model.getListOfRules(), subModelPrefix.toString()) + || checkIfListIDsContainPrefix(model.getListOfFunctionDefinitions(), subModelPrefix.toString()) + || checkIfListIDsContainPrefix(model.getListOfSpecies(), subModelPrefix.toString()) + || checkIfListIDsContainPrefix(model.getListOfUnitDefinitions(), subModelPrefix.toString()) + || checkIfListIDsContainPrefix(model.getListOfReactions(), subModelPrefix.toString()) + ) { + subModelPrefix.append("_"); + } + + + return subModelPrefix.toString(); + } +} \ No newline at end of file diff --git a/extensions/fbc/src/org/sbml/jsbml/ext/fbc/GeneProduct.java b/extensions/fbc/src/org/sbml/jsbml/ext/fbc/GeneProduct.java index 534d2de4b..47871c706 100644 --- a/extensions/fbc/src/org/sbml/jsbml/ext/fbc/GeneProduct.java +++ b/extensions/fbc/src/org/sbml/jsbml/ext/fbc/GeneProduct.java @@ -112,12 +112,22 @@ public GeneProduct() { } /** - * Creates a new {@link GeneProduct} instance. + * Creates a new {@link GeneProduct} instance by copying all fields. * * @param nsb the instance to clone */ public GeneProduct(GeneProduct nsb) { super(nsb); + + // copy GeneProduct-specific fields + if (nsb.isSetLabel()) { + // use the setter to keep property-change semantics consistent + setLabel(nsb.getLabel()); + } + + if (nsb.isSetAssociatedSpecies()) { + setAssociatedSpecies(nsb.getAssociatedSpecies()); + } } /**