Skip to content

Commit

Permalink
Add pre and post synaptic pathways to BG -> ES projection
Browse files Browse the repository at this point in the history
  • Loading branch information
shinyhappydan committed Oct 25, 2023
1 parent 66113f4 commit 7d63947
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.client.{ElasticSearch
import ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.indexing.{ElasticSearchSink, GraphResourceToDocument}
import ch.epfl.bluebrain.nexus.delta.rdf.RdfError
import ch.epfl.bluebrain.nexus.delta.rdf.graph.Graph
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi}
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi, JsonLdOptions}
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution}
import ch.epfl.bluebrain.nexus.delta.rdf.query.SparqlQuery.SparqlConstructQuery
import ch.epfl.bluebrain.nexus.delta.rdf.syntax.iriStringContextSyntax
Expand Down Expand Up @@ -123,6 +123,7 @@ final class Batch[SinkFormat](
/** Replaces the graph of a provided [[GraphResource]] by extracting its new graph from the provided (full) graph. */
private def replaceGraph(gr: GraphResource, fullGraph: Graph) = {
implicit val api: JsonLdApi = JsonLdJavaApi.lenient
implicit val options = JsonLdOptions.AlwaysEmbed
fullGraph
.replaceRootNode(iri"${gr.id}/alias")
.toCompactedJsonLd(ContextValue.empty)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package ch.epfl.bluebrain.nexus.delta.plugins.elasticsearch.indexing

import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi}
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi, JsonLdOptions}
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.{ContextValue, RemoteContextResolution}
import ch.epfl.bluebrain.nexus.delta.sdk.implicits._
Expand Down Expand Up @@ -33,6 +33,7 @@ final class GraphResourceToDocument(context: ContextValue, includeContext: Boole

/** Given a [[GraphResource]] returns a JSON-LD created from the merged graph and metadata graph */
def graphToDocument(element: GraphResource): Task[Option[Json]] = {
implicit val jsonLdOptions = JsonLdOptions.AlwaysEmbed
val graph = element.graph ++ element.metadataGraph
val json =
if (element.source.isEmpty())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ final case class JsonLdOptions(

object JsonLdOptions {
implicit val defaults: JsonLdOptions = JsonLdOptions()
val AlwaysEmbed: JsonLdOptions = defaults.copy(embed = "@always")
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package ch.epfl.bluebrain.nexus.delta.rdf.jsonld

import ch.epfl.bluebrain.nexus.delta.rdf.{Fixtures, GraphHelpers}
import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.BNode
import ch.epfl.bluebrain.nexus.delta.rdf.implicits._
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.ContextValue
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords
import ch.epfl.bluebrain.nexus.delta.rdf.{Fixtures, GraphHelpers}
import ch.epfl.bluebrain.nexus.testkit.scalatest.ce.CatsEffectSpec

class CompactedJsonLdSpec extends CatsEffectSpec with Fixtures with GraphHelpers {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package ch.epfl.bluebrain.nexus.delta.sdk

import cats.effect.IO
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._
import ch.epfl.bluebrain.nexus.delta.rdf.IriOrBNode.Iri
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi}
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.api.{JsonLdApi, JsonLdJavaApi, JsonLdOptions}
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.JsonLdContext.keywords
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.context.RemoteContextResolution
import ch.epfl.bluebrain.nexus.delta.rdf.jsonld.encoder.JsonLdEncoder
import ch.epfl.bluebrain.nexus.delta.sdk.jsonld.JsonLdContent
import ch.epfl.bluebrain.nexus.delta.sdk.model.{BaseUri, ResourceF}
import ch.epfl.bluebrain.nexus.delta.sdk.syntax._
import ch.epfl.bluebrain.nexus.delta.kernel.effect.migration._
import ch.epfl.bluebrain.nexus.delta.sourcing.Serializer
import ch.epfl.bluebrain.nexus.delta.sourcing.model.{EntityType, ProjectRef, ResourceRef}
import ch.epfl.bluebrain.nexus.delta.sourcing.offset.Offset
Expand Down Expand Up @@ -89,9 +89,10 @@ abstract class ResourceShift[State <: ScopedState, A, M](
private def toGraphResource(project: ProjectRef, resource: ResourceF[A])(implicit
cr: RemoteContextResolution
): IO[GraphResource] = {
val content = resourceToContent(resource)
val metadata = content.metadata
val id = resource.resolvedId
implicit val jsonLdOptions = JsonLdOptions.AlwaysEmbed
val content = resourceToContent(resource)
val metadata = content.metadata
val id = resource.resolvedId
for {
graph <- valueJsonLdEncoder.graph(resource.value).toCatsIO
rootGraph = graph.replaceRootNode(id)
Expand All @@ -114,7 +115,7 @@ abstract class ResourceShift[State <: ScopedState, A, M](
)
}

private def encodeMetadata(id: Iri, metadata: Option[M])(implicit cr: RemoteContextResolution) =
private def encodeMetadata(id: Iri, metadata: Option[M])(implicit cr: RemoteContextResolution, opts: JsonLdOptions) =
(metadata, metadataEncoder) match {
case (Some(m), Some(e)) => e.graph(m).toCatsIO.map { g => Some(g.replaceRootNode(id)) }
case (_, _) => IO.none
Expand Down
24 changes: 24 additions & 0 deletions tests/docker/config/construct-query.sparql
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,18 @@ CONSTRUCT {
?alias :campaign ?campaignId .
?campaignId :identifier ?campaignId ;
:name ?campaignName .

## Synapses
?alias :postSynapticPathway ?postSynaptic .
?postSynaptic :about ?postSynapticAbout ;
:label ?postSynapticLabel ;
:notation ?postSynapticNotation .

?alias :preSynapticPathway ?preSynaptic .
?preSynaptic :about ?preSynapticAbout ;
:label ?preSynapticLabel ;
:notation ?preSynapticNotation .

} WHERE {
VALUES ?id { {resource_id} } .
BIND( IRI(concat(str(?id), '/', 'alias')) AS ?alias ) .
Expand Down Expand Up @@ -633,4 +645,16 @@ CONSTRUCT {
} .
} .

# Synapses
OPTIONAL {
?id bmo:synapticPathway / nsg:postSynaptic ?postSynaptic .
?postSynaptic schema:about ?postSynapticAbout ;
rdfs:label ?postSynapticLabel .
OPTIONAL { ?postSynaptic skos:notation ?postSynapticNotation . } .
?id bmo:synapticPathway / nsg:preSynaptic ?preSynaptic .
?preSynaptic schema:about ?preSynapticAbout ;
rdfs:label ?preSynapticLabel .
OPTIONAL { ?preSynaptic skos:notation ?preSynapticNotation . } .

} .
}
6 changes: 6 additions & 0 deletions tests/docker/config/search-context.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
"protocol": {
"@container": "@set"
},
"preSynapticPathway": {
"@container": "@set"
},
"postSynapticPathway": {
"@container": "@set"
},
"project": {
"@type": "@id"
},
Expand Down
93 changes: 93 additions & 0 deletions tests/src/test/resources/kg/search/synapse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{
"@context": "https://bbp.neuroshapes.org",
"@id": "https://bbp.epfl.ch/data/synapse",
"@type": [
"Entity",
"Parameter",
"SchafferCollateralAnatomyParameter",
"SynapsesPerConnection",
"ExperimentalSynapsesPerConnection"
],
"brainLocation": {
"brainRegion": {
"@id": "http://purl.obolibrary.org/obo/UBERON_0003883",
"label": "CA3"
}
},
"comment": "inferred from Sik et al (1993) but unclear exactly how ",
"contribution": {
"@type": "Contribution",
"agent": {
"@id": "https://bbp.epfl.ch/nexus/v1/realms/bbp/users/aantonie",
"@type": "Agent"
}
},
"description": "SynapseDensity parameter from Schaffer axon collateral (pre-synaptic) to PV+ (post-synaptic) neurons. These data are part of the Schaffer collateral anatomy data.",
"name": "SynapseDensity parameter from Schaffer axon collateral (pre-synaptic) to PV+ (post-synaptic) neurons",
"series": [
{
"statistic": "mean",
"unitCode": "synapses/connection",
"value": 1.19
},
{
"statistic": "N synapses",
"unitCode": "dimensionless",
"value": 274
},
{
"statistic": "standard deviation",
"unitCode": "synapses/connection",
"value": 0.48
},
{
"statistic": "standard error of the mean",
"unitCode": "synapses/connection",
"value": 0.028997860479848495
},
{
"statistic": "minimum",
"unitCode": "synapses/connection",
"value": 1
},
{
"statistic": "maximum",
"unitCode": "synapses/connection",
"value": 3
}
],
"subject": {
"@type": "Subject",
"species": {
"@id": "http://purl.obolibrary.org/obo/NCBITaxon_10116",
"label": "Rattus norvegicus"
},
"strain": {
"@id": "http://www.ebi.ac.uk/efo/EFO_0001352",
"label": "Sprague Dawley"
},
"weight": {
"maxValue": 300,
"minValue": 200,
"unitCode": "grams"
}
},
"synapticPathway": {
"postSynaptic": [
{
"@id": "http://api.brain-map.org/api/v2/data/Structure/454",
"about": "OtherBrainRegion",
"label": "Other somatosensory areas",
"notation": "OSS"
}
],
"preSynaptic": [
{
"@id": "http://api.brain-map.org/api/v2/data/Structure/453",
"about": "BrainRegion",
"label": "Somatosensory areas",
"notation": "SS"
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import ch.epfl.bluebrain.nexus.tests.BaseIntegrationSpec
import ch.epfl.bluebrain.nexus.tests.Identity.resources.Rick
import ch.epfl.bluebrain.nexus.tests.iam.types.Permission.{Organizations, Resources}
import io.circe.Json
import org.scalactic.source.Position
import org.scalatest.Assertion

import java.time.Instant
import concurrent.duration._

class SearchConfigSpec extends BaseIntegrationSpec {
class SearchConfigIndexingSpec extends BaseIntegrationSpec {

implicit override def patienceConfig: PatienceConfig = PatienceConfig(config.patience * 2, 300.millis)

Expand All @@ -30,6 +31,7 @@ class SearchConfigSpec extends BaseIntegrationSpec {
private val boutonDensityId = "https://bbp.epfl.ch/data/bouton-density"
private val simulationCampaignId = "https://bbp.epfl.ch/data/simulation-campaign"
private val simulationId = "https://bbp.epfl.ch/data/simulation"
private val synapseId = "https://bbp.epfl.ch/data/synapse"
private val detailedCircuitId = "https://bbp.epfl.ch/data/detailed-circuit"

// the resources that should appear in the search index
Expand All @@ -40,6 +42,7 @@ class SearchConfigSpec extends BaseIntegrationSpec {
"/kg/search/unassessed-trace.json",
"/kg/search/neuron-morphology.json",
"/kg/search/neuron-density.json",
// "/kg/search/synapse.json",
"/kg/search/layer-thickness.json",
"/kg/search/bouton-density.json",
"/kg/search/detailed-circuit.json",
Expand Down Expand Up @@ -784,6 +787,45 @@ class SearchConfigSpec extends BaseIntegrationSpec {
}
}

"have the correct pre synaptic pathway" in {
val query = queryField(synapseId, "preSynapticPathway")
val expected =
json"""{
"preSynapticPathway": [
{
"@id": "http://api.brain-map.org/api/v2/data/Structure/453",
"about": "https://bbp.epfl.ch/neurosciencegraph/data/BrainRegion",
"label": "Somatosensory areas",
"notation": "SS"
}
]
}
"""

assertOneSource(query) { json =>
json should equalIgnoreArrayOrder(expected)
}
}

"have the correct post synaptic pathway" in {
val query = queryField(synapseId, "postSynapticPathway")
val expected =
json"""{
"postSynapticPathway": [
{
"@id": "http://api.brain-map.org/api/v2/data/Structure/454",
"about": "https://bbp.epfl.ch/neurosciencegraph/data/OtherBrainRegion",
"label": "Other somatosensory areas",
"notation": "OSS"
}
]
}
"""

assertOneSource(query) { json =>
json should equalIgnoreArrayOrder(expected)
}
}
}

/**
Expand All @@ -809,20 +851,35 @@ class SearchConfigSpec extends BaseIntegrationSpec {
* Queries ES using the provided query. Asserts that there is only on result in _source. Runs the provided assertion
* on the _source.
*/
private def assertOneSource(query: Json)(assertion: Json => Assertion): IO[Assertion] =
private def assertOneSource(query: Json)(assertion: Json => Assertion)(implicit pos: Position): IO[Assertion] =
eventually {
deltaClient.post[Json]("/search/query", query, Rick) { (body, response) =>
response.status shouldEqual StatusCodes.OK
val sources = Json.fromValues(body.findAllByKey("_source"))
val source = sources.hcursor.downArray.as[Json]
val actual = source.getOrElse(Json.Null)

sources.asArray.value.size shouldBe 1
assertion(actual)
val results = Json
.fromValues(body.findAllByKey("_source"))
.hcursor
.as[List[Json]]
.getOrElse(Nil)

results match {
case single :: Nil => assertion(single)
case Nil =>
fail(
s"Expected exactly 1 source to match query, got 0.\n " +
s"Query was ${query.spaces2}"
)
case many =>
fail(
s"Expected exactly 1 source to match query, got ${many.size}.\n" +
s"Query was: ${query.spaces2}\n" +
s"Results were: $results"
)
}
}
}

private def assertEmpty(query: Json): IO[Assertion] =
private def assertEmpty(query: Json)(implicit pos: Position): IO[Assertion] =
assertOneSource(query)(j => assert(j == json"""{ }"""))

/** Check that a given field in the json can be parsed as [[Instant]] */
Expand Down

0 comments on commit 7d63947

Please sign in to comment.