From fddf225726311b4a766894dfb3d163b2c65cf7eb Mon Sep 17 00:00:00 2001 From: lcottret <ludovic.cottret@inrae.fr> Date: Mon, 4 Oct 2021 15:50:49 +0200 Subject: [PATCH 1/9] change the test for charge reading To check if a double that can be casted to an integer (e.g. 3.0) can be set as charge value (integer) --- .../met4j_io/jsbml/reader/plugin/NotesParserTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/met4j-io/src/test/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/reader/plugin/NotesParserTest.java b/met4j-io/src/test/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/reader/plugin/NotesParserTest.java index f3948292c..9ce8834e9 100644 --- a/met4j-io/src/test/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/reader/plugin/NotesParserTest.java +++ b/met4j-io/src/test/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/reader/plugin/NotesParserTest.java @@ -252,7 +252,7 @@ public class NotesParserTest { Species m1 = model.createSpecies("m1"); String notesStr = "<body xmlns=\"http://www.w3.org/1999/xhtml\">\n" - + " <p>Attribut1 : value1</p><p>Formula: C5H403</p>" + "<p>CHARGE: 3</p>" + + " <p>Attribut1 : value1</p><p>Formula: C5H403</p>" + "<p>CHARGE: 3.0</p>" + "<p>INCHI: InChI=inchiCode</p>" + "<p>SMILES: smilesCode</p>\n" + " </body>"; m1.setNotes(notesStr); -- GitLab From bb684bf034f12cde5a0cde7a8ce77debb06bc363 Mon Sep 17 00:00:00 2001 From: lcottret <ludovic.cottret@inrae.fr> Date: Mon, 4 Oct 2021 16:21:50 +0200 Subject: [PATCH 2/9] code cleanup --- .../met4j_core/biodata/BioCompartment.java | 159 +++-- .../met4j_core/biodata/BioEntity.java | 589 +++++++++--------- .../met4j_core/biodata/BioEnzyme.java | 182 +++--- .../biodata/BioEnzymeParticipant.java | 75 ++- .../met4j_core/biodata/BioGene.java | 50 +- .../met4j_core/biodata/BioMetabolite.java | 4 - .../met4j_core/biodata/BioNetwork.java | 44 +- 7 files changed, 546 insertions(+), 557 deletions(-) diff --git a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioCompartment.java b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioCompartment.java index 2c622c937..14b41a5b3 100644 --- a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioCompartment.java +++ b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioCompartment.java @@ -39,91 +39,86 @@ package fr.inrae.toulouse.metexplore.met4j_core.biodata; import fr.inrae.toulouse.metexplore.met4j_core.biodata.collection.BioCollection; /** - * - * Biological cellular compartment, e.g. mitochondria, cytoplasm + * Biological cellular compartment, e.g. mitochondria, cytoplasm * * @author lcottret * @version $Id: $Id */ -public class BioCompartment extends BioPhysicalEntity{ - - - private BioCollection<BioEntity> components; - - /** - * Constructor from an id - * - * @param id : must be not null - */ - public BioCompartment(String id) { - super(id); - - components = new BioCollection<>(); - } - - /** - * - * Constructor from an id and a name - * - * @param id must be not null - * @param name the name of the compartment - */ - public BioCompartment(String id, String name) { - super(id, name); - - components = new BioCollection<>(); - } - - /** - * Copy of a compartment - * Do not copy the list of components - * - * @param compartment the original compartment - */ - public BioCompartment(BioCompartment compartment) { - super(compartment); - - components = new BioCollection<>(); - } - - - /** - * <p>getComponentsView.</p> - * - * @return an unmodifiable {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.collection.BioCollection} of the components - */ - public BioCollection<BioEntity> getComponentsView() { - return components.getView(); - } - - /** - * <p>Getter for the field <code>components</code>.</p> - * - * @return the components - */ - protected BioCollection<BioEntity> getComponents() { - return components; - } - - /** - * - * set the components - * - * @param components a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.collection.BioCollection} of {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioEntity} to set - */ - protected void setComponents(BioCollection<BioEntity> components) { - this.components = components; - } - - /** - * - * Add a component - * - * @param e : the {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioEntity} to add - */ - protected void addComponent(BioEntity e) - { - this.components.add(e); - } +public class BioCompartment extends BioPhysicalEntity { + + + private BioCollection<BioEntity> components; + + /** + * Constructor from an id + * + * @param id : must be not null + */ + public BioCompartment(String id) { + super(id); + + components = new BioCollection<>(); + } + + /** + * Constructor from an id and a name + * + * @param id must be not null + * @param name the name of the compartment + */ + public BioCompartment(String id, String name) { + super(id, name); + + components = new BioCollection<>(); + } + + /** + * Copy of a compartment + * Do not copy the list of components + * + * @param compartment the original compartment + */ + public BioCompartment(BioCompartment compartment) { + super(compartment); + + components = new BioCollection<>(); + } + + + /** + * <p>getComponentsView.</p> + * + * @return an unmodifiable {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.collection.BioCollection} of the components + */ + public BioCollection<BioEntity> getComponentsView() { + return components.getView(); + } + + /** + * <p>Getter for the field <code>components</code>.</p> + * + * @return the components + */ + protected BioCollection<BioEntity> getComponents() { + return components; + } + + /** + * set the components + * + * @param components a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.collection.BioCollection} of {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioEntity} to set + */ + protected void setComponents(BioCollection<BioEntity> components) { + this.components = components; + } + + /** + * Add a component + * + * @param e : the {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioEntity} to add + */ + protected void addComponent(BioEntity e) { + this.components.add(e); + } } diff --git a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEntity.java b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEntity.java index 461bc11db..5553b0441 100755 --- a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEntity.java +++ b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEntity.java @@ -47,298 +47,301 @@ import fr.inrae.toulouse.metexplore.met4j_core.utils.StringUtils; */ public abstract class BioEntity { - private final String id; - private String name; - private ArrayList<String> synonyms = new ArrayList<>(); - private String comment; - - private HashMap<String, Set<BioRef>> refs; - - private HashMap<String, Object> attributes; - - /** - * Constructor from an id and a name - * - * @param id String not null - * @param name String - */ - public BioEntity(String id, String name) { - - if (StringUtils.isVoid(id)) { - String newId = UUID.randomUUID().toString(); - this.id = newId; - System.err.println("Invalid id for building a BioEntity: "+id); - System.err.println("Creates a random unique id : "+newId); - } - else { - this.id = id; - } - this.setName(name); - this.setRefs(new HashMap<>()); - - attributes = new HashMap<>(); - } - - /** - * Constructor from an id - * - * @param id String not null - */ - public BioEntity(String id) { - this(id, id); - } - - /** - * Deep copy - * - * The refs and attributes are not deeply copied - * - * @param e the original bioentity - */ - public BioEntity(BioEntity e) - { - this.id = e.getId(); - this.name =e.getName(); - this.setSynonyms(new ArrayList<>(e.getSynonyms())); - this.setComment(e.getComment()); - this.setRefs(new HashMap<>(e.getRefs())); - this.setAttributes(new HashMap<>(e.getAttributes())); - } - - - /** {@inheritDoc} */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - BioEntity bioEntity = (BioEntity) o; - return id.equals(bioEntity.id); - } - - /** {@inheritDoc} */ - @Override - public int hashCode() { - return Objects.hashCode(id); - } - - /** - * Set the name of the entity - * - * @param n String - */ - public void setName(String n) { - this.name = n; - } - - /** - * Get the name of the entity - * - * @return the name of the entity - */ - public String getName() { - return this.name; - } - - /** - * Get the list of the synonyms - * - * @return an ArrayList of the synonyms of the entity - */ - public ArrayList<String> getSynonyms() { - return this.synonyms; - } - - /** - * Add a synonym in the list - * - * @param s synonym to add - */ - public void addSynonym(String s) { - this.synonyms.add(s); - } - - /** - * Set the comment on the entity - * - * @param c String - */ - public void setComment(String c) { - this.comment = c; - } - - /** - * Get the comment on the entity - * - * @return the comment on the entity - */ - public String getComment() { - return this.comment; - } - - /** - * <p>Getter for the field <code>id</code>.</p> - * - * @return Returns the id. - */ - public String getId() { - return id; - } - - /** - * <p>Setter for the field <code>synonyms</code>.</p> - * - * @param synonyms a {@link java.util.ArrayList} object. - */ - public void setSynonyms(ArrayList<String> synonyms) { - this.synonyms = synonyms; - } - - /** - * TODO : voir la coherence du code entre les deux methodes addRef. Celle ci - * devrait se terminer par un this.addRef(ref) - * - * @param dbName name of the database - * @param dbId id of the database - * @param confidenceLevel confidence level - * @param relation Type of relation - * @param origin Origin database - */ - public void addRef(String dbName, String dbId, int confidenceLevel, String relation, String origin) { - BioRef ref = new BioRef(origin, dbName, dbId, confidenceLevel); - ref.setLogicallink(relation); - this.addRef(ref); - } - - /** - * - * Add a reference - * - * @param ref a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioRef} - */ - public void addRef(BioRef ref) { - String dbName = ref.getDbName(); - if (!this.hasRef(ref)) { - if (this.refs.containsKey(dbName)) { - refs.get(dbName).add(ref); - } else { - Set<BioRef> refList = new HashSet<>(); - refList.add(ref); - this.refs.put(dbName, refList); - } - } - } - - /** - * Get all refs - * - * @return a {@link java.util.HashMap} for which the key is the database name and the values the set of {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioRef} - * associated to this database - */ - public HashMap<String, Set<BioRef>> getRefs() { - return this.refs; - } - - /** - * Get all refs associated to a database - * - * @param dbName the database name - * @return a {@link java.util.Set} of {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioRef} - */ - public Set<BioRef> getRefs(String dbName) { - return this.refs.getOrDefault(dbName, null); - } - - /** - * Check if the entity has a reference whose the database name is dbName and that contains a refence - * whose the id is refId - * - * @param dbName the database name - * @param refId the reference id - * @return true if the entity has the reference - */ - public boolean hasRef(String dbName, String refId) { - if (this.refs == null || !this.refs.containsKey(dbName)) { - return false; - } - for (BioRef ref : this.refs.get(dbName)) { - if (ref.getId().equals(refId)) { - return true; - } - } - return false; - } - - /** - * Check if the entity contains a reference - * - * @param unkRef a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioRef} - * @return true if the entity has the reference - */ - public boolean hasRef(BioRef unkRef) { - - if (this.refs == null || !this.refs.containsKey(unkRef.dbName)) { - return false; - } - for (BioRef ref : this.refs.get(unkRef.dbName)) { - if (ref.equals(unkRef)) { - return true; - } - } - return false; - } - - /** - * <p>Setter for the field <code>refs</code>.</p> - * - * @param refs a {@link java.util.HashMap} object. - */ - public void setRefs(HashMap<String, Set<BioRef>> refs) { - this.refs = refs; - } - - /** {@inheritDoc} */ - @Override - public String toString() { - return this.getId(); - } - - /** - * <p>Getter for the field <code>attributes</code>.</p> - * - * @return a {@link java.util.HashMap} object. - */ - public HashMap<String, Object> getAttributes() { - return attributes; - } - - /** - * <p>Setter for the field <code>attributes</code>.</p> - * - * @param attributes a {@link java.util.HashMap} object. - */ - public void setAttributes(HashMap<String, Object> attributes) { - this.attributes = attributes; - } - - /** - * <p>setAttribute.</p> - * - * @param key a {@link java.lang.String} object. - * @param value a {@link java.lang.Object} object. - * @return a {@link java.lang.Object} object. - */ - public Object setAttribute(String key, Object value) { - return attributes.put(key, value); - } - - /** - * <p>getAttribute.</p> - * - * @param key a {@link java.lang.String} object. - * @return a {@link java.lang.Object} object. - */ - public Object getAttribute(String key) { - return attributes.get(key); - } + private final String id; + private String name; + private ArrayList<String> synonyms = new ArrayList<>(); + private String comment; + + private HashMap<String, Set<BioRef>> refs; + + private HashMap<String, Object> attributes; + + /** + * Constructor from an id and a name + * + * @param id String not null + * @param name String + */ + public BioEntity(String id, String name) { + + if (StringUtils.isVoid(id)) { + String newId = UUID.randomUUID().toString(); + this.id = newId; + System.err.println("Invalid id for building a BioEntity: " + id); + System.err.println("Creates a random unique id : " + newId); + } else { + this.id = id; + } + this.setName(name); + this.setRefs(new HashMap<>()); + + attributes = new HashMap<>(); + } + + /** + * Constructor from an id + * + * @param id String not null + */ + public BioEntity(String id) { + this(id, id); + } + + /** + * Deep copy + * <p> + * The refs and attributes are not deeply copied + * + * @param e the original bioentity + */ + public BioEntity(BioEntity e) { + this.id = e.getId(); + this.name = e.getName(); + this.setSynonyms(new ArrayList<>(e.getSynonyms())); + this.setComment(e.getComment()); + this.setRefs(new HashMap<>(e.getRefs())); + this.setAttributes(new HashMap<>(e.getAttributes())); + } + + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BioEntity bioEntity = (BioEntity) o; + return id.equals(bioEntity.id); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + /** + * Set the name of the entity + * + * @param n String + */ + public void setName(String n) { + this.name = n; + } + + /** + * Get the name of the entity + * + * @return the name of the entity + */ + public String getName() { + return this.name; + } + + /** + * Get the list of the synonyms + * + * @return an ArrayList of the synonyms of the entity + */ + public ArrayList<String> getSynonyms() { + return this.synonyms; + } + + /** + * Add a synonym in the list + * + * @param s synonym to add + */ + public void addSynonym(String s) { + this.synonyms.add(s); + } + + /** + * Set the comment on the entity + * + * @param c String + */ + public void setComment(String c) { + this.comment = c; + } + + /** + * Get the comment on the entity + * + * @return the comment on the entity + */ + public String getComment() { + return this.comment; + } + + /** + * <p>Getter for the field <code>id</code>.</p> + * + * @return Returns the id. + */ + public String getId() { + return id; + } + + /** + * <p>Setter for the field <code>synonyms</code>.</p> + * + * @param synonyms a {@link java.util.ArrayList} object. + */ + public void setSynonyms(ArrayList<String> synonyms) { + this.synonyms = synonyms; + } + + /** + * TODO : voir la coherence du code entre les deux methodes addRef. Celle ci + * devrait se terminer par un this.addRef(ref) + * + * @param dbName name of the database + * @param dbId id of the database + * @param confidenceLevel confidence level + * @param relation Type of relation + * @param origin Origin database + */ + public void addRef(String dbName, String dbId, int confidenceLevel, String relation, String origin) { + BioRef ref = new BioRef(origin, dbName, dbId, confidenceLevel); + ref.setLogicallink(relation); + this.addRef(ref); + } + + /** + * Add a reference + * + * @param ref a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioRef} + */ + public void addRef(BioRef ref) { + String dbName = ref.getDbName(); + if (!this.hasRef(ref)) { + if (this.refs.containsKey(dbName)) { + refs.get(dbName).add(ref); + } else { + Set<BioRef> refList = new HashSet<>(); + refList.add(ref); + this.refs.put(dbName, refList); + } + } + } + + /** + * Get all refs + * + * @return a {@link java.util.HashMap} for which the key is the database name and the values the set of {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioRef} + * associated to this database + */ + public HashMap<String, Set<BioRef>> getRefs() { + return this.refs; + } + + /** + * Get all refs associated to a database + * + * @param dbName the database name + * @return a {@link java.util.Set} of {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioRef} + */ + public Set<BioRef> getRefs(String dbName) { + return this.refs.getOrDefault(dbName, null); + } + + /** + * Check if the entity has a reference whose the database name is dbName and that contains a refence + * whose the id is refId + * + * @param dbName the database name + * @param refId the reference id + * @return true if the entity has the reference + */ + public boolean hasRef(String dbName, String refId) { + if (this.refs == null || !this.refs.containsKey(dbName)) { + return false; + } + for (BioRef ref : this.refs.get(dbName)) { + if (ref.getId().equals(refId)) { + return true; + } + } + return false; + } + + /** + * Check if the entity contains a reference + * + * @param unkRef a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioRef} + * @return true if the entity has the reference + */ + public boolean hasRef(BioRef unkRef) { + + if (this.refs == null || !this.refs.containsKey(unkRef.dbName)) { + return false; + } + for (BioRef ref : this.refs.get(unkRef.dbName)) { + if (ref.equals(unkRef)) { + return true; + } + } + return false; + } + + /** + * <p>Setter for the field <code>refs</code>.</p> + * + * @param refs a {@link java.util.HashMap} object. + */ + public void setRefs(HashMap<String, Set<BioRef>> refs) { + this.refs = refs; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return this.getId(); + } + + /** + * <p>Getter for the field <code>attributes</code>.</p> + * + * @return a {@link java.util.HashMap} object. + */ + public HashMap<String, Object> getAttributes() { + return attributes; + } + + /** + * <p>Setter for the field <code>attributes</code>.</p> + * + * @param attributes a {@link java.util.HashMap} object. + */ + public void setAttributes(HashMap<String, Object> attributes) { + this.attributes = attributes; + } + + /** + * <p>setAttribute.</p> + * + * @param key a {@link java.lang.String} object. + * @param value a {@link java.lang.Object} object. + * @return a {@link java.lang.Object} object. + */ + public Object setAttribute(String key, Object value) { + return attributes.put(key, value); + } + + /** + * <p>getAttribute.</p> + * + * @param key a {@link java.lang.String} object. + * @return a {@link java.lang.Object} object. + */ + public Object getAttribute(String key) { + return attributes.get(key); + } } diff --git a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEnzyme.java b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEnzyme.java index 72f72f09d..6a23fc00a 100644 --- a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEnzyme.java +++ b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEnzyme.java @@ -45,98 +45,94 @@ import fr.inrae.toulouse.metexplore.met4j_core.biodata.collection.BioCollection; */ public class BioEnzyme extends BioPhysicalEntity { - private BioCollection<BioEnzymeParticipant> participants; - - /** - * Constructor - * - * @param id the id of the BioEnzyme - */ - public BioEnzyme(String id) { - super(id); - - participants = new BioCollection<>(); - } - - /** - * Constructor - * - * @param id the id of the BioEnzyme - * @param name the name of the BioEnzyme - */ - public BioEnzyme(String id, String name) { - super(id, name); - - participants = new BioCollection<>(); - } - - /** - * Copy of an enzyme - * Don't copy the participants - * - * @param e Original enzyme - */ - public BioEnzyme(BioEnzyme e) { - super(e); - - participants = new BioCollection<>(); - } - - /** - * <p>Getter for the field <code>participants</code>.</p> - * - * @return the participants - */ - protected BioCollection<BioEnzymeParticipant> getParticipants() { - return participants; - } - - /** - * <p>getParticipantsView.</p> - * - * @return a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.collection.BioCollection} object. - */ - public BioCollection<BioEnzymeParticipant> getParticipantsView() { - return participants.getView(); - } - - /** - * <p>Setter for the field <code>participants</code>.</p> - * - * @param participants the participants to set - */ - protected void setParticipants(BioCollection<BioEnzymeParticipant> participants) { - this.participants = participants; - } - - /** - * <p>addParticipant.</p> - * - * @param participant a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioEnzymeParticipant} object. - */ - protected void addParticipant(BioEnzymeParticipant participant) - { - this.participants.add(participant); - } - - /** - * Remove a participant from ifs physical entity - * - * @param e a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioPhysicalEntity} - */ - protected void removeParticipant(BioPhysicalEntity e) - { - - BioCollection<BioEnzymeParticipant> tmp = new BioCollection<>(this.participants); - - for(BioEnzymeParticipant p : tmp) - { - if(p.getPhysicalEntity().equals(e)) - { - this.participants.remove(p); - return; - } - } - } + private BioCollection<BioEnzymeParticipant> participants; + + /** + * Constructor + * + * @param id the id of the BioEnzyme + */ + public BioEnzyme(String id) { + super(id); + + participants = new BioCollection<>(); + } + + /** + * Constructor + * + * @param id the id of the BioEnzyme + * @param name the name of the BioEnzyme + */ + public BioEnzyme(String id, String name) { + super(id, name); + + participants = new BioCollection<>(); + } + + /** + * Copy of an enzyme + * Don't copy the participants + * + * @param e Original enzyme + */ + public BioEnzyme(BioEnzyme e) { + super(e); + + participants = new BioCollection<>(); + } + + /** + * <p>Getter for the field <code>participants</code>.</p> + * + * @return the participants + */ + protected BioCollection<BioEnzymeParticipant> getParticipants() { + return participants; + } + + /** + * <p>getParticipantsView.</p> + * + * @return a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.collection.BioCollection} object. + */ + public BioCollection<BioEnzymeParticipant> getParticipantsView() { + return participants.getView(); + } + + /** + * <p>Setter for the field <code>participants</code>.</p> + * + * @param participants the participants to set + */ + protected void setParticipants(BioCollection<BioEnzymeParticipant> participants) { + this.participants = participants; + } + + /** + * <p>addParticipant.</p> + * + * @param participant a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioEnzymeParticipant} object. + */ + protected void addParticipant(BioEnzymeParticipant participant) { + this.participants.add(participant); + } + + /** + * Remove a participant from ifs physical entity + * + * @param e a {@link fr.inrae.toulouse.metexplore.met4j_core.biodata.BioPhysicalEntity} + */ + protected void removeParticipant(BioPhysicalEntity e) { + + BioCollection<BioEnzymeParticipant> tmp = new BioCollection<>(this.participants); + + for (BioEnzymeParticipant p : tmp) { + if (p.getPhysicalEntity().equals(e)) { + this.participants.remove(p); + return; + } + } + } } diff --git a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEnzymeParticipant.java b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEnzymeParticipant.java index 3fb3070be..775d1be34 100644 --- a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEnzymeParticipant.java +++ b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioEnzymeParticipant.java @@ -48,54 +48,53 @@ import java.util.Locale; */ public class BioEnzymeParticipant extends BioParticipant { + /** + * Constructor + * + * @param physicalEntity the physical entity that is contained in the enzyme + * @param stoichiometry the number of unities of the physical entity + */ + public BioEnzymeParticipant(BioPhysicalEntity physicalEntity, Double stoichiometry) { + super(physicalEntity, stoichiometry); + } - /** - * Constructor - * - * @param physicalEntity the physical entity that is contained in the enzyme - * @param stoichiometry the number of unities of the physical entity - */ - public BioEnzymeParticipant(BioPhysicalEntity physicalEntity, Double stoichiometry) { - super(physicalEntity, stoichiometry); - } + /** + * Constructor + * + * @param physicalEntity the physical entity that is contained in the enzyme + * The stoichiometry is put to 1. + */ + public BioEnzymeParticipant(BioPhysicalEntity physicalEntity) { + super(physicalEntity, 1.0); + } - /** - * Constructor - * - * @param physicalEntity the physical entity that is contained in the enzyme - * The stoichiometry is put to 1. - */ - public BioEnzymeParticipant(BioPhysicalEntity physicalEntity) { - super(physicalEntity, 1.0); - } + /** + * {@inheritDoc} + */ + @Override + public String toString() { - /** {@inheritDoc} */ - @Override - public String toString() { + String quantityStr = ""; - String quantityStr = ""; + if (this.getQuantity() == Math.floor(this.getQuantity())) { - if (this.getQuantity() == Math.floor(this.getQuantity())) { + quantityStr += this.getQuantity().intValue(); - quantityStr += this.getQuantity().intValue(); + } else { + Locale currentLocale = Locale.getDefault(); - } else { - Locale currentLocale = Locale.getDefault(); + DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(currentLocale); + otherSymbols.setDecimalSeparator('.'); + otherSymbols.setGroupingSeparator('.'); - DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols(currentLocale); - otherSymbols.setDecimalSeparator('.'); - otherSymbols.setGroupingSeparator('.'); + NumberFormat formater = new DecimalFormat("#0.00", otherSymbols); + quantityStr = formater.format(this.getQuantity()); - NumberFormat formater = new DecimalFormat("#0.00", otherSymbols); - quantityStr = formater.format(this.getQuantity()); + } - } + String buffer = quantityStr + " " + this.getId(); - StringBuilder buffer = new StringBuilder(quantityStr); - buffer.append(" "); - buffer.append(this.getId()); + return buffer; - return buffer.toString(); - - } + } } diff --git a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioGene.java b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioGene.java index 27ce1de65..834bc8558 100755 --- a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioGene.java +++ b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioGene.java @@ -43,32 +43,32 @@ package fr.inrae.toulouse.metexplore.met4j_core.biodata; */ public class BioGene extends BioEntity { - /** - * Constructor - * - * @param id the id of the BioGene - */ - public BioGene(String id) { - super(id); - } + /** + * Constructor + * + * @param id the id of the BioGene + */ + public BioGene(String id) { + super(id); + } - /** - * Constructor - * - * @param id the id of the BioGene - * @param name the name of the BioGene - */ - public BioGene(String id, String name) { - super(id, name); - } + /** + * Constructor + * + * @param id the id of the BioGene + * @param name the name of the BioGene + */ + public BioGene(String id, String name) { + super(id, name); + } - /** - * Copy of a gene - * - * @param gene the original gene - */ - public BioGene(BioGene gene) { - super(gene); - } + /** + * Copy of a gene + * + * @param gene the original gene + */ + public BioGene(BioGene gene) { + super(gene); + } } diff --git a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioMetabolite.java b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioMetabolite.java index 29155b438..985703b3b 100644 --- a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioMetabolite.java +++ b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioMetabolite.java @@ -35,10 +35,6 @@ */ package fr.inrae.toulouse.metexplore.met4j_core.biodata; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Set; - /** * <p>BioMetabolite class.</p> * diff --git a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioNetwork.java b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioNetwork.java index 650357d9b..982c9f2db 100755 --- a/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioNetwork.java +++ b/met4j-core/src/main/java/fr/inrae/toulouse/metexplore/met4j_core/biodata/BioNetwork.java @@ -58,19 +58,19 @@ import java.util.stream.Collectors; */ public class BioNetwork extends BioEntity { - private BioCollection<BioPathway> pathways = new BioCollection<>(); + private final BioCollection<BioPathway> pathways = new BioCollection<>(); - private BioCollection<BioMetabolite> metabolites = new BioCollection<>(); + private final BioCollection<BioMetabolite> metabolites = new BioCollection<>(); - private BioCollection<BioProtein> proteins = new BioCollection<>(); + private final BioCollection<BioProtein> proteins = new BioCollection<>(); - private BioCollection<BioGene> genes = new BioCollection<>(); + private final BioCollection<BioGene> genes = new BioCollection<>(); - private BioCollection<BioReaction> reactions = new BioCollection<>(); + private final BioCollection<BioReaction> reactions = new BioCollection<>(); - private BioCollection<BioCompartment> compartments = new BioCollection<>(); + private final BioCollection<BioCompartment> compartments = new BioCollection<>(); - private BioCollection<BioEnzyme> enzymes = new BioCollection<>(); + private final BioCollection<BioEnzyme> enzymes = new BioCollection<>(); /** * <p>Constructor for BioNetwork.</p> @@ -179,7 +179,7 @@ public class BioNetwork extends BioEntity { if (protein.getGene() != null) { throw new IllegalArgumentException("[addProtein] The protein must not be affected to a gene before adding it to a BioNetwork"); } - this.proteins.add(protein); + this.proteins.add(protein); } /** @@ -194,6 +194,7 @@ public class BioNetwork extends BioEntity { /** * Add a reaction in a BioNetwork * The reaction must not be linked to reactants nor to enzymes + * * @param reaction : the {@link BioReaction} to add */ public void addReaction(BioReaction reaction) { @@ -202,7 +203,7 @@ public class BioNetwork extends BioEntity { throw new IllegalArgumentException("[addReaction] The reaction must not contain substrates before adding it to a BioNetwork"); } - if(reaction.getEnzymes().size() > 0) { + if (reaction.getEnzymes().size() > 0) { throw new IllegalArgumentException("[addReaction] The reaction must not be affected to a reaction before adding it to a BioNetwork"); } @@ -213,11 +214,12 @@ public class BioNetwork extends BioEntity { /** * Add a compartment in a BioNetwork * The compartment must be empty + * * @param compartment : the {@link BioCompartment} to add */ public void addCompartment(BioCompartment compartment) { - if(compartment.getComponents().size()>0) { + if (compartment.getComponents().size() > 0) { throw new IllegalArgumentException("[addCompartment] The compartment must be empty before adding it to a BioNetwork"); } @@ -227,11 +229,12 @@ public class BioNetwork extends BioEntity { /** * Add an enzyme in a BioNetwork * The enzyme must not contain participants + * * @param enzyme : the {@link BioEnzyme} to add */ public void addEnzyme(BioEnzyme enzyme) { - if(enzyme.getParticipants().size() > 0) { + if (enzyme.getParticipants().size() > 0) { throw new IllegalArgumentException("[addEnzyme] The enzyme must not contain participants before adding it to a BioNetwork"); } @@ -242,7 +245,7 @@ public class BioNetwork extends BioEntity { /** * Remove on cascade a BioEntity * - * @param e + * @param e the {@link BioEntity} to remove */ private void removeOnCascade(BioEntity e) { @@ -269,8 +272,6 @@ public class BioNetwork extends BioEntity { "BioEntity \"" + e.getClass().getSimpleName() + "\" not supported by BioNetwork"); } - e = null; - } /** @@ -686,15 +687,15 @@ public class BioNetwork extends BioEntity { throw new IllegalArgumentException("Compartment " + localisation.getId() + " not in the network"); } - if (!this.metabolites.contains(reactant.getPhysicalEntity())) { + if (!this.metabolites.contains(reactant.getMetabolite())) { throw new IllegalArgumentException( - "Metabolite " + reactant.getPhysicalEntity().getId() + " not in the network"); + "Metabolite " + reactant.getMetabolite().getId() + " not in the network"); } // The metabolite must be connected to the compartment - if (!localisation.getComponents().contains(reactant.getPhysicalEntity())) { + if (!localisation.getComponents().contains(reactant.getMetabolite())) { throw new IllegalArgumentException( - "Metabolite " + reactant.getPhysicalEntity().getId() + " not in the compartment"); + "Metabolite " + reactant.getMetabolite().getId() + " not in the compartment"); } // The network must contain the reaction @@ -1196,8 +1197,8 @@ public class BioNetwork extends BioEntity { } /** - * @param m - * @param isSubstrate + * @param m a {@link BioMetabolite} + * @param isSubstrate a {@link Boolean} indicating if the metabolite is a substrate or a product * @return a {@link BioCollection} of {@link BioReaction} */ private BioCollection<BioReaction> getReactionsFromSubstrateOrProduct(BioMetabolite m, Boolean isSubstrate) { @@ -1209,7 +1210,7 @@ public class BioNetwork extends BioEntity { BioCollection<BioReaction> reactions = new BioCollection<>(); this.getReactionsView().forEach(r -> { - boolean flag = false; + boolean flag; BioCollection<BioMetabolite> lefts = r.getLeftsView(); BioCollection<BioMetabolite> rights = r.getRightsView(); @@ -1351,7 +1352,6 @@ public class BioNetwork extends BioEntity { /** * @param reaction a {@link BioReaction} * @return a {@link BioCollection} of {@link BioGene} - * @return a {@link BioCollection} of {@link BioGene} * @throws IllegalArgumentException if reaction is not present in the network */ private BioCollection<BioGene> getGenesFromReaction(BioReaction reaction) { -- GitLab From 90bcd014fee4ca0a2ece5d9f76eb502a5e276c27 Mon Sep 17 00:00:00 2001 From: cfrainay <clement.frainay@inrae.fr> Date: Thu, 21 Oct 2021 10:29:08 +0200 Subject: [PATCH 3/9] [graph][doc] improve edge merger description --- .../met4j_graph/computation/transform/EdgeMerger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/met4j-graph/src/main/java/fr/inrae/toulouse/metexplore/met4j_graph/computation/transform/EdgeMerger.java b/met4j-graph/src/main/java/fr/inrae/toulouse/metexplore/met4j_graph/computation/transform/EdgeMerger.java index 5520884e6..316f20161 100644 --- a/met4j-graph/src/main/java/fr/inrae/toulouse/metexplore/met4j_graph/computation/transform/EdgeMerger.java +++ b/met4j-graph/src/main/java/fr/inrae/toulouse/metexplore/met4j_graph/computation/transform/EdgeMerger.java @@ -51,7 +51,7 @@ import fr.inrae.toulouse.metexplore.met4j_core.biodata.BioEntity; public class EdgeMerger { /** - * Merge edges sharing same source and target, create new one with concatenated labels + * Merge edges sharing same source and target, create new one with concatenated labels and summed weights * * @param g a graph. * @param <V> vertex class -- GitLab From 4d30e8a420224efc0d185ec8e396c6c1e81b5584 Mon Sep 17 00:00:00 2001 From: cfrainay <clement.frainay@inrae.fr> Date: Thu, 21 Oct 2021 10:34:03 +0200 Subject: [PATCH 4/9] [chem-utils][toolbox] create class to analyse chemical formula inorganic compound detection from SideCompoundScan app has been moved to this class. Also check if formula is valid (check string structure and atom symbols), and if entity represent a compound class --- .../met4j_chemUtils/FormulaParser.java | 59 +++++++++++++++ .../chemicalSimilarity/FormulaParserTest.java | 73 +++++++++++++++++++ .../networkAnalysis/SideCompoundsScan.java | 40 +++++++--- 3 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 met4j-chemUtils/src/main/java/fr/inrae/toulouse/metexplore/met4j_chemUtils/FormulaParser.java create mode 100644 met4j-chemUtils/src/test/java/fr/inrae/toulouse/metexplore/met4j_chemUtils/chemicalSimilarity/FormulaParserTest.java diff --git a/met4j-chemUtils/src/main/java/fr/inrae/toulouse/metexplore/met4j_chemUtils/FormulaParser.java b/met4j-chemUtils/src/main/java/fr/inrae/toulouse/metexplore/met4j_chemUtils/FormulaParser.java new file mode 100644 index 000000000..0cdf9b5a6 --- /dev/null +++ b/met4j-chemUtils/src/main/java/fr/inrae/toulouse/metexplore/met4j_chemUtils/FormulaParser.java @@ -0,0 +1,59 @@ +package fr.inrae.toulouse.metexplore.met4j_chemUtils; + +import fr.inrae.toulouse.metexplore.met4j_core.utils.StringUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Provides utility methods to analyse formula's content + */ +public class FormulaParser { + + String formula; + Pattern inorgaRegex; + Pattern groupMatch; + Pattern genericFormula = Pattern.compile("^([\\*\\(\\)A-Z][a-z]*\\d*)+$");; + List<String> atoms = Arrays.asList("H","B","C","N","O","F","Na","Mg","Al","Si","P","S","Cl","K","Ca","Cr","Mn","Fe","Co","Cu","Zn","Se","Mo","Cd","Sn","I"); + + + public FormulaParser(String formula){ + if(StringUtils.isVoid(formula) || !genericFormula.matcher(formula).find()) throw new IllegalArgumentException("Unable to parse formula"); + this.formula=formula; + } + + /** + * @return true if the formula string contains only known atom type found in living organism. + */ + public boolean hasValidAtomSymbols(){ + if(hasUndefinedPart()) return false; + Matcher atomMatch = Pattern.compile("([A-Z][a-z]*)").matcher(formula); + while (atomMatch.find()){ + String atom = atomMatch.group(1); + if(!atoms.contains(atom)) return false; + } + return true; + } + + /** + * @return false if the formula contains R- or *. + */ + public boolean hasUndefinedPart(){ + if(groupMatch==null) groupMatch = Pattern.compile(".*([RX\\*]-?).*"); + return (groupMatch.matcher(formula).matches()); + } + + /** + * Detect inorganic compounds. Inorganic compound is an ill-defined concept, sometimes defined as compound lacking C-C or C-H bonds. Since chemical structure is rarely available " + + * in SBML model beyond chemical formula, we use a less restrictive criterion by flagging compound with one or no carbons. This cover most inorganic compounds, but include few compounds" + + * such as methane usually considered as organic. + * @return false if the formula contains a radical or more than one carbon atom. + */ + public boolean isExpectedInorganic(){ + if(inorgaRegex==null) inorgaRegex = Pattern.compile(".*(R\\-?|\\*|C\\d).*"); + return (!inorgaRegex.matcher(formula).matches()); + } + +} diff --git a/met4j-chemUtils/src/test/java/fr/inrae/toulouse/metexplore/met4j_chemUtils/chemicalSimilarity/FormulaParserTest.java b/met4j-chemUtils/src/test/java/fr/inrae/toulouse/metexplore/met4j_chemUtils/chemicalSimilarity/FormulaParserTest.java new file mode 100644 index 000000000..685aac3b6 --- /dev/null +++ b/met4j-chemUtils/src/test/java/fr/inrae/toulouse/metexplore/met4j_chemUtils/chemicalSimilarity/FormulaParserTest.java @@ -0,0 +1,73 @@ +package fr.inrae.toulouse.metexplore.met4j_chemUtils.chemicalSimilarity; + +import fr.inrae.toulouse.metexplore.met4j_chemUtils.FormulaParser; +import org.junit.Assert; +import org.junit.Test; + +public class FormulaParserTest { + + public String f1 = "NaHCO3"; + public String f2 = "RCO2H"; + public String f3 = "*OH"; + public String f4 = "C8H10N4O2"; + public String f5 = "Ca(NO3)2"; + public String f6 = "Cz(NJ3)2"; + + public String f7 = "lardonsfumes"; + public String f8 = "f02mu14$$$"; + public String f9 = "Ca(N O3)2"; + public String f10 = " "; + + @Test + public void testFormulaParser(){ + try{FormulaParser fp = new FormulaParser(f1);} + catch (IllegalArgumentException e){Assert.fail("Valid formula structure rejected");} + try{FormulaParser fp = new FormulaParser(f2);} + catch (IllegalArgumentException e){Assert.fail("Valid formula structure rejected");} + try{FormulaParser fp = new FormulaParser(f3);} + catch (IllegalArgumentException e){Assert.fail("Valid formula structure rejected");} + try{FormulaParser fp = new FormulaParser(f4);} + catch (IllegalArgumentException e){Assert.fail("Valid formula structure rejected");} + try{FormulaParser fp = new FormulaParser(f5);} + catch (IllegalArgumentException e){Assert.fail("Valid formula structure rejected");} + try{FormulaParser fp = new FormulaParser(f7);Assert.fail("Malformed formula passed");} + catch (IllegalArgumentException e){} + try{FormulaParser fp = new FormulaParser(f8);Assert.fail("Malformed formula passed");} + catch (IllegalArgumentException e){} + try{FormulaParser fp = new FormulaParser(f9);Assert.fail("Malformed formula passed");} + catch (IllegalArgumentException e){} + try{FormulaParser fp = new FormulaParser(f10);Assert.fail("Malformed formula passed");} + catch (IllegalArgumentException e){} + } + + @Test + public void testHasUndefinedPart(){ + Assert.assertFalse((new FormulaParser(f1)).hasUndefinedPart()); + Assert.assertTrue((new FormulaParser(f2)).hasUndefinedPart()); + Assert.assertTrue((new FormulaParser(f3)).hasUndefinedPart()); + Assert.assertFalse((new FormulaParser(f4)).hasUndefinedPart()); + Assert.assertFalse((new FormulaParser(f5)).hasUndefinedPart()); + Assert.assertFalse((new FormulaParser(f6)).hasUndefinedPart()); + } + + @Test + public void testHasValidAtomSymbols(){ + Assert.assertTrue((new FormulaParser(f1)).hasValidAtomSymbols()); + Assert.assertFalse((new FormulaParser(f2)).hasValidAtomSymbols()); + Assert.assertFalse((new FormulaParser(f3)).hasValidAtomSymbols()); + Assert.assertTrue((new FormulaParser(f4)).hasValidAtomSymbols()); + Assert.assertTrue((new FormulaParser(f5)).hasValidAtomSymbols()); + Assert.assertFalse((new FormulaParser(f6)).hasValidAtomSymbols()); + } + + @Test + public void testIsExpectedInorganic(){ + Assert.assertTrue((new FormulaParser(f1)).isExpectedInorganic()); + Assert.assertFalse((new FormulaParser(f2)).isExpectedInorganic()); + Assert.assertFalse((new FormulaParser(f3)).isExpectedInorganic()); + Assert.assertFalse((new FormulaParser(f4)).isExpectedInorganic()); + Assert.assertTrue((new FormulaParser(f5)).isExpectedInorganic()); + Assert.assertTrue((new FormulaParser(f6)).isExpectedInorganic()); + } + +} diff --git a/met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/SideCompoundsScan.java b/met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/SideCompoundsScan.java index 0f2e2dffe..22be98f91 100644 --- a/met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/SideCompoundsScan.java +++ b/met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/SideCompoundsScan.java @@ -1,5 +1,6 @@ package fr.inrae.toulouse.metexplore.met4j_toolbox.networkAnalysis; +import fr.inrae.toulouse.metexplore.met4j_chemUtils.FormulaParser; import fr.inrae.toulouse.metexplore.met4j_core.biodata.BioMetabolite; import fr.inrae.toulouse.metexplore.met4j_core.biodata.BioNetwork; import fr.inrae.toulouse.metexplore.met4j_graph.computation.connect.weighting.DefaultWeightPolicy; @@ -43,6 +44,9 @@ public class SideCompoundsScan extends AbstractMet4jApplication { @Option(name = "-cc", aliases = {"--noCarbonSkeleton"}, usage = "flag as side compound any compounds with less than 2 carbons in formula") public Boolean flagInorganic = false; + @Option(name = "-uf", aliases = {"--undefinedFormula"}, usage = "flag as side compound any compounds with no valid chemical formula") + public Boolean flagNoFormula = false; + @Option(name = "-er", aliases = {"--edgeRedundancy"}, usage = "flag as side compound any compound with a number of redundancy in incident edges (parallel edges connecting to the same neighbor) above the given threshold") public double parallelEdge = Double.NaN; @@ -92,9 +96,6 @@ public class SideCompoundsScan extends AbstractMet4jApplication { dt = degreeStats.getPercentile(degreePrecentile); } - //formula regex - Pattern regex = Pattern.compile(".*(R[^a-z]|C\\d).*"); - //header Boolean reportValue = (!noReportValue); if (reportValue) { @@ -102,6 +103,7 @@ public class SideCompoundsScan extends AbstractMet4jApplication { l.append("\tDEGREE"); if (!Double.isNaN(parallelEdge)) l.append("\tINCIDENT_PARALLEL_EDGES"); if (flagInorganic) l.append("\tNO_CARBON_BOND"); + if (flagNoFormula) l.append("\tVALID_CHEMICAL"); l.append("\tIS_SIDE\n"); fw.write(l.toString()); } @@ -132,18 +134,31 @@ public class SideCompoundsScan extends AbstractMet4jApplication { } //check formula - if (flagInorganic) { + if (flagInorganic || flagNoFormula) { String formula = v.getChemicalFormula(); String inorganic = "?"; - if (!StringUtils.isVoid(formula) && !formula.equals("NA") && !formula.equals("*")) { - if (regex.matcher(formula).matches()) { - inorganic = "false"; - } else { - inorganic = "true"; + String validForumla = "true"; + try{ + FormulaParser fp = new FormulaParser(formula); + if(flagInorganic){ + if(fp.isExpectedInorganic()){ + inorganic = "true"; + side = true; + }else{ + inorganic = "false"; + } + } + }catch(IllegalArgumentException e){ + if(flagNoFormula){ + validForumla = "false"; side = true; } + + } + if (reportValue){ + if(flagInorganic) l.append("\t" + inorganic); + if(flagNoFormula) l.append("\t" + validForumla); } - if (reportValue) l.append("\t" + inorganic); } if (reportValue) l.append("\t" + side); @@ -182,7 +197,10 @@ public class SideCompoundsScan extends AbstractMet4jApplication { "- *Carbon Count*: Metabolic \"waste\", or degradation end-product such as ammonia or carbon dioxide are usually considered as side compounds.\n" + "Most of them are inorganic compound, another ill-defined concept, sometimes defined as compound lacking C-C or C-H bonds. Since chemical structure is rarely available " + "in SBML model beyond chemical formula, we use a less restrictive criterion by flagging compound with one or no carbons. This cover most inorganic compounds, but include few compounds" + - " such as methane usually considered as organic. "; + " such as methane usually considered as organic. " + + "- *Chemical Formula*: Metabolic network often contains 'artifacts' that serve modelling purpose (to define a composite objective function for example). " + + "Such entities can be considered as 'side entities'. Since they are not actual chemical compounds, they can be detected by their lack of valid chemical formula. " + + "However, this can also flag main compounds with erroneous or missing annotation."; } @Override -- GitLab From b22ddf5b37bd3fb5c0d1d0ae5549c8e440b2f0fd Mon Sep 17 00:00:00 2001 From: cfrainay <clement.frainay@inrae.fr> Date: Thu, 21 Oct 2021 15:14:03 +0200 Subject: [PATCH 5/9] [toolbox] workflow for carbon skeleton network creation create from SBML and GSAM output the recommended networks for metaborank and path search. --- .../networkAnalysis/CarbonSkeletonNet.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/CarbonSkeletonNet.java diff --git a/met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/CarbonSkeletonNet.java b/met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/CarbonSkeletonNet.java new file mode 100644 index 000000000..5f9d790c6 --- /dev/null +++ b/met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/CarbonSkeletonNet.java @@ -0,0 +1,187 @@ +package fr.inrae.toulouse.metexplore.met4j_toolbox.networkAnalysis; + +import fr.inrae.toulouse.metexplore.met4j_chemUtils.FormulaParser; +import fr.inrae.toulouse.metexplore.met4j_core.biodata.BioMetabolite; +import fr.inrae.toulouse.metexplore.met4j_core.biodata.BioNetwork; +import fr.inrae.toulouse.metexplore.met4j_graph.computation.connect.weighting.AtomMappingWeightPolicy; +import fr.inrae.toulouse.metexplore.met4j_graph.computation.connect.weighting.ReactionProbabilityWeight; +import fr.inrae.toulouse.metexplore.met4j_graph.computation.transform.EdgeMerger; +import fr.inrae.toulouse.metexplore.met4j_graph.computation.transform.VertexContraction; +import fr.inrae.toulouse.metexplore.met4j_graph.computation.utils.ComputeAdjacencyMatrix; +import fr.inrae.toulouse.metexplore.met4j_graph.core.compound.CompoundGraph; +import fr.inrae.toulouse.metexplore.met4j_graph.io.Bionetwork2BioGraph; +import fr.inrae.toulouse.metexplore.met4j_graph.io.ExportGraph; +import fr.inrae.toulouse.metexplore.met4j_io.jsbml.reader.JsbmlReader; +import fr.inrae.toulouse.metexplore.met4j_io.jsbml.reader.Met4jSbmlReaderException; +import fr.inrae.toulouse.metexplore.met4j_io.jsbml.reader.plugin.FBCParser; +import fr.inrae.toulouse.metexplore.met4j_io.jsbml.reader.plugin.GroupPathwayParser; +import fr.inrae.toulouse.metexplore.met4j_io.jsbml.reader.plugin.NotesParser; +import fr.inrae.toulouse.metexplore.met4j_io.jsbml.reader.plugin.PackageParser; +import fr.inrae.toulouse.metexplore.met4j_mathUtils.matrix.ExportMatrix; +import fr.inrae.toulouse.metexplore.met4j_toolbox.generic.AbstractMet4jApplication; +import org.kohsuke.args4j.Option; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; + +public class CarbonSkeletonNet extends AbstractMet4jApplication { + + @Option(name = "-s", usage = "input SBML file", required = true) + public String inputPath = null; + + @Option(name = "-g", usage = "input GSAM file", required = true) + public String inputAAM = null; + + @Option(name = "-o", usage = "output Graph file", required = true) + public String outputPath = null; + + @Option(name = "-ks", aliases = {"--keepSingleCarbon"}, usage = "keep edges involving single-carbon compounds, such as CO2 (requires formulas in SBML)", required = false) + public boolean keepSingleCarbon = false; + + @Option(name = "-mc", aliases = {"--nocomp"}, usage = "merge compartments (requires unique compound names that are consistent across compartments)", required = false) + public boolean mergeComp = false; + + @Option(name = "-me", aliases = {"--simple"}, usage = "merge parallel edges to produce a simple graph", required = false) + public boolean mergeEdges = false; + + @Option(name = "-ri", aliases = {"--removeIsolatedNodes"}, usage = "remove isolated nodes", required = false) + public boolean removeIsolated = false; + + @Option(name = "-un", aliases = {"--undirected"}, usage = "create as undirected", required = false) + public boolean undirected = false; + + @Option(name = "-tp", aliases = {"--transitionproba"}, usage = "set transition probability as weight", required = false) + public boolean computeWeight = false; + + @Option(name = "-am", aliases = {"--asmatrix"}, usage = "export as matrix (implies simple graph conversion). Default export as GML file", required = false) + public boolean asMatrix = false; + + public static void main(String[] args) throws IOException, Met4jSbmlReaderException { + + CarbonSkeletonNet app = new CarbonSkeletonNet(); + + app.parseArguments(args); + + app.run(); + + } + + + public void run() throws IOException, Met4jSbmlReaderException { + System.out.print("Reading SBML..."); + JsbmlReader reader = new JsbmlReader(this.inputPath, false); + ArrayList<PackageParser> pkgs = new ArrayList<>(Arrays.asList( + new NotesParser(false), new FBCParser(), new GroupPathwayParser())); + BioNetwork network = reader.read(pkgs); + System.out.println(" Done."); + + + System.out.print("Buildinig Network..."); + Bionetwork2BioGraph builder = new Bionetwork2BioGraph(network); + CompoundGraph graph = builder.getCompoundGraph(); + System.out.println(" Done."); + + System.out.print("Processing atom mappings..."); + AtomMappingWeightPolicy wp = new AtomMappingWeightPolicy() + .fromNumberOfConservedCarbons(inputAAM) + .binarize() + .removeEdgeWithoutMapping() + .removeEdgesWithoutConservedCarbon(); + + wp.setWeight(graph); + System.out.println(" Done."); + + //invert graph as undirected (copy edge weight to reversed edge) + if(undirected){ + System.out.print("Create Undirected..."); + graph.asUndirected(); + System.out.println(" Done."); + } + + //merge compartment + if(mergeComp){ + System.out.print("Merging compartments..."); + VertexContraction vc = new VertexContraction(); + graph = vc.decompartmentalize(graph, new VertexContraction.MapByName()); + System.out.println(" Done."); + } + + //remove single-carbon compounds + if(!keepSingleCarbon){ + System.out.println("Skip compounds with less than two carbons detected..."); + HashSet<BioMetabolite> toRemove = new HashSet<>(); + for(BioMetabolite n : graph.vertexSet()) { + if (!graph.edgesOf(n).isEmpty()) { + String formula = n.getChemicalFormula(); + try { + FormulaParser fp = new FormulaParser(formula); + if (fp.isExpectedInorganic()) { + graph.removeAllEdges(graph.edgesOf(n)); + System.out.println("\tdisconnecting " + n.getName()); + } + } catch (IllegalArgumentException e) { + System.out.println("\tcan't define structure of " + n.getName()); + } + } + } + System.out.println(" Done."); + } + + //remove isolated nodes + if(removeIsolated){ + System.out.println("Remove isolated nodes..."); + HashSet<BioMetabolite> nodes = new HashSet<>(graph.vertexSet()); + graph.removeIsolatedNodes(); + nodes.removeAll(graph.vertexSet()); + for(BioMetabolite n : nodes){ + System.out.println("\tremoving " + n.getName()); + } + System.out.println(" Done."); + } + + //compute transitions probability from weights + if(computeWeight) { + System.out.print("Compute transition matrix..."); + ReactionProbabilityWeight wp2 = new ReactionProbabilityWeight(); + wp2.setWeight(graph); + System.out.println(" Done."); + } + + //merge parallel edges + if(mergeEdges){ + System.out.print("Merging edges..."); + EdgeMerger.mergeEdgesWithOverride(graph); + System.out.println(" Done."); + } + + System.out.print("Exporting..."); + if(asMatrix){ + ComputeAdjacencyMatrix adjBuilder = new ComputeAdjacencyMatrix(graph); + if (undirected) adjBuilder.asUndirected(); + if(!computeWeight) adjBuilder.parallelEdgeWeightsHandling((u, v) -> Math.max(u,v)); + ExportMatrix.toCSV(this.outputPath,adjBuilder.getadjacencyMatrix()); + }else{ + ExportGraph.toGmlWithAttributes(graph, this.outputPath, true); + } + System.out.println(" Done."); + return; + } + + @Override + public String getLabel() {return this.getClass().getSimpleName();} + + @Override + public String getLongDescription() { + return "Metabolic networks used for quantitative analysis often contain links that are irrelevant for graph-based structural analysis. For example, inclusion of side compounds or modelling artifacts such as 'biomass' nodes." + + " Focusing on links between compounds that share parts of their carbon skeleton allows to avoid many transitions involving side compounds, and removes entities without defined chemical structure. " + + "This app produce a Carbon Skeleton Network relevant for graph-based analysis of metabolism, in GML or matrix format, from a SBML and an GSAM atom mapping file. " + + "GSAM (see https://forgemia.inra.fr/metexplore/gsam) perform atom mapping at genome-scale level using the Reaction Decoder Tool (https://github.com/asad/ReactionDecoder) and allows to compute the number of conserved atoms of a given type between reactants." + + "This app also enable Markov-chain based analysis of metabolic networks by computing reaction-normalized transition probabilities on the Carbon Skeleton Network."; + } + + @Override + public String getShortDescription() {return "Create a carbon skeleton graph representation of a SBML file content, using GSAM atom-mapping file (see https://forgemia.inra.fr/metexplore/gsam)";} +} + -- GitLab From 2002f955f6edd23c546eacb8d73f5102ec1d0a3d Mon Sep 17 00:00:00 2001 From: cfrainay <clement.frainay@inrae.fr> Date: Tue, 26 Oct 2021 15:12:24 +0200 Subject: [PATCH 6/9] [graph] Add convenience comparators for edge merger --- .../computation/transform/EdgeMerger.java | 37 +++++++++++++++++- .../met4j_graph/TestEdgeMerger.java | 39 ++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/met4j-graph/src/main/java/fr/inrae/toulouse/metexplore/met4j_graph/computation/transform/EdgeMerger.java b/met4j-graph/src/main/java/fr/inrae/toulouse/metexplore/met4j_graph/computation/transform/EdgeMerger.java index 316f20161..8eaaf1be9 100644 --- a/met4j-graph/src/main/java/fr/inrae/toulouse/metexplore/met4j_graph/computation/transform/EdgeMerger.java +++ b/met4j-graph/src/main/java/fr/inrae/toulouse/metexplore/met4j_graph/computation/transform/EdgeMerger.java @@ -37,6 +37,7 @@ package fr.inrae.toulouse.metexplore.met4j_graph.computation.transform; import java.util.*; +import com.google.common.primitives.Doubles; import fr.inrae.toulouse.metexplore.met4j_graph.core.BioGraph; import fr.inrae.toulouse.metexplore.met4j_graph.core.Edge; import fr.inrae.toulouse.metexplore.met4j_graph.core.parallel.MergedGraph; @@ -206,6 +207,38 @@ public class EdgeMerger { return mergedG; } - - + + + public static <V extends BioEntity,E extends Edge<V>, G extends BioGraph<V,E>> Comparator<E> alphabeticalOrder(){ + return new Comparator<E>() { + @Override + public int compare(E e1, E e2) { + return e1.toString().compareTo(e2.toString()); + } + }; + } + public static <V extends BioEntity,E extends Edge<V>, G extends BioGraph<V,E>> Comparator<E> highWeightFirst(G g){ + return new Comparator<E>() { + @Override + public int compare(E e1, E e2) { + return Doubles.compare( + g.getEdgeWeight(e2), + g.getEdgeWeight(e1) + ); + } + }; + } + public static <V extends BioEntity,E extends Edge<V>, G extends BioGraph<V,E>> Comparator<E> lowWeightFirst(G g){ + return new Comparator<E>() { + @Override + public int compare(E e1, E e2) { + return Doubles.compare( + g.getEdgeWeight(e1), + g.getEdgeWeight(e2) + ); + } + }; + } + + } diff --git a/met4j-graph/src/test/java/fr/inrae/toulouse/metexplore/met4j_graph/TestEdgeMerger.java b/met4j-graph/src/test/java/fr/inrae/toulouse/metexplore/met4j_graph/TestEdgeMerger.java index 2b21bef87..2dcf39805 100644 --- a/met4j-graph/src/test/java/fr/inrae/toulouse/metexplore/met4j_graph/TestEdgeMerger.java +++ b/met4j-graph/src/test/java/fr/inrae/toulouse/metexplore/met4j_graph/TestEdgeMerger.java @@ -42,6 +42,7 @@ package fr.inrae.toulouse.metexplore.met4j_graph; import static org.junit.Assert.*; import fr.inrae.toulouse.metexplore.met4j_graph.computation.transform.EdgeMerger; +import fr.inrae.toulouse.metexplore.met4j_graph.core.Edge; import fr.inrae.toulouse.metexplore.met4j_graph.core.compound.CompoundGraph; import fr.inrae.toulouse.metexplore.met4j_graph.core.compound.ReactionEdge; import fr.inrae.toulouse.metexplore.met4j_graph.core.parallel.MergedGraph; @@ -141,7 +142,7 @@ public class TestEdgeMerger { bb1 = new ReactionEdge(b1,b2,r7);g.addEdge(b1,b2, bb1); bb2 = new ReactionEdge(b2,b1,r7);g.addEdge(b2,b1, bb2); } - + @Test public void testMergeEdgeOverride() { CompoundGraph g2 = new CompoundGraph(g); @@ -175,4 +176,40 @@ public class TestEdgeMerger { } + @Test + public void testMergeEdgeComparator1() { + CompoundGraph g2 = new CompoundGraph(g); + EdgeMerger.mergeEdgesWithOverride(g2, EdgeMerger.alphabeticalOrder()); + g2.setEdgeWeight(ab1,2.0); + g2.setEdgeWeight(ab2,5.0); + g2.setEdgeWeight(ab3,0.5); + Assert.assertEquals("Error while creating the initial graph", 10, g.edgeSet().size()); + Assert.assertEquals("Wrong final number of edges", 8, g2.edgeSet().size()); + Assert.assertTrue("Wrong edge kept", g2.containsEdge(ab1)); + } + + @Test + public void testMergeEdgeComparator2() { + CompoundGraph g2 = new CompoundGraph(g); + g2.setEdgeWeight(ab1,2.0); + g2.setEdgeWeight(ab2,5.0); + g2.setEdgeWeight(ab3,0.5); + EdgeMerger.mergeEdgesWithOverride(g2, EdgeMerger.highWeightFirst(g2)); + Assert.assertEquals("Error while creating the initial graph", 10, g.edgeSet().size()); + Assert.assertEquals("Wrong final number of edges", 8, g2.edgeSet().size()); + Assert.assertTrue("Wrong edge kept", g2.containsEdge(ab2)); + } + + @Test + public void testMergeEdgeComparator3() { + CompoundGraph g2 = new CompoundGraph(g); + g2.setEdgeWeight(ab1,2.0); + g2.setEdgeWeight(ab2,5.0); + g2.setEdgeWeight(ab3,0.5); + EdgeMerger.mergeEdgesWithOverride(g2, EdgeMerger.lowWeightFirst(g2)); + Assert.assertEquals("Error while creating the initial graph", 10, g.edgeSet().size()); + Assert.assertEquals("Wrong final number of edges", 8, g2.edgeSet().size()); + Assert.assertTrue("Wrong edge kept", g2.containsEdge(ab3)); + } + } -- GitLab From ea856968b2ab470cc4affa4c709883c2057e603e Mon Sep 17 00:00:00 2001 From: cfrainay <clement.frainay@inrae.fr> Date: Wed, 27 Oct 2021 11:28:32 +0200 Subject: [PATCH 7/9] fix bug in CSN matrix export undirected weighted Carbon Skeleton Network still have different weights for different directions of the same edge, and thus should be considered directed for the matrix representation. --- .../met4j_toolbox/networkAnalysis/CarbonSkeletonNet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/CarbonSkeletonNet.java b/met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/CarbonSkeletonNet.java index 5f9d790c6..c0a17d40e 100644 --- a/met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/CarbonSkeletonNet.java +++ b/met4j-toolbox/src/main/java/fr/inrae/toulouse/metexplore/met4j_toolbox/networkAnalysis/CarbonSkeletonNet.java @@ -156,10 +156,10 @@ public class CarbonSkeletonNet extends AbstractMet4jApplication { System.out.println(" Done."); } + //export graph System.out.print("Exporting..."); if(asMatrix){ ComputeAdjacencyMatrix adjBuilder = new ComputeAdjacencyMatrix(graph); - if (undirected) adjBuilder.asUndirected(); if(!computeWeight) adjBuilder.parallelEdgeWeightsHandling((u, v) -> Math.max(u,v)); ExportMatrix.toCSV(this.outputPath,adjBuilder.getadjacencyMatrix()); }else{ -- GitLab From 0428d8e2b98b9cb7da306a47ad8fb26f152ecf88 Mon Sep 17 00:00:00 2001 From: cfrainay <clement.frainay@inrae.fr> Date: Thu, 28 Oct 2021 16:18:45 +0200 Subject: [PATCH 8/9] [io] handle indentifiers.org urls with https --- .../met4j_io/jsbml/reader/plugin/AnnotationParser.java | 2 +- .../met4j_io/jsbml/writer/plugin/AnnotationWriter.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/met4j-io/src/main/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/reader/plugin/AnnotationParser.java b/met4j-io/src/main/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/reader/plugin/AnnotationParser.java index 4335f7160..10e11f24d 100644 --- a/met4j-io/src/main/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/reader/plugin/AnnotationParser.java +++ b/met4j-io/src/main/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/reader/plugin/AnnotationParser.java @@ -98,7 +98,7 @@ public class AnnotationParser implements PackageParser, AdditionalDataTag, Reade * </ul> * The first parenthesis group is */ - public static final String defaultAnnotationPattern = "http://identifiers.org/([^/]+)/(.*)"; + public static final String defaultAnnotationPattern = "https?://identifiers.org/([^/]+)/(.*)"; /** * Constructor diff --git a/met4j-io/src/main/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/writer/plugin/AnnotationWriter.java b/met4j-io/src/main/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/writer/plugin/AnnotationWriter.java index b24b425da..1e971c1c1 100644 --- a/met4j-io/src/main/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/writer/plugin/AnnotationWriter.java +++ b/met4j-io/src/main/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/writer/plugin/AnnotationWriter.java @@ -81,7 +81,7 @@ public class AnnotationWriter implements PackageWriter, AdditionalDataTag { /** * The default URL pattern for the annotations */ - public static final String DEFAULT_URL_BASE = "http://identifiers.org/"; + public static final String DEFAULT_URL_BASE = "https://identifiers.org/"; /** * The user defined URL pattern for the annotations -- GitLab From 8e596c2ad77ebba5c3cb6fcfae2fdc24e9820e3e Mon Sep 17 00:00:00 2001 From: cfrainay <clement.frainay@inrae.fr> Date: Fri, 29 Oct 2021 10:15:15 +0200 Subject: [PATCH 9/9] [io] update test default annot build https://identifiers.org uris --- .../writer/plugin/AnnotationWriterTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/met4j-io/src/test/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/writer/plugin/AnnotationWriterTest.java b/met4j-io/src/test/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/writer/plugin/AnnotationWriterTest.java index 1aca694fe..0c8acfbb1 100644 --- a/met4j-io/src/test/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/writer/plugin/AnnotationWriterTest.java +++ b/met4j-io/src/test/java/fr/inrae/toulouse/metexplore/met4j_io/jsbml/writer/plugin/AnnotationWriterTest.java @@ -189,7 +189,7 @@ public class AnnotationWriterTest { assertNotNull(s1); - assertTrue(s1.getAnnotationString().contains("http://identifiers.org/kegg.reaction/id2")); + assertTrue(s1.getAnnotationString().contains("https://identifiers.org/kegg.reaction/id2")); } @@ -213,7 +213,7 @@ public class AnnotationWriterTest { assertNotNull(s1); assertTrue(s1.getAnnotationString() - .contains("<rdf:li rdf:resource=\"http://identifiers.org/pubchem.compound/puchem123\"/>")); + .contains("<rdf:li rdf:resource=\"https://identifiers.org/pubchem.compound/puchem123\"/>")); } @@ -225,10 +225,10 @@ public class AnnotationWriterTest { assertNotNull(s1); assertTrue(s1.getAnnotationString() - .contains("<rdf:li rdf:resource=\"http://identifiers.org/pubmed/23456\"/>")); + .contains("<rdf:li rdf:resource=\"https://identifiers.org/pubmed/23456\"/>")); assertTrue(s1.getAnnotationString() - .contains("<rdf:li rdf:resource=\"http://identifiers.org/pubmed/12345\"/>")); + .contains("<rdf:li rdf:resource=\"https://identifiers.org/pubmed/12345\"/>")); } @@ -239,7 +239,7 @@ public class AnnotationWriterTest { assertNotNull(r1); - assertTrue(r1.getAnnotationString().contains("http://identifiers.org/kegg.reaction/id1")); + assertTrue(r1.getAnnotationString().contains("https://identifiers.org/kegg.reaction/id1")); } @@ -250,7 +250,7 @@ public class AnnotationWriterTest { assertNotNull(r1); - assertTrue(r1.getAnnotationString().contains("http://identifiers.org/ec-code/1.2.3.4")); + assertTrue(r1.getAnnotationString().contains("https://identifiers.org/ec-code/1.2.3.4")); } @@ -262,10 +262,10 @@ public class AnnotationWriterTest { assertNotNull(r1); assertTrue(r1.getAnnotationString() - .contains("<rdf:li rdf:resource=\"http://identifiers.org/pubmed/23456\"/>")); + .contains("<rdf:li rdf:resource=\"https://identifiers.org/pubmed/23456\"/>")); assertTrue(r1.getAnnotationString() - .contains("<rdf:li rdf:resource=\"http://identifiers.org/pubmed/12345\"/>")); + .contains("<rdf:li rdf:resource=\"https://identifiers.org/pubmed/12345\"/>")); } -- GitLab