Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add specimen and taxon circumscription #75

Merged
merged 14 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bin/phyx2owl.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ function convertFileToOWL(filename, argOutputFilename = "") {

return true;
} catch(e) {
console.error(`Could not convert ${filename} to ${outputFilename}: ${e}`);
console.error(`Could not convert ${filename} to ${outputFilename}: ${e} at ${e.stack}`);
console.error(``)
}
return false;
}
Expand Down
1 change: 1 addition & 0 deletions docs/context/development/phyx.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"@type": "xsd:string"
},

"basisOfRecord": "dwc:basisOfRecord",
"occurrenceID": "dwc:occurrenceID",
"catalogNumber": "dwc:catalogNumber",
"collectionCode": "dwc:collectionCode",
Expand Down
34 changes: 32 additions & 2 deletions docs/context/development/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,38 @@
},
"label": {
"type": "string",
"minLength": 1
"minLength": 1,
"description": "A label for this taxonomic unit."
},
"nameAccordingTo": {
"type": "string",
"minLength": 1,
"description": "For a taxon concept, provides the source in which the specific taxon concept circumscription is defined or implied (i.e. the 'sensu' or 'sec' of the name)."
},
"basisOfRecord": {
"type": "string",
"minLength": 1,
"description": "The basis of record for this specimen."
},
"occurrenceID": {
"type": "string",
"minLength": 1,
"description": "The occurrence ID of a specimen."
},
"institutionCode": {
"type": "string",
"minLength": 1,
"description": "The institution code of a specimen."
},
"collectionCode": {
"type": "string",
"minLength": 1,
"description": "The collection code of a specimen."
},
"catalogNumber": {
"type": "string",
"minLength": 1,
"description": "The catalog number of a specimen."
},
"hasName": {
"type": "object",
Expand Down Expand Up @@ -206,7 +237,6 @@
},
"required": [
"@type",
"label",
"nameComplete"
]
}
Expand Down
194 changes: 45 additions & 149 deletions src/wrappers/PhylorefWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -579,140 +579,6 @@ class PhylorefWrapper {
return classExprs;
}


/*
* Given an expression that evaluates to an included node and a taxonomic unit,
* return an expression for including it and excluding the TU. Note that this
* always returns an array.
*
* When the included expression includes a single taxonomic unit (i.e. is in the
* form `includes_TU some [TU]`), then the simple form is adequate. However, when
* it's a more complex expression, it's possible that the excluded TU isn't just
* sister to this clade but outside of it entirely. In that case, we add another
* class expression:
* [includesExpr] and (has_Ancestor some (excludes_TU some [TU]))
*/
getClassExpressionsForExprAndTU(includedExpr, tu) {
if (!includedExpr) throw new Error('Exclusions require an included expression');

const exprs = [{
'@type': 'owl:Class',
intersectionOf: [
includedExpr,
{
'@type': 'owl:Restriction',
onProperty: 'phyloref:excludes_TU',
someValuesFrom: new TaxonomicUnitWrapper(tu, this.defaultNomenCode).asOWLEquivClass,
},
],
}];

if (!Array.isArray(includedExpr) && has(includedExpr, 'onProperty') && includedExpr.onProperty === 'phyloref:includes_TU') {
// In this specific set of circumstances, we do NOT need to add the has_Ancestor check.
} else {
// Add the has_Ancestor check!
exprs.push({
'@type': 'owl:Class',
intersectionOf: [
includedExpr,
{
'@type': 'owl:Restriction',
onProperty: 'obo:CDAO_0000144', // has_Ancestor
someValuesFrom: {
'@type': 'owl:Restriction',
onProperty: 'phyloref:excludes_TU',
someValuesFrom: new TaxonomicUnitWrapper(
tu,
this.defaultNomenCode
).asOWLEquivClass,
},
},
],
});
}

return exprs;
}


/*
* Returns a list of class expressions for a phyloreference that has an expression
* for the MRCA of its internal specifiers, but also has one or more external specifiers.
* - jsonld: The JSON-LD form of the Phyloreference from Model 1.0. Mainly used
* for retrieving the '@id' and the specifiers.
* - accumulatedExpr: Initially, an expression that evaluates to the MRCA of all
* internal specifiers, calculated using createClassExpressionsForInternals().
* When we call this method recusively, this expression will incorporate
* representations of external references.
* - remainingExternals: External specifiers that are yet to be incorporated into the
* expressions we are building.
* - selected: External specifiers that have already been selected.
*
* Our overall algorithm here is:
* 1. If we need to add a single remaining external to the accumulated expression,
* we can do that by adding an `excludes_TU` to the expression (and possibly a
* `has_Ancestor` check, see getClassExpressionsForExprAndTU()).
* 2. If we need to add more than one remaining external, we select each external
* specifier one at a time. We add the selected specifier to the accumulated
* expression using getClassExpressionsForExprAndTU(), and then call ourselves
* recursively to add the remaining specifiers.
*
* The goal here is to create expressions for every possible sequence of external
* specifiers, so we can account for cases where some external specifiers are closer
* to the initial internal-specifier-only expression than others.
*/
createClassExpressionsForExternals(jsonld, accumulatedExpr, remainingExternals, selected) {
// process.stderr.write(`@id [${jsonld['@id']}] Remaining externals:
// ${remainingExternals.length}, selected: ${selected.length}\n`);

// Step 1. If we only have one external remaining, we can provide our two-case example
// to detect it.
const classExprs = [];
if (remainingExternals.length === 0) {
throw new Error('Cannot create class expression when no externals remain');
} else if (remainingExternals.length === 1) {
const remainingExternalsExprs = this.getClassExpressionsForExprAndTU(
accumulatedExpr,
remainingExternals[0],
selected.length > 0
);
remainingExternalsExprs.forEach(expr => classExprs.push(expr));
} else { // if(remainingExternals.length > 1)
// Recurse into remaining externals. Every time we select a single entry,
// we create a class expression for that.
remainingExternals.map((newlySelected) => {
// process.stderr.write(`Selecting new object, remaining now at:
// ${remainingExternals.filter(i => i !== newlySelected).length},
// selected: ${selected.concat([newlySelected]).length}\n`);

// Create a new component class for the accumulated expression plus the
// newly selected external specifier.
const newlyAccumulatedExpr = this.createComponentClass(
jsonld,
jsonld.internalSpecifiers,
selected.concat([newlySelected]),
this.getClassExpressionsForExprAndTU(
accumulatedExpr, newlySelected, selected.length > 0
)
);

// Call ourselves recursively to add the remaining externals.
return this.createClassExpressionsForExternals(
jsonld,
newlyAccumulatedExpr,
// The new remaining is the old remaining minus the selected TU.
remainingExternals.filter(i => i !== newlySelected),
// The new selected is the old selected plus the selected TU.
selected.concat([newlySelected])
);
})
.reduce((acc, val) => acc.concat(val), [])
.forEach(expr => classExprs.push(expr));
}

return classExprs;
}

/*
* Phyloref.asJSONLD(fallbackIRI)
*
Expand Down Expand Up @@ -784,24 +650,54 @@ class PhylorefWrapper {
// in the form:
// phyloref:includes_TU some [internal1] and
// phyloref:includes_TU some [internal2] and ...
// To which we can then add the external specifiers.
if (internalSpecifiers.length === 1) {
logicalExpressions = this.createClassExpressionsForExternals(
phylorefAsJSONLD,
this.getIncludesRestrictionForTU(internalSpecifiers[0]),
externalSpecifiers,
[]
// phyloref:excludes_TU some [exclusion1] and
// has_Ancestor some (phyloref:excludesTU some [exclusion2]) ...
//
// Since we don't know which of the external specifiers will actually
// be the one that will be used, we need to generate logical expressions
// for every possibility.

logicalExpressions = externalSpecifiers.map((selectedExternal) => {
// Add the internal specifiers.
const intersectionExprs = internalSpecifiers.map(
sp => this.getIncludesRestrictionForTU(sp)
);
} else {
const expressionForInternals = {

// Add the selected external specifier.
intersectionExprs.push({
'@type': 'owl:Restriction',
onProperty: 'phyloref:excludes_TU',
someValuesFrom: new TaxonomicUnitWrapper(
selectedExternal,
this.defaultNomenCode
).asOWLEquivClass,
});

// Collect all of the externals that are not selected.
const remainingExternals = externalSpecifiers.filter(ex => ex !== selectedExternal);

// Add the remaining externals, which we assume will resolve outside of
// this clade.
remainingExternals.forEach((externalTU) => {
intersectionExprs.push({
'@type': 'owl:Restriction',
onProperty: 'obo:CDAO_0000144', // has_Ancestor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that there's a property has_outside_TU for this in the next release of the Phyloref Ontology: https://github.com/phyloref/phyloref-ontology/blob/master/phyloref.ofn#L38

Not sure whether using it should be a downstream change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I forgot about that property! I've opened an issue to make the change in Phyx.js once the new version of the Phyloref Ontology has been released: #83.

someValuesFrom: {
'@type': 'owl:Restriction',
onProperty: 'phyloref:excludes_TU',
someValuesFrom: new TaxonomicUnitWrapper(
externalTU,
this.defaultNomenCode
).asOWLEquivClass,
},
});
});

return {
'@type': 'owl:Class',
intersectionOf: internalSpecifiers.map(sp => this.getIncludesRestrictionForTU(sp)),
intersectionOf: intersectionExprs,
};

logicalExpressions = this.createClassExpressionsForExternals(
phylorefAsJSONLD, expressionForInternals, externalSpecifiers, []
);
}
});
} else {
calculatedPhylorefType = 'phyloref:PhyloreferenceUsingMinimumClade';

Expand Down
Loading