Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6816da3
Parse Manchester templates at compile time.
balhoff May 13, 2026
376a64d
Add Expansion module for pure row-time axiom construction.
balhoff May 13, 2026
507a012
Wire Generate through Expansion and drop row-time code in ExpandedDOSDP.
balhoff May 13, 2026
5bb147d
Fix regressions in row-time expansion uncovered by reviewer.
balhoff May 14, 2026
363aa35
Fix two more row-time regressions caught in second review.
balhoff May 14, 2026
b24cee4
Fix two multi_clause sub_clause issues from third review.
balhoff May 14, 2026
c9a6320
Honor explicit multi_clause separator even on a single clause.
balhoff May 14, 2026
3f628c0
Add Phase 0.3 behavior-preservation harness for the generate path.
balhoff May 15, 2026
d661e6d
Extend behavior-preservation harness to query, terms, and docs.
balhoff May 15, 2026
d2b7ccc
Resolve class-label bindings at row time so prototype works again.
balhoff May 15, 2026
39dc441
Catch placeholder IRIs in annotation subject and value positions.
balhoff May 15, 2026
c7bfc4a
Close out Phase 0.3 with audit-by-shape and broader query coverage.
balhoff May 15, 2026
74f21d3
Introduce a placeholder RowBindings constructor for Phase 3.0.1.
balhoff May 17, 2026
57db37c
Collapse to one expansion path for Phase 3.0.2.
balhoff May 18, 2026
61f700e
Emit MultiValue placeholders for list and data_list vars.
balhoff May 18, 2026
2a0648d
Recognize $name literal placeholders as SPARQL variables.
balhoff May 18, 2026
a83653d
Add isLiteral filter for $name annotation-value placeholders.
balhoff May 18, 2026
87d7ce8
Audit literal-walker coverage against OWL API enums.
balhoff May 18, 2026
3b2df37
Split ExpandedDOSDP into focused modules for Phase 3.1.
balhoff May 19, 2026
9dec3a5
Add module-level docstring to DocsMarkdown.
balhoff May 19, 2026
530f2b0
Fix two row-expansion correctness regressions.
balhoff May 19, 2026
c3c8927
Extend correctness tests with regression-prone shapes.
balhoff May 19, 2026
d827b48
Drop empty GCI templates silently to match the class-expression types.
balhoff May 19, 2026
58516e4
Restrict DOSDP variable names
balhoff May 19, 2026
6a00743
Simplify describePiece and pin variableNames walker coverage.
balhoff May 19, 2026
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
165 changes: 165 additions & 0 deletions src/main/scala/org/monarchinitiative/dosdp/AnnotationTranslation.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package org.monarchinitiative.dosdp

import org.phenoscape.scowl._
import org.semanticweb.owlapi.model._

/**
* Pure rendering of a `NormalizedAnnotation` into one or more `OWLAnnotation`
* values for a single row's bindings. Handles printf-style text templates,
* literal list values, IRI-valued annotations, and permutation expansion
* driven by an annotation-property index built from the loaded ontology.
*/
private[dosdp] object AnnotationTranslation {

private type Bindings = Map[String, Binding]
private type PermutationIndex = Map[IRI, Map[IRI, Set[String]]]

def translate(
annotationField: NormalizedAnnotation,
annotationBindings: Option[Bindings],
logicalBindings: Option[Bindings],
permutationIndex: PermutationIndex,
prefixes: PartialFunction[String, String]
): Set[OWLAnnotation] = annotationField match {
case NormalizedPrintfAnnotation(prop, text, vars, multiClause, overrideColumnOpt, subAnnotations, permutations) =>
val valueOpts = (for {
column <- overrideColumnOpt
bindings <- annotationBindings
SingleValue(binding) <- bindings.get(column)
trimmed = binding.trim
if trimmed.nonEmpty
} yield Seq(trimmed)).orElse(Some(
printAnnotationWithPermutations(text, vars, multiClause, annotationBindings, logicalBindings, permutations, permutationIndex, prefixes)))
valueOpts.getOrElse(Seq.empty).toSet[String].map(value =>
Annotation(subAnnotations.flatMap(translate(_, annotationBindings, logicalBindings, permutationIndex, prefixes)), prop, value))
case NormalizedListAnnotation(prop, value, subAnnotations) =>
val multiValBindingsOpt = annotationBindings.map(multiValueBindings)
val bindingsMap = multiValBindingsOpt.getOrElse(Map(value -> MultiValue(Set("'$" + value + "'"))))
val listValueOpt = bindingsMap.get(value)
listValueOpt.toSet[MultiValue].flatMap(listValue =>
listValue.value.map(v =>
Annotation(subAnnotations.flatMap(translate(_, annotationBindings, logicalBindings, permutationIndex, prefixes)), prop, v)))
case NormalizedIRIValueAnnotation(prop, varr, subAnnotations) =>
val maybeIRIValue = logicalBindings.map { actualBindings =>
for {
SingleValue(value) <- actualBindings.get(varr)
iri <- Prefixes.idToIRI(value, prefixes)
} yield iri
}.getOrElse(Some(DOSDP.variableToIRI(varr)))
maybeIRIValue.toSet[IRI].map(iriValue =>
Annotation(subAnnotations.flatMap(translate(_, annotationBindings, logicalBindings, permutationIndex, prefixes)), prop, iriValue))
}

/**
* Render an annotation's text against the row bindings, expanding any single
* multi-value binding that appears in the clause's variable list into one
* rendered string per value.
*/
def printAnnotation(
text: Option[String],
vars: Option[List[String]],
multiClause: Option[MultiClausePrintf],
annotationBindings: Option[Bindings]
): Seq[String] = {
val clauseVars = for {
mc <- multiClause.toList
clauses <- mc.clauses.toList
clause <- clauses
vars <- clause.vars
} yield vars
val variables = vars.getOrElse(List.empty) ++ clauseVars.flatten
val annotationRelatedMultiValueBindings = annotationBindings.getOrElse(Map.empty[String, Binding])
.view.filterKeys(variables.contains(_)).collectFirst { case (key, MultiValue(value)) => (key, value) }
val singleValBindings = annotationBindings.getOrElse(Map.empty[String, Binding]).collect { case (key, SingleValue(value)) => (key, SingleValue(value)) }
annotationRelatedMultiValueBindings match {
case None =>
PrintfText.replaced(text, vars, multiClause, annotationBindings.map(singleValueBindings), quote = false).toSeq
case Some(multiValuePair) =>
val multiValueText = for {
value <- multiValuePair._2
multiText <- PrintfText.replaced(None, None, multiClause, Some(singleValBindings + (multiValuePair._1 -> SingleValue(value))), quote = false)
} yield multiText
multiValueText.toSeq
}
}

/**
* Generate annotation texts with permutations: for each annotation variable,
* collect its label value plus permutation values pulled from the filler
* term's annotation properties, then format the cartesian product of those
* value lists through the printf template.
*/
def printAnnotationWithPermutations(
text: Option[String],
vars: Option[List[String]],
multiClause: Option[MultiClausePrintf],
annotationBindings: Option[Bindings],
logicalBindings: Option[Bindings],
permutations: List[NormalizedPermutation],
permutationIndex: PermutationIndex,
prefixes: PartialFunction[String, String]
): Seq[String] = {
val variableList = vars.getOrElse(List.empty)
if (permutations.isEmpty || variableList.isEmpty)
printAnnotation(text, vars, multiClause, annotationBindings)
else {
val permutationsByVar: Map[String, NormalizedPermutation] = permutations.map(p => p.`var` -> p).toMap
val valuesPerVar = variableList.map(v =>
valuesForVar(v, permutationsByVar, annotationBindings, logicalBindings, permutationIndex, prefixes))
cartesianProduct(valuesPerVar).flatMap { combination =>
val bindingsForCombination = variableList.zip(combination).map { case (n, v) => n -> SingleValue(v) }.toMap
PrintfText.replaced(text, vars, multiClause, Some(bindingsForCombination), quote = false)
}.distinct
}
}

/**
* Candidate values for one variable in a permuted annotation: the label
* always comes first (so its combination is emitted before any
* permutation-only combination), followed by values pulled from the filler
* term's annotation properties named by any matching permutation spec.
*/
private def valuesForVar(
varName: String,
permutationsByVar: Map[String, NormalizedPermutation],
annotationBindings: Option[Bindings],
logicalBindings: Option[Bindings],
permutationIndex: PermutationIndex,
prefixes: PartialFunction[String, String]
): List[String] = {
val labelValue: Option[String] = for {
bindings <- annotationBindings
SingleValue(value) <- bindings.get(varName)
} yield value
val fillerIRI: Option[IRI] = for {
bindings <- logicalBindings
SingleValue(value) <- bindings.get(varName)
iri <- Prefixes.idToIRI(value, prefixes)
} yield iri
val permutationValues: Set[String] = (for {
perm <- permutationsByVar.get(varName)
iri <- fillerIRI
termAnnos <- permutationIndex.get(iri)
} yield perm.annotationProperties.flatMap(prop => termAnnos.getOrElse(prop.getIRI, Set.empty)).toSet)
.getOrElse(Set.empty)
labelValue.toList ++ permutationValues.toList
}

/** Cartesian product: `[[a, b], [1, 2]] => [[a, 1], [a, 2], [b, 1], [b, 2]]`. */
private def cartesianProduct[T](lists: List[List[T]]): List[List[T]] = lists match {
case Nil => List(Nil)
case head :: tail =>
val tailProduct = cartesianProduct(tail)
for {
h <- head
t <- tailProduct
} yield h :: t
}

private def singleValueBindings(bindings: Bindings): Map[String, SingleValue] =
bindings.collect { case (key, value: SingleValue) => key -> value }

private def multiValueBindings(bindings: Bindings): Map[String, MultiValue] =
bindings.collect { case (key, value: MultiValue) => key -> value }

}
22 changes: 22 additions & 0 deletions src/main/scala/org/monarchinitiative/dosdp/Bindings.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
package org.monarchinitiative.dosdp

import scala.util.matching.Regex.Match

sealed trait Binding extends Product with Serializable

final case class SingleValue(value: String) extends Binding

final case class MultiValue(value: Set[String]) extends Binding

final case class ExpandedRegexSub(regexSub: RegexSub) {

private val groupFinder = raw"\\(\d+)".r

private val regex = regexSub.`match`.r

/** Apply `regexSub` to a value; if the pattern does not match, return the value unchanged. */
def substitute(value: String): String =
regex.findFirstMatchIn(value).map { valueMatch =>
groupFinder.replaceAllIn(regexSub.sub, (placeholder: Match) => valueMatch.group(placeholder.group(1).toInt))
}.getOrElse(value)

def expandBindings(bindings: Map[String, Binding]): Map[String, Binding] =
bindings.get(regexSub.in).map {
case SingleValue(value) => regexSub.out -> SingleValue(substitute(value))
case MultiValue(values) => regexSub.out -> MultiValue(values.map(substitute))
}.fold(bindings)(bindings + _)

}
Loading
Loading