Skip to content

Commit 8ef593c

Browse files
Read Zarr Connectome Files (#8717)
- Support reading connectome files registered as attachment - read the new zarr3-based format - for hdf5 connectome files, always assume static list of synapse type names (there only ever was this static list, except for very few exceptions, compare https://scm.slack.com/archives/C5AKLAV0B/p1750852209211939?thread_ts=1705502230.128199&cid=C5AKLAV0B ### Steps to test: - In connectome tab, load synapses both from hdf5 and zarr connectome file, should work as expected. ### TODOs: - [x] list + look up connectome file keys - [x] delegate to correct service - [x] re-connnect hdf5 connectome file service - [x] How to deal with synapse type names for legacy files? Compare [slack thread](https://scm.slack.com/archives/C5AKLAV0B/p1750851969007069?thread_ts=1705502230.128199&cid=C5AKLAV0B) - [x] implement zarr connectome file service - [x] enable cache clear - [x] test - [x] extract string constants like for the other attachments ### Issues: - contributes to #8618 - contributes to #8567 ------ - [x] Added changelog entry (create a `$PR_NUMBER.md` file in `unreleased_changes` or use `./tools/create-changelog-entry.py`) - [x] Removed dev-only changes like prints and application.conf edits - [x] Considered [common edge cases](../blob/master/.github/common_edge_cases.md) - [x] Needs datastore update after deployment --------- Co-authored-by: MichaelBuessemeyer <39529669+MichaelBuessemeyer@users.noreply.github.com>
1 parent 531a578 commit 8ef593c

File tree

15 files changed

+799
-351
lines changed

15 files changed

+799
-351
lines changed

unreleased_changes/8717.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
### Added
2+
- Connectomes can now also be read from the new zarr3-based format, and from remote object storage.

util/src/main/scala/com/scalableminds/util/collections/SequenceUtils.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.scalableminds.util.collections
22

3+
import scala.collection.Searching.{Found, InsertionPoint}
4+
35
object SequenceUtils {
46
def findUniqueElement[T](list: Seq[T]): Option[T] = {
57
val uniqueElements = list.distinct
@@ -51,4 +53,11 @@ object SequenceUtils {
5153
val batchTo = Math.min(to, (batchIndex + 1) * batchSize + from - 1)
5254
(batchFrom, batchTo)
5355
}
56+
57+
// Search in a sorted array, returns Box of index where element is found or, if missing, where element would be inserted
58+
def searchSorted(haystack: Array[Long], needle: Long): Int =
59+
haystack.search(needle) match {
60+
case Found(i) => i
61+
case InsertionPoint(i) => i
62+
}
5463
}

webknossos-datastore/app/com/scalableminds/webknossos/datastore/DataStoreModule.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import org.apache.pekko.actor.ActorSystem
44
import com.google.inject.AbstractModule
55
import com.google.inject.name.Names
66
import com.scalableminds.webknossos.datastore.services._
7+
import com.scalableminds.webknossos.datastore.services.connectome.{
8+
ConnectomeFileService,
9+
Hdf5ConnectomeFileService,
10+
ZarrConnectomeFileService
11+
}
712
import com.scalableminds.webknossos.datastore.services.mapping.{
813
AgglomerateService,
914
Hdf5AgglomerateService,
@@ -52,6 +57,9 @@ class DataStoreModule extends AbstractModule {
5257
bind(classOf[SegmentIndexFileService]).asEagerSingleton()
5358
bind(classOf[ZarrSegmentIndexFileService]).asEagerSingleton()
5459
bind(classOf[Hdf5SegmentIndexFileService]).asEagerSingleton()
60+
bind(classOf[ConnectomeFileService]).asEagerSingleton()
61+
bind(classOf[ZarrConnectomeFileService]).asEagerSingleton()
62+
bind(classOf[Hdf5ConnectomeFileService]).asEagerSingleton()
5563
bind(classOf[NeuroglancerPrecomputedMeshFileService]).asEagerSingleton()
5664
bind(classOf[RemoteSourceDescriptorService]).asEagerSingleton()
5765
bind(classOf[ChunkCacheService]).asEagerSingleton()

webknossos-datastore/app/com/scalableminds/webknossos/datastore/controllers/DataSourceController.scala

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ import com.scalableminds.webknossos.datastore.services.uploading._
2525
import com.scalableminds.webknossos.datastore.storage.DataVaultService
2626
import com.scalableminds.util.tools.Box.tryo
2727
import com.scalableminds.util.tools.{Box, Empty, Failure, Full}
28+
import com.scalableminds.webknossos.datastore.services.connectome.{
29+
ByAgglomerateIdsRequest,
30+
BySynapseIdsRequest,
31+
ConnectomeFileService,
32+
SynapticPartnerDirection
33+
}
2834
import com.scalableminds.webknossos.datastore.services.mapping.AgglomerateService
2935
import play.api.data.Form
3036
import play.api.data.Forms.{longNumber, nonEmptyText, number, tuple}
@@ -452,14 +458,16 @@ class DataSourceController @Inject()(
452458
meshFileService.clearCache(dataSourceId, layerName)
453459
val closedSegmentIndexFileHandleCount =
454460
segmentIndexFileService.clearCache(dataSourceId, layerName)
461+
val closedConnectomeFileHandleCount =
462+
connectomeFileService.clearCache(dataSourceId, layerName)
455463
val reloadedDataSource: InboxDataSource = dataSourceService.dataSourceFromDir(
456464
dataSourceService.dataBaseDir.resolve(organizationId).resolve(datasetDirectoryName),
457465
organizationId)
458466
datasetErrorLoggingService.clearForDataset(organizationId, datasetDirectoryName)
459467
val clearedVaultCacheEntriesOpt = dataSourceService.invalidateVaultCache(reloadedDataSource, layerName)
460468
clearedVaultCacheEntriesOpt.foreach { clearedVaultCacheEntries =>
461469
logger.info(
462-
s"Cleared caches for ${layerName.map(l => s"layer '$l' of ").getOrElse("")}dataset $organizationId/$datasetDirectoryName: closed $closedAgglomerateFileHandleCount agglomerate file handles, $closedMeshFileHandleCount mesh file handles, $closedSegmentIndexFileHandleCount segment index file handles, removed $clearedBucketProviderCount bucketProviders, $clearedVaultCacheEntries vault cache entries and $removedChunksCount image chunk cache entries.")
470+
s"Cleared caches for ${layerName.map(l => s"layer '$l' of ").getOrElse("")}dataset $organizationId/$datasetDirectoryName: closed $closedAgglomerateFileHandleCount agglomerate file handles, $closedMeshFileHandleCount mesh file handles, $closedSegmentIndexFileHandleCount segment index file handles, $closedConnectomeFileHandleCount connectome file handles, removed $clearedBucketProviderCount bucketProviders, $clearedVaultCacheEntries vault cache entries and $removedChunksCount image chunk cache entries.")
463471
}
464472
reloadedDataSource
465473
}
@@ -510,21 +518,12 @@ class DataSourceController @Inject()(
510518
Action.async { implicit request =>
511519
accessTokenService.validateAccessFromTokenContext(
512520
UserAccessRequest.readDataSources(DataSourceId(datasetDirectoryName, organizationId))) {
513-
val connectomeFileNames =
514-
connectomeFileService.exploreConnectomeFiles(organizationId, datasetDirectoryName, dataLayerName)
515521
for {
516-
mappingNames <- Fox.serialCombined(connectomeFileNames.toList) { connectomeFileName =>
517-
val path =
518-
connectomeFileService.connectomeFilePath(organizationId,
519-
datasetDirectoryName,
520-
dataLayerName,
521-
connectomeFileName)
522-
connectomeFileService.mappingNameForConnectomeFile(path)
523-
}
524-
connectomesWithMappings = connectomeFileNames
525-
.zip(mappingNames)
526-
.map(tuple => ConnectomeFileNameWithMappingName(tuple._1, tuple._2))
527-
} yield Ok(Json.toJson(connectomesWithMappings))
522+
(dataSource, dataLayer) <- dataSourceRepository.getDataSourceAndDataLayer(organizationId,
523+
datasetDirectoryName,
524+
dataLayerName)
525+
connectomeFileInfos <- connectomeFileService.listConnectomeFiles(dataSource.id, dataLayer)
526+
} yield Ok(Json.toJson(connectomeFileInfos))
528527
}
529528
}
530529

@@ -535,10 +534,13 @@ class DataSourceController @Inject()(
535534
accessTokenService.validateAccessFromTokenContext(
536535
UserAccessRequest.readDataSources(DataSourceId(datasetDirectoryName, organizationId))) {
537536
for {
538-
meshFilePath <- Fox.successful(
539-
connectomeFileService
540-
.connectomeFilePath(organizationId, datasetDirectoryName, dataLayerName, request.body.connectomeFile))
541-
synapses <- connectomeFileService.synapsesForAgglomerates(meshFilePath, request.body.agglomerateIds)
537+
(dataSource, dataLayer) <- dataSourceRepository.getDataSourceAndDataLayer(organizationId,
538+
datasetDirectoryName,
539+
dataLayerName)
540+
meshFileKey <- connectomeFileService.lookUpConnectomeFileKey(dataSource.id,
541+
dataLayer,
542+
request.body.connectomeFile)
543+
synapses <- connectomeFileService.synapsesForAgglomerates(meshFileKey, request.body.agglomerateIds)
542544
} yield Ok(Json.toJson(synapses))
543545
}
544546
}
@@ -551,12 +553,18 @@ class DataSourceController @Inject()(
551553
accessTokenService.validateAccessFromTokenContext(
552554
UserAccessRequest.readDataSources(DataSourceId(datasetDirectoryName, organizationId))) {
553555
for {
554-
meshFilePath <- Fox.successful(
555-
connectomeFileService
556-
.connectomeFilePath(organizationId, datasetDirectoryName, dataLayerName, request.body.connectomeFile))
557-
agglomerateIds <- connectomeFileService.synapticPartnerForSynapses(meshFilePath,
556+
directionValidated <- SynapticPartnerDirection
557+
.fromString(direction)
558+
.toFox ?~> "could not parse synaptic partner direction"
559+
(dataSource, dataLayer) <- dataSourceRepository.getDataSourceAndDataLayer(organizationId,
560+
datasetDirectoryName,
561+
dataLayerName)
562+
meshFileKey <- connectomeFileService.lookUpConnectomeFileKey(dataSource.id,
563+
dataLayer,
564+
request.body.connectomeFile)
565+
agglomerateIds <- connectomeFileService.synapticPartnerForSynapses(meshFileKey,
558566
request.body.synapseIds,
559-
direction)
567+
directionValidated)
560568
} yield Ok(Json.toJson(agglomerateIds))
561569
}
562570
}
@@ -568,10 +576,13 @@ class DataSourceController @Inject()(
568576
accessTokenService.validateAccessFromTokenContext(
569577
UserAccessRequest.readDataSources(DataSourceId(datasetDirectoryName, organizationId))) {
570578
for {
571-
meshFilePath <- Fox.successful(
572-
connectomeFileService
573-
.connectomeFilePath(organizationId, datasetDirectoryName, dataLayerName, request.body.connectomeFile))
574-
synapsePositions <- connectomeFileService.positionsForSynapses(meshFilePath, request.body.synapseIds)
579+
(dataSource, dataLayer) <- dataSourceRepository.getDataSourceAndDataLayer(organizationId,
580+
datasetDirectoryName,
581+
dataLayerName)
582+
meshFileKey <- connectomeFileService.lookUpConnectomeFileKey(dataSource.id,
583+
dataLayer,
584+
request.body.connectomeFile)
585+
synapsePositions <- connectomeFileService.positionsForSynapses(meshFileKey, request.body.synapseIds)
575586
} yield Ok(Json.toJson(synapsePositions))
576587
}
577588
}
@@ -583,10 +594,13 @@ class DataSourceController @Inject()(
583594
accessTokenService.validateAccessFromTokenContext(
584595
UserAccessRequest.readDataSources(DataSourceId(datasetDirectoryName, organizationId))) {
585596
for {
586-
meshFilePath <- Fox.successful(
587-
connectomeFileService
588-
.connectomeFilePath(organizationId, datasetDirectoryName, dataLayerName, request.body.connectomeFile))
589-
synapseTypes <- connectomeFileService.typesForSynapses(meshFilePath, request.body.synapseIds)
597+
(dataSource, dataLayer) <- dataSourceRepository.getDataSourceAndDataLayer(organizationId,
598+
datasetDirectoryName,
599+
dataLayerName)
600+
meshFileKey <- connectomeFileService.lookUpConnectomeFileKey(dataSource.id,
601+
dataLayer,
602+
request.body.connectomeFile)
603+
synapseTypes <- connectomeFileService.typesForSynapses(meshFileKey, request.body.synapseIds)
590604
} yield Ok(Json.toJson(synapseTypes))
591605
}
592606
}

webknossos-datastore/app/com/scalableminds/webknossos/datastore/datareaders/DatasetArray.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ class DatasetArray(vaultPath: VaultPath,
193193
tc: TokenContext): Fox[MultiArray] =
194194
if (shape.contains(0)) {
195195
Fox.successful(MultiArrayUtils.createEmpty(header.resolvedDataType, rank))
196+
} else if (shape.exists(_ < 0)) {
197+
Fox.failure(s"Trying to read negative shape from DatasetArray: ${shape.mkString(",")}")
196198
} else {
197199
val totalOffset: Array[Long] = offset.zip(header.voxelOffset).map { case (o, v) => o - v }.padTo(offset.length, 0)
198200
val chunkIndices = ChunkUtils.computeChunkIndices(datasetShape, chunkShape, shape, totalOffset)

0 commit comments

Comments
 (0)