diff --git a/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/Ontology.java b/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/Ontology.java index 35f6b72..5acadf9 100644 --- a/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/Ontology.java +++ b/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/Ontology.java @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.BiConsumer; import java.util.zip.GZIPInputStream; /** @@ -34,6 +35,7 @@ private final OWLOntology owlOntology; private final OWLOntologyManager ontologyManager; + private final OWLDataFactory owlDataFactory; private Ontology(OntologySourceData sourceData, Concurrency concurrency) { Objects.requireNonNull(sourceData, "Unable to create Ontology without data sources."); @@ -41,6 +43,7 @@ private Ontology(OntologySourceData sourceData, Concurrency concurrency) { this.curieUtil = new CurieUtil(sourceData.getCuries()); this.ontologyManager = createOntologyManager(concurrency); this.owlOntology = createEmptyOntology(ontologyManager); + this.owlDataFactory = ontologyManager.getOWLDataFactory(); loadOwlOntology(); } @@ -81,12 +84,29 @@ public CurieUtil getCurieUtil() { return curieUtil; } + /** + * @param curie + * @return + */ + public OWLClass getOWLClass(String curie) { + return owlDataFactory.getOWLClass(toIri(curie)); + } + + public OWLNamedIndividual getOWLNamedIndividual(String curie) { + return owlDataFactory.getOWLNamedIndividual(toIri(curie)); + } + + public String toCurie(IRI iri) { + String iriString = iri.toString(); + return curieUtil.getCurie(iriString).orElse(iriString); + } + private void loadOwlOntology() { //Order matters here - don't change it. mergeOntologies(sourceData.getOntologies()); mergeOntologies(sourceData.getDataOntologies()); loadDataFromTsv(sourceData.getDataTsvs()); - loadDataFromPairwiseMappings(sourceData.getPairwiseMappings()); + loadDataFromMap(sourceData.getIndividuals()); logger.info("Ontology loaded"); } @@ -191,8 +211,20 @@ private OWLOntology loadDataFromTsvGzip(String path) { return owlOntology; } - private void loadDataFromPairwiseMappings(Map pairwiseMappings) { - pairwiseMappings.forEach(this::addInstanceOf); + private void loadDataFromMap(Map> individuals) { + if(!individuals.isEmpty()){ + logger.info("Loading individuals from map"); + } + //e.g. 'ORPHA:710': ['HP:0000194','HP:0000218','HP:0000262','HP:0000303','HP:0000316'] + individuals.forEach(addIndividual()); + } + + private BiConsumer> addIndividual() { + return (individual, annotations) -> { + for (String curie : annotations) { + addInstanceOf(individual, curie); + } + }; } private void loadLineIntoDataOntology(String line) { @@ -204,15 +236,18 @@ private void loadLineIntoDataOntology(String line) { } private void addInstanceOf(String individual, String ontologyClass) { -// logger.info("Adding axiom " + individual + ": " + ontologyClass); - OWLDataFactory owlDataFactory = ontologyManager.getOWLDataFactory(); - OWLClass owlClass = owlDataFactory.getOWLClass(toIri(ontologyClass)); - OWLNamedIndividual owlNamedIndividual = owlDataFactory.getOWLNamedIndividual(toIri(individual)); - OWLClassAssertionAxiom axiom = owlDataFactory.getOWLClassAssertionAxiom(owlClass, owlNamedIndividual); - addAxiom(axiom); + Objects.requireNonNull(individual, "Individual identifier cannot be null. Check your input."); + Objects.requireNonNull(ontologyClass, "Class identifier(s) cannot be null. Check your input."); + if (!ontologyClass.isEmpty()) { +// logger.info("Adding axiom " + individual + ": " + ontologyClass); + OWLClass owlClass = getOWLClass(ontologyClass); + OWLNamedIndividual owlNamedIndividual = getOWLNamedIndividual(individual); + OWLClassAssertionAxiom axiom = owlDataFactory.getOWLClassAssertionAxiom(owlClass, owlNamedIndividual); + addAxiom(axiom); + } } - private IRI toIri(String id) { + IRI toIri(String id) { return IRI.create(curieUtil.getIri(id).orElse(id)); } diff --git a/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/OntologySourceData.java b/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/OntologySourceData.java index 8e47937..876ff85 100644 --- a/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/OntologySourceData.java +++ b/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/OntologySourceData.java @@ -20,16 +20,17 @@ private final List dataOntologies; private final Map curies; + //TODO: shouldn't this be individualsTsvs? private final List dataTsvs; - //TODO: add these so people can programmatically add individual assertions - private final Map pairwiseMappings; + private final Map> individuals; + //TODO - labels? private OntologySourceData(Builder builder) { this.ontologies = distinctImmutableListOf(builder.ontologies); this.dataOntologies = distinctImmutableListOf(builder.dataOntologies); this.curies = ImmutableMap.copyOf(builder.curies); this.dataTsvs = distinctImmutableListOf(builder.dataTsvs); - this.pairwiseMappings = ImmutableMap.copyOf(builder.pairwiseMappings); + this.individuals = ImmutableMap.copyOf(builder.individuals); } private ImmutableList distinctImmutableListOf(List list) { @@ -52,8 +53,8 @@ private OntologySourceData(Builder builder) { return dataTsvs; } - public Map getPairwiseMappings() { - return pairwiseMappings; + public Map> getIndividuals() { + return individuals; } @Override @@ -65,12 +66,12 @@ public boolean equals(Object o) { Objects.equals(dataOntologies, that.dataOntologies) && Objects.equals(curies, that.curies) && Objects.equals(dataTsvs, that.dataTsvs) && - Objects.equals(pairwiseMappings, that.pairwiseMappings); + Objects.equals(individuals, that.individuals); } @Override public int hashCode() { - return Objects.hash(ontologies, dataOntologies, curies, dataTsvs, pairwiseMappings); + return Objects.hash(ontologies, dataOntologies, curies, dataTsvs, individuals); } @Override @@ -80,7 +81,7 @@ public String toString() { ", dataOntologies=" + dataOntologies + ", curies=" + curies + ", dataTsvs=" + dataTsvs + - ", pairwiseMappings=" + pairwiseMappings + + ", individuals=" + individuals + '}'; } @@ -94,7 +95,7 @@ public static Builder builder() { //Curies need to be supplied if people are adding data using TSV files or pairwise mappings using curies. private Map curies = Collections.emptyMap(); private List dataTsvs = new ArrayList<>(); - private Map pairwiseMappings = Collections.emptyMap(); + private Map> individuals = new HashMap<>(); private Builder(){ //use the static method. @@ -176,6 +177,11 @@ public Builder dataTsv(Collection paths) { return this; } + public Builder data(Map> mappings) { + individuals.putAll(mappings); + return this; + } + public OntologySourceData build() { if(ontologies.isEmpty()) { throw new OntologyLoadException("No ontology defined."); @@ -187,7 +193,7 @@ public OntologySourceData build() { } private boolean hasNonOntologyData() { - return !dataTsvs.isEmpty() || !pairwiseMappings.isEmpty(); + return !dataTsvs.isEmpty() || !individuals.isEmpty(); } } } diff --git a/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/OwlKnowledgeBase.java b/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/OwlKnowledgeBase.java index cf206de..8b40f02 100644 --- a/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/OwlKnowledgeBase.java +++ b/owlsim-core/src/main/java/org/monarchinitiative/owlsim/io/OwlKnowledgeBase.java @@ -57,7 +57,6 @@ public Loader loadCuries(Map curies) { * @param file */ public Loader loadOntology(File file) { - Math.random(); sourceDataBuilder.ontology(file); return this; } @@ -118,6 +117,11 @@ public Loader loadDataFromTsv(Collection paths) { return this; } + public Loader loadDataFromMap(Map> data) { + sourceDataBuilder.data(data); + return this; + } + /** * Creates an {@link OWLOntologyManager} that is configured with the standard parsers and storers and provides * locking for concurrent access (default). diff --git a/owlsim-core/src/test/java/org/monarchinitiative/owlsim/io/OntologySourceDataTest.java b/owlsim-core/src/test/java/org/monarchinitiative/owlsim/io/OntologySourceDataTest.java index c01b275..b392205 100644 --- a/owlsim-core/src/test/java/org/monarchinitiative/owlsim/io/OntologySourceDataTest.java +++ b/owlsim-core/src/test/java/org/monarchinitiative/owlsim/io/OntologySourceDataTest.java @@ -2,7 +2,9 @@ import org.junit.Test; +import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; /** @@ -18,12 +20,37 @@ public void testItAll() { curies.put("MP", "http://purl.obolibrary.org/obo/MP_"); curies.put("NCBITaxon", "http://purl.obolibrary.org/obo/NCBITaxon_"); + Map> individuals = new LinkedHashMap<>(); + individuals.put("ORPHA:710", Arrays.asList("HP:0000194", + "HP:0000218", + "HP:0000262", + "HP:0000303", + "HP:0000316", + "HP:0000322", + "HP:0000324", + "HP:0000348", + "HP:0000431", + "HP:0000470", + "HP:0000508", + "HP:0001156", + "HP:0001385", + "HP:0003307", + "HP:0004209", + "HP:0004322", + "HP:0005048", + "HP:0006101", + "HP:0009773", + "HP:0010669", + "HP:0011304", + "HP:0012368", "")); + OntologySourceData sourceData = OntologySourceData.builder() .curies(curies) .ontology("src/test/resources/ontologies/mammal.obo.gz") .dataTsv("src/test/resources/data/gene2taxon.tsv.gz") .dataTsv("src/test/resources/data/mouse-pheno.assocs.gz") .dataTsv("src/test/resources/data/human-pheno.assocs.gz") + .data(individuals) .build(); System.out.println(sourceData); diff --git a/owlsim-core/src/test/java/org/monarchinitiative/owlsim/io/OntologyTest.java b/owlsim-core/src/test/java/org/monarchinitiative/owlsim/io/OntologyTest.java new file mode 100644 index 0000000..a1c6e30 --- /dev/null +++ b/owlsim-core/src/test/java/org/monarchinitiative/owlsim/io/OntologyTest.java @@ -0,0 +1,171 @@ +package org.monarchinitiative.owlsim.io; + +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.semanticweb.owlapi.model.*; +import org.semanticweb.owlapi.model.parameters.Imports; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +/** + * @author Jules Jacobsen + */ +public class OntologyTest { + + private Map getHpAndNameCurieMap() { + Map curies = new HashMap<>(); + curies.put("HP", "http://purl.obolibrary.org/obo/HP_"); + curies.put("MINION", "http://despicableme.wikia.com/wiki/"); + return curies; + } + + private Ontology getBobOnlyOntology() { + Map curies = getHpAndNameCurieMap(); + + Map> data = new HashMap<>(); + data.put("MINION:Bob", toSet("HP:0000952;HP:0001090;HP:0008857;HP:0001006;HP:0006101;HP:0001100")); + + OntologySourceData sourceData = OntologySourceData.builder() + .ontology("src/test/resources/species-no-individuals.owl") + .curies(curies) + .data(data) + .build(); + + return Ontology.load(sourceData); + } + + private Set toSet(String input) { + return Arrays.stream(input.split("[,;]")).map(String::trim).collect(ImmutableSet.toImmutableSet()); + } + + private Set getAxiomsForIndividual(Ontology ontology, String individual) { + OWLOntology owlOntology = ontology.getOwlOntology(); + OWLOntologyManager manager = owlOntology.getOWLOntologyManager(); + OWLDataFactory owlDataFactory = manager.getOWLDataFactory(); + OWLNamedIndividual owlNamedIndividual = owlDataFactory.getOWLNamedIndividual(ontology.toIri(individual)); + return owlOntology.getAxioms(owlNamedIndividual, Imports.INCLUDED); + } + + @Test(expected = NullPointerException.class) + public void testAddNullIndividuals() { + Map curies = getHpAndNameCurieMap(); + + Map> data = new HashMap<>(); + data.put(null, toSet("HP:0000952,HP:0001090,HP:0004322,HP:0001006,HP:0006101")); + + OntologySourceData sourceData = OntologySourceData.builder() + .ontology("src/test/resources/species-no-individuals.owl") + .curies(curies) + .data(data) + .build(); + + Ontology.load(sourceData); + } + + @Test(expected = NullPointerException.class) + public void testAddNullClasses() { + Map curies = getHpAndNameCurieMap(); + + Map> data = new HashMap<>(); + data.put("MINION:Kevin", null); + + OntologySourceData sourceData = OntologySourceData.builder() + .ontology("src/test/resources/species-no-individuals.owl") + .curies(curies) + .data(data) + .build(); + + Ontology.load(sourceData); + } + + @Test + public void testAddIndividuals() { + Map curies = getHpAndNameCurieMap(); + + Map> data = new HashMap<>(); + //Concatenated - should be able to parse TSV, CSV and trim whitespace + data.put("MINION:Kevin", toSet("HP:0000952,HP:0001090,HP:0004322,HP:0001006,HP:0006101")); + data.put("MINION:Bob", toSet("HP:0000952;HP:0001090;HP:0008857;HP:0001006;HP:0006101;HP:0001100")); + //mixed + data.put("MINION:Stuart", toSet("HP:0000952; HP:0001090;HP:0008857 ;HP:0001006, HP:0006101,HP:0100754, HP:0009914 ")); + + OntologySourceData sourceData = OntologySourceData.builder() + .ontology("src/test/resources/species-no-individuals.owl") + .curies(curies) + .data(data) + .build(); + + Ontology ontology = Ontology.load(sourceData); + + Set kevinAxioms = getAxiomsForIndividual(ontology, "MINION:Kevin"); + kevinAxioms.forEach(axiom -> { + Set classes = axiom.getClassesInSignature(); + classes.forEach(ontologyClass -> System.out.printf("Individual: %s Class: %s%n", axiom.getIndividualsInSignature(), ontologyClass)); + }); + + assertEquals(5, kevinAxioms.size()); + Set bobAxioms = getAxiomsForIndividual(ontology, "MINION:Bob"); + assertEquals(6, bobAxioms.size()); + Set stuartAxioms = getAxiomsForIndividual(ontology, "MINION:Stuart"); + assertEquals(7, stuartAxioms.size()); + } + + @Test + public void testIriConversion() { + Map curies = getHpAndNameCurieMap(); + + OntologySourceData sourceData = OntologySourceData.builder() + .ontology("src/test/resources/species-no-individuals.owl") + .curies(curies) + .build(); + + Ontology ontology = Ontology.load(sourceData); + + IRI bobNotFound = ontology.toIri("Bob"); + System.out.println(bobNotFound); + assertEquals("Bob", bobNotFound.toString()); + assertEquals(ontology.toCurie(bobNotFound), "Bob"); + + IRI bobFound = ontology.toIri("MINION:Bob"); + System.out.println(bobFound); + assertEquals("http://despicableme.wikia.com/wiki/Bob", bobFound.toString()); + assertEquals(ontology.toCurie(bobFound), "MINION:Bob"); + + } + + @Test + public void testGetOwlClass() { + Ontology ontology = getBobOnlyOntology(); + + OWLClass hpClass = ontology.getOWLClass("HP:0000952"); + System.out.println(hpClass); + assertEquals(hpClass.toString(), ""); + + OWLClass bobFound = ontology.getOWLClass("MINION:Bob"); + System.out.println(bobFound); + assertEquals(bobFound.toString(), ""); + } + + @Test + public void testGetOwlNamedIndividual() { + Ontology ontology = getBobOnlyOntology(); + + OWLNamedIndividual notFound = ontology.getOWLNamedIndividual("wibble"); + System.out.println(notFound); + assertEquals(notFound.toString(), ""); + + OWLNamedIndividual hpTerm = ontology.getOWLNamedIndividual("HP:0000952"); + System.out.println(hpTerm); + assertEquals(hpTerm.toString(), ""); + + OWLNamedIndividual bob = ontology.getOWLNamedIndividual("MINION:Bob"); + System.out.println(bob); + assertEquals(bob.toString(), ""); + } + +} \ No newline at end of file diff --git a/owlsim-core/src/test/java/org/monarchinitiative/owlsim/io/OwlKnowledgeBaseTest.java b/owlsim-core/src/test/java/org/monarchinitiative/owlsim/io/OwlKnowledgeBaseTest.java index 57fa1ec..e25c085 100644 --- a/owlsim-core/src/test/java/org/monarchinitiative/owlsim/io/OwlKnowledgeBaseTest.java +++ b/owlsim-core/src/test/java/org/monarchinitiative/owlsim/io/OwlKnowledgeBaseTest.java @@ -1,13 +1,12 @@ package org.monarchinitiative.owlsim.io; +import com.google.common.collect.Sets; import org.junit.Test; import org.monarchinitiative.owlsim.kb.BMKnowledgeBase; import uk.ac.manchester.cs.jfact.JFactFactory; import java.io.File; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; import static org.junit.Assert.assertEquals; @@ -103,8 +102,8 @@ public void loadDataFromOntologies() { public void loadDataFromTsv() { BMKnowledgeBase bmKnowledgeBase = OwlKnowledgeBase.loader() .loadCuries(curies()) - .loadOntology("src/test/resources/ontologies/mammal.obo.gz") - .loadDataFromTsv("src/test/resources/data/human-pheno.assocs.gz") + .loadOntology("src/test/resources/species-no-individuals.owl") + .loadDataFromTsv("src/test/resources/data/species-individuals.tsv") .createKnowledgeBase(); } @@ -142,6 +141,28 @@ public void loadDataFromTsvCollection() { } @Test + public void loadDataFromMap() { + Map curies = new HashMap<>(); + curies.put("HP", "http://purl.obolibrary.org/obo/HP_"); + curies.put("NAME:", "http://x.org/NAME_"); + + Map> data = new HashMap<>(); + data.put("NAME:Kevin", Arrays.asList("HP:0000952","HP:0001090","HP:0004322","HP:0001006","HP:0006101","HP:0009914")); + data.put("NAME:Bob", Arrays.asList("HP:0000952","HP:0001090","HP:0004322","HP:0001006","HP:0006101","HP:0001100")); + data.put("NAME:Stuart", Arrays.asList("HP:0000952","HP:0001090","HP:0004322","HP:0001006","HP:0006101","HP:0100754")); + + BMKnowledgeBase knowledgeBase = BMKnowledgeBase.owlLoader() + .loadOntology("src/test/resources/species-no-individuals.owl") + .loadCuries(curies) + .loadDataFromMap(data) + .createKnowledgeBase(); + + System.out.println("knowledgebase individuals are: " + knowledgeBase.getIndividualIdsInSignature()); + System.out.println(knowledgeBase.getEntity("NAME:Kevin")); + assertEquals(Sets.newHashSet("NAME:Kevin", "NAME:Bob", "NAME:Stuart"), knowledgeBase.getIndividualIdsInSignature()); + } + + @Test public void testLoadOwlFromFileLocationWithStandardOntologyManager() throws Exception { BMKnowledgeBase bmKnowledgeBase = OwlKnowledgeBase.loader() .useStandardOntologyManager() diff --git a/owlsim-core/src/test/resources/data/species-individuals.tsv b/owlsim-core/src/test/resources/data/species-individuals.tsv new file mode 100644 index 0000000..6faf0c4 --- /dev/null +++ b/owlsim-core/src/test/resources/data/species-individuals.tsv @@ -0,0 +1,24 @@ +Sharktopus shark, octopus +SquidMan squid, human +SpiderMan spider, human +DolphinCat dolphin, cat +DogMouse dog, mouse +SpongeBob poriferan, human +JellyChimp jelly, chimp +GreatOldOne amphibian, cephalopod, human +PorpoiseMarmosetFly porpoise, marmoset, fruitfly +SuperMammal human, dolphin, chimp, dog, cat, rat, mouse +ProtoSpider spider +ProtoHuman human +ProtoChimp chimp +ProtoWhale cetacean +ProtoMammal mammal +ProtoShark shark +TranslucentTrait jelly, salp +CuteTrait cat, dolphin, koala +WeirdTrait platypus, cephalopod, hammerhead +ScaryTrait tarantula, octopus, shark +BigTrait blueWhale, greatWhiteShark +SmallTrait rat, mouse, insect, xenopus +SwimmingTrait shark, cetacean, teleost +StupidTrait poriferan, tunicate, jelly diff --git a/owlsim-core/src/test/resources/species-no-individuals.owl b/owlsim-core/src/test/resources/species-no-individuals.owl new file mode 100644 index 0000000..2e08973 --- /dev/null +++ b/owlsim-core/src/test/resources/species-no-individuals.owl @@ -0,0 +1,165 @@ +Prefix: : + +Ontology: + +Class: organism +Class: animal + SubClassOf: organism + Class: bilaterian + SubClassOf: animal + + Class: protostome + SubClassOf: bilaterian + Class: arthropod + SubClassOf: protostome + Class: spider + SubClassOf: arthropod + Class: tarantula + SubClassOf: spider + + Class: insect + SubClassOf: arthropod + Class: dipteran + SubClassOf: insect + Class: fruitfly + SubClassOf: dipteran + Class: hymenopteran + SubClassOf: insect + Class: bee + SubClassOf: hymenopteran + Class: ant + SubClassOf: hymenopteran + + Class: cephalopod + SubClassOf: protostome + Class: squid + SubClassOf: cephalopod + Class: octopus + SubClassOf: cephalopod + Class: nautilus + SubClassOf: cephalopod + Class: cuttlefish + SubClassOf: cephalopod + + Class: deuterostome + SubClassOf: bilaterian + Class: echinoderm + SubClassOf: deuterostome + Class: starfish + SubClassOf: echinoderm + Class: urchin + SubClassOf: echinoderm + Class: chordate + SubClassOf: deuterostome + Class: vertebrate + SubClassOf: chordate + Class: cartilaginousFish + SubClassOf: vertebrate + Class: shark + SubClassOf: cartilaginousFish + Class: hammerhead + SubClassOf: shark + Class: greatWhiteShark + SubClassOf: shark + Class: ray + SubClassOf: cartilaginousFish + Class: teleost + SubClassOf: vertebrate + Class: zebrafish + SubClassOf: teleost + Class: tetrapod + SubClassOf: vertebrate + Class: anamniote + SubClassOf: tetrapod + Class: amphibian + SubClassOf: anamniote + Class: xenopus + SubClassOf: amphibian + Class: amniote + SubClassOf: tetrapod + Class: reptile + SubClassOf: amniote + Class: bird + SubClassOf: amniote + Class: chicken + SubClassOf: bird + Class: pigeon + SubClassOf: bird + Class: parrot + SubClassOf: bird + Class: mammal + SubClassOf: amniote + Class: monotreme + SubClassOf: mammal + Class: platypus + SubClassOf: monotreme + Class: therian + SubClassOf: mammal + Class: metatherian + SubClassOf: therian + Class: kangaroo + SubClassOf: metatherian + Class: koala + SubClassOf: metatherian + Class: eutherian + SubClassOf: therian + Class: carnivore + SubClassOf: eutherian + Class: cat + SubClassOf: carnivore + Class: dog + SubClassOf: carnivore + Class: euarchontoglires + SubClassOf: eutherian + Class: rodent + SubClassOf: euarchontoglires + Class: rat + SubClassOf: rodent + Class: mouse + SubClassOf: rodent + Class: primate + SubClassOf: euarchontoglires + Class: ape + SubClassOf: primate + Class: human + SubClassOf: ape + Class: chimp + SubClassOf: ape + Class: orangutan + SubClassOf: ape + Class: platyrrhine + SubClassOf: primate + Class: marmoset + SubClassOf: platyrrhine + Class: howler + SubClassOf: platyrrhine + + Class: cetacean + SubClassOf: eutherian + Class: baleenWhale + SubClassOf: cetacean + Class: blueWhale + SubClassOf: baleenWhale + Class: toothedWhale + SubClassOf: cetacean + Class: dolphin + SubClassOf: toothedWhale + Class: porpoise + SubClassOf: toothedWhale + SubClassOf: cetacean + + Class: tunicate + SubClassOf: chordate + Class: squirt + SubClassOf: tunicate + Class: salp + SubClassOf: tunicate + + Class: cnidarian + SubClassOf: animal + Class: jelly + SubClassOf: cnidarian + Class: coral + SubClassOf: cnidarian + Class: poriferan + SubClassOf: animal \ No newline at end of file