Skip to content
Open
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
23 changes: 18 additions & 5 deletions src/main/scala/org/monarchinitiative/dosdp/DOSDP.scala
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ final case class PrintfAnnotation(
text: Option[String],
vars: Option[List[String]],
`override`: Option[String],
multi_clause: Option[MultiClausePrintf] = None)
multi_clause: Option[MultiClausePrintf] = None,
permutations: Option[List[Permutation]] = None)
extends Annotations with PrintfText {

val shouldQuote = false
Expand All @@ -243,9 +244,9 @@ object Annotations {

implicit val decodeAnnotations: Decoder[Annotations] = Decoder[ListAnnotation].map[Annotations](identity).or(Decoder[IRIValueAnnotation].map[Annotations](identity)).or(Decoder[PrintfAnnotation].map[Annotations](identity))
implicit val encodeAnnotations: Encoder[Annotations] = Encoder.instance {
case pfa @ PrintfAnnotation(_, _, _, _, _, _) => pfa.asJson
case la @ ListAnnotation(_, _, _) => la.asJson
case iva @ IRIValueAnnotation(_, _, _) => iva.asJson
case pfa @ PrintfAnnotation(_, _, _, _, _, _, _) => pfa.asJson
case la @ ListAnnotation(_, _, _) => la.asJson
case iva @ IRIValueAnnotation(_, _, _) => iva.asJson
}

}
Expand All @@ -257,7 +258,8 @@ final case class PrintfAnnotationOBO(
xrefs: Option[String],
text: Option[String],
vars: Option[List[String]],
multi_clause: Option[MultiClausePrintf] = None) extends PrintfText with AnnotationLike with OBOAnnotations {
multi_clause: Option[MultiClausePrintf] = None,
permutations: Option[List[Permutation]] = None) extends PrintfText with AnnotationLike with OBOAnnotations {

val shouldQuote = false

Expand Down Expand Up @@ -350,4 +352,15 @@ final case class RegexFunction(regex: RegexSub) extends Function {

final case class Join(sep: String)

/**
* Represents a permutation specification for generating additional annotations
* using values from annotation properties on filler terms.
*
* @param `var` The name of a single variable for which to generate permutations.
* Must correspond to a variable specified in the 'vars' field of the annotation.
* @param annotationProperties A list of annotation property names (as declared in the pattern's
* annotationProperties dictionary) whose values from the filler term
* will be used to generate additional annotations.
*/
final case class Permutation(`var`: String, annotationProperties: List[String])

150 changes: 134 additions & 16 deletions src/main/scala/org/monarchinitiative/dosdp/ExpandedDOSDP.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ final case class ExpandedDOSDP(dosdp: DOSDP, prefixes: PartialFunction[String, S

private type Bindings = Map[String, Binding]

/**
* Index mapping filler term IRIs to their annotation property values.
* Structure: Map[FillerTermIRI, Map[AnnotationPropertyIRI, Set[AnnotationValues]]]
*/
type PermutationIndex = Map[IRI, Map[IRI, Set[String]]]

val substitutions: Seq[ExpandedRegexSub] = dosdp.substitutions.toSeq.flatten.map(ExpandedRegexSub)

def allObjectProperties: Map[String, String] = dosdp.relations.getOrElse(Map.empty) ++ dosdp.objectProperties.getOrElse(Map.empty)
Expand Down Expand Up @@ -132,7 +138,7 @@ final case class ExpandedDOSDP(dosdp: DOSDP, prefixes: PartialFunction[String, S
ZIO.collectAll(owlAnnotations).map(_.to(Set).flatten)
}

def filledAnnotationAxioms(annotationBindings: Option[Bindings], logicalBindings: Option[Bindings]): ZIO[Logging, DOSDPError, Set[OWLAnnotationAssertionAxiom]] = {
def filledAnnotationAxioms(annotationBindings: Option[Bindings], logicalBindings: Option[Bindings], permutationIndex: PermutationIndex = Map.empty): ZIO[Logging, DOSDPError, Set[OWLAnnotationAssertionAxiom]] = {
val definedTerm = (for {
actualBindings <- annotationBindings
SingleValue(value) <- actualBindings.get(DOSDP.DefinedClassVariable)
Expand All @@ -145,7 +151,7 @@ final case class ExpandedDOSDP(dosdp: DOSDP, prefixes: PartialFunction[String, S
} yield {
for {
normalizedAnnotationField <- allNormalizedAnns
annotation <- translateAnnotations(normalizedAnnotationField, annotationBindings, logicalBindings)
annotation <- translateAnnotations(normalizedAnnotationField, annotationBindings, logicalBindings, permutationIndex)
} yield AnnotationAssertion(annotation.getAnnotations.asScala.toSet, annotation.getProperty, definedTerm, annotation.getValue)
}
}
Expand All @@ -171,22 +177,22 @@ final case class ExpandedDOSDP(dosdp: DOSDP, prefixes: PartialFunction[String, S
}.map(_.flatten.to(Set))
}

private def translateAnnotations(annotationField: NormalizedAnnotation, annotationBindings: Option[Bindings], logicalBindings: Option[Bindings]): Set[OWLAnnotation] = annotationField match {
case NormalizedPrintfAnnotation(prop, text, vars, multiClause, overrideColumnOpt, subAnnotations) =>
private def translateAnnotations(annotationField: NormalizedAnnotation, annotationBindings: Option[Bindings], logicalBindings: Option[Bindings], permutationIndex: PermutationIndex = Map.empty): 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(printAnnotation(text, vars, multiClause, annotationBindings)))
valueOpts.getOrElse(Seq.empty).toSet[String].map(value => Annotation(subAnnotations.flatMap(translateAnnotations(_, annotationBindings, logicalBindings)), prop, value))
} yield Seq(trimmed)).orElse(Some(printAnnotationWithPermutations(text, vars, multiClause, annotationBindings, logicalBindings, permutations, permutationIndex)))
valueOpts.getOrElse(Seq.empty).toSet[String].map(value => Annotation(subAnnotations.flatMap(translateAnnotations(_, annotationBindings, logicalBindings, permutationIndex)), prop, value))
case NormalizedListAnnotation(prop, value, subAnnotations) =>
// If no variable bindings are passed in, dummy value is filled in using variable name
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(translateAnnotations(_, annotationBindings, logicalBindings)), prop, v)))
listValueOpt.toSet[MultiValue].flatMap(listValue => listValue.value.map(v => Annotation(subAnnotations.flatMap(translateAnnotations(_, annotationBindings, logicalBindings, permutationIndex)), prop, v)))
case NormalizedIRIValueAnnotation(prop, varr, subAnnotations) =>
val maybeIRIValue = logicalBindings.map { actualBindings =>
for {
Expand All @@ -195,7 +201,7 @@ final case class ExpandedDOSDP(dosdp: DOSDP, prefixes: PartialFunction[String, S
} yield iri
}.getOrElse(Some(DOSDP.variableToIRI(varr)))
maybeIRIValue.toSet[IRI].map(iriValue => Annotation(
subAnnotations.flatMap(translateAnnotations(_, annotationBindings, logicalBindings)),
subAnnotations.flatMap(translateAnnotations(_, annotationBindings, logicalBindings, permutationIndex)),
prop,
iriValue))
}
Expand Down Expand Up @@ -233,12 +239,101 @@ final case class ExpandedDOSDP(dosdp: DOSDP, prefixes: PartialFunction[String, S
}
}

/**
* Generates annotation texts with permutations. This extends the standard annotation generation
* by also substituting synonym values from filler terms to generate additional annotations.
*
* The algorithm:
* 1. For each variable in vars, collect all possible values:
* - Always include the label (from annotationBindings)
* - If the variable has permutation specs, also include values from the specified annotation properties
* 2. Generate the cartesian product of all value combinations
* 3. Format each combination using the text template
*/
private def printAnnotationWithPermutations(
text: Option[String],
vars: Option[List[String]],
multiClause: Option[MultiClausePrintf],
annotationBindings: Option[Bindings],
logicalBindings: Option[Bindings],
permutations: List[NormalizedPermutation],
permutationIndex: PermutationIndex
): Seq[String] = {
val variableList = vars.getOrElse(List.empty)

// If no permutations or no variables, fall back to standard behavior
if (permutations.isEmpty || variableList.isEmpty) {
return printAnnotation(text, vars, multiClause, annotationBindings)
}

// Build a map from variable name to permutation spec
val permutationsByVar: Map[String, NormalizedPermutation] = permutations.map(p => p.`var` -> p).toMap

// For each variable, collect all possible values (label + permutation values)
val valueLists: List[(String, List[String])] = variableList.map { varName =>
// Get the label value from annotation bindings
val labelValue: Option[String] = for {
bindings <- annotationBindings
SingleValue(value) <- bindings.get(varName)
} yield value

// Get the filler IRI from logical bindings to look up permutation values
val fillerIRI: Option[IRI] = for {
bindings <- logicalBindings
SingleValue(value) <- bindings.get(varName)
iri <- Prefixes.idToIRI(value, prefixes)
} yield iri

// Collect permutation values if this variable has a permutation spec
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)

// Combine label with permutation values, label always first
val allValues = labelValue.toList ++ permutationValues.toList
varName -> allValues
}

// Generate cartesian product of all value lists
val combinations: List[List[String]] = cartesianProduct(valueLists.map(_._2))

// Format each combination using the text template
combinations.flatMap { values =>
val bindingsForCombination = variableList.zip(values).map { case (varName, value) =>
varName -> SingleValue(value)
}.toMap
PrintfText.replaced(text, vars, multiClause, Some(bindingsForCombination), quote = false)
}.distinct
}

/**
* Computes the cartesian product of a list of lists.
* E.g., [[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 normalizeAnnotation(annotation: Annotations): ZIO[Logging, DOSDPError, NormalizedAnnotation] = annotation match {
case PrintfAnnotation(anns, ap, text, vars, overrideColumn, multiClause) =>
case PrintfAnnotation(anns, ap, text, vars, overrideColumn, multiClause, perms) =>
for {
prop <- safeChecker.getOWLAnnotationProperty(ap).orElse(logErrorFail(s"No annotation property binding: $ap"))
annotations <- ZIO.foreach(anns.to(List).flatten)(normalizeAnnotation)
} yield NormalizedPrintfAnnotation(prop, text, vars, multiClause, overrideColumn, annotations.to(Set))
_ <- validatePermutationVars(perms.getOrElse(List.empty), vars.getOrElse(List.empty))
normalizedPerms <- ZIO.foreach(perms.getOrElse(List.empty))(normalizePermutation)
} yield NormalizedPrintfAnnotation(prop, text, vars, multiClause, overrideColumn, annotations.to(Set), normalizedPerms)
case ListAnnotation(anns, ap, value) =>
for {
prop <- safeChecker.getOWLAnnotationProperty(ap).orElse(logErrorFail(s"No annotation property binding: $ap"))
Expand All @@ -253,11 +348,14 @@ final case class ExpandedDOSDP(dosdp: DOSDP, prefixes: PartialFunction[String, S

private def normalizeOBOAnnotation(annotation: OBOAnnotations, property: OWLAnnotationProperty, overrideColumn: Option[String]): ZIO[Logging, DOSDPError, NormalizedAnnotation] =
annotation match {
case PrintfAnnotationOBO(anns, xrefs, text, vars, multiClause) =>
ZIO.foreach(anns.to(List).flatten)(normalizeAnnotation).map { annotations =>
NormalizedPrintfAnnotation(property, text, vars, multiClause, overrideColumn,
annotations.to(Set) ++ xrefs.map(NormalizedListAnnotation(PrintfAnnotationOBO.Xref, _, Set.empty)))
}
case PrintfAnnotationOBO(anns, xrefs, text, vars, multiClause, perms) =>
for {
annotations <- ZIO.foreach(anns.to(List).flatten)(normalizeAnnotation)
_ <- validatePermutationVars(perms.getOrElse(List.empty), vars.getOrElse(List.empty))
normalizedPerms <- ZIO.foreach(perms.getOrElse(List.empty))(normalizePermutation)
} yield NormalizedPrintfAnnotation(property, text, vars, multiClause, overrideColumn,
annotations.to(Set) ++ xrefs.map(NormalizedListAnnotation(PrintfAnnotationOBO.Xref, _, Set.empty)),
normalizedPerms)
case ListAnnotationOBO(value, xrefs) => ZIO.succeed(
NormalizedListAnnotation(
property,
Expand All @@ -266,6 +364,24 @@ final case class ExpandedDOSDP(dosdp: DOSDP, prefixes: PartialFunction[String, S
)
}

private def normalizePermutation(permutation: Permutation): ZIO[Logging, DOSDPError, NormalizedPermutation] = {
for {
props <- ZIO.foreach(permutation.annotationProperties) { apName =>
safeChecker.getOWLAnnotationProperty(apName).orElse(logErrorFail(s"No annotation property binding for permutation: $apName"))
}
} yield NormalizedPermutation(permutation.`var`, props)
}

/**
* Validates that all permutation vars reference variables in the annotation's vars list.
*/
private def validatePermutationVars(permutations: List[Permutation], vars: List[String]): ZIO[Logging, DOSDPError, Unit] = {
val varSet = vars.toSet
val invalidVars = permutations.map(_.`var`).filterNot(varSet.contains)
if (invalidVars.isEmpty) ZIO.unit
else logErrorFail(s"Permutation vars not found in annotation vars list: ${invalidVars.mkString(", ")}. Available vars: ${vars.mkString(", ")}")
}

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 }
Expand All @@ -284,7 +400,9 @@ final case class ExpandedDOSDP(dosdp: DOSDP, prefixes: PartialFunction[String, S
def subAnnotations: Set[NormalizedAnnotation]
}

private case class NormalizedPrintfAnnotation(property: OWLAnnotationProperty, text: Option[String], vars: Option[List[String]], multiClause: Option[MultiClausePrintf], overrideColumn: Option[String], subAnnotations: Set[NormalizedAnnotation]) extends NormalizedAnnotation
private case class NormalizedPrintfAnnotation(property: OWLAnnotationProperty, text: Option[String], vars: Option[List[String]], multiClause: Option[MultiClausePrintf], overrideColumn: Option[String], subAnnotations: Set[NormalizedAnnotation], permutations: List[NormalizedPermutation] = List.empty) extends NormalizedAnnotation

private case class NormalizedPermutation(`var`: String, annotationProperties: List[OWLAnnotationProperty])

private case class NormalizedListAnnotation(property: OWLAnnotationProperty, value: String, subAnnotations: Set[NormalizedAnnotation]) extends NormalizedAnnotation

Expand Down
15 changes: 14 additions & 1 deletion src/main/scala/org/monarchinitiative/dosdp/cli/Generate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ object Generate {
def renderPattern(dosdp: DOSDP, prefixes: PartialFunction[String, String], fillers: List[Map[String, String]], ontOpt: Option[OWLOntology], outputLogicalAxioms: Boolean, outputAnnotationAxioms: Boolean, restrictAxiomsColumnName: Option[String], annotateAxiomSource: Boolean, axiomSourceProperty: OWLAnnotationProperty, generateDefinedClass: Boolean, extraReadableIdentifiers: Map[IRI, Map[IRI, String]]): ZIO[Logging, DOSDPError, Set[OWLAxiom]] = {
val eDOSDP = ExpandedDOSDP(dosdp, prefixes)
val knownColumns = dosdp.allVars
// Create permutation index for looking up annotation property values from filler terms
val permutationIndex: eDOSDP.PermutationIndex = ontOpt.map(createPermutationIndex).getOrElse(Map.empty)
for {
readableIdentifiers <- eDOSDP.readableIdentifierProperties
initialReadableIDIndex = ontOpt.map(ont => createReadableIdentifierIndex(readableIdentifiers, eDOSDP, ont)).getOrElse(Map.empty)
Expand Down Expand Up @@ -127,7 +129,7 @@ object Generate {
eDOSDP.filledLogicalAxioms(Some(logicalBindingsExtended), Some(annotationBindings))
else ZIO.succeed(Set.empty)
annotationAxioms <- if (localOutputAnnotationAxioms)
eDOSDP.filledAnnotationAxioms(Some(annotationBindings), Some(logicalBindingsExtended))
eDOSDP.filledAnnotationAxioms(Some(annotationBindings), Some(logicalBindingsExtended), permutationIndex)
else ZIO.succeed(Set.empty)
} yield logicalAxioms ++ annotationAxioms
maybeAxioms
Expand Down Expand Up @@ -193,6 +195,17 @@ object Generate {
mappings.fold(Map.empty)(_ combine _)
}

/**
* Creates an index of annotation property values for filler terms, used for permutation generation.
* Structure: Map[FillerTermIRI, Map[AnnotationPropertyIRI, Set[AnnotationValues]]]
*/
private def createPermutationIndex(ont: OWLOntology): Map[IRI, Map[IRI, Set[String]]] = {
val mappings = for {
AnnotationAssertion(_, prop, subj: IRI, value ^^ _) <- ont.getAxioms(AxiomType.ANNOTATION_ASSERTION, Imports.INCLUDED).asScala
} yield Map(subj -> Map(prop.getIRI -> Set(value)))
mappings.fold(Map.empty)(_ combine _)
}

private def irisToLabels(readableIdentifiers: List[OWLAnnotationProperty], binding: Binding, dosdp: ExpandedDOSDP, index: Map[IRI, Map[IRI, String]]): Binding = binding match {
case SingleValue(value) => SingleValue(Prefixes.idToIRI(value, dosdp.prefixes).map(iri => readableIdentifierForIRI(readableIdentifiers, iri, dosdp, index)).getOrElse(value))
case MultiValue(values) => MultiValue(values.map(value => Prefixes.idToIRI(value, dosdp.prefixes).map(iri => readableIdentifierForIRI(readableIdentifiers, iri, dosdp, index)).getOrElse(value)))
Expand Down
Loading
Loading