diff --git a/src/main/scala/AssignTraceIds.scala b/src/main/scala/AssignTraceIds.scala index 00889380..d565f8bd 100644 --- a/src/main/scala/AssignTraceIds.scala +++ b/src/main/scala/AssignTraceIds.scala @@ -37,7 +37,7 @@ import at.ac.oeaw.imba.gerlich.gerlib.syntax.all.* import at.ac.oeaw.imba.gerlich.looptrace.ImagingRoundsConfiguration.{ RoiPartnersRequirementType, - TraceIdDefinitionAndFiltrationRule, + TraceIdDefinitionRule, } import at.ac.oeaw.imba.gerlich.looptrace.cli.ScoptCliReaders import at.ac.oeaw.imba.gerlich.looptrace.csv.ColumnNames.{ @@ -113,7 +113,7 @@ object AssignTraceIds extends ScoptCliReaders, StrictLogging: } private def definePairwiseDistanceThresholds( - rules: NonEmptyList[TraceIdDefinitionAndFiltrationRule], + rules: NonEmptyList[TraceIdDefinitionRule], ): Map[(ImagingTimepoint, ImagingTimepoint), DistanceThreshold] = import AtLeast2.syntax.toList rules.map(_.mergeGroup) @@ -139,7 +139,7 @@ object AssignTraceIds extends ScoptCliReaders, StrictLogging: } private def computeNeighborsGraph( - rules: NonEmptyList[TraceIdDefinitionAndFiltrationRule], + rules: NonEmptyList[TraceIdDefinitionRule], pixels: Pixels3D, )(records: NonEmptyList[InputRecord]): SimplestGraph[RoiIndex] = val lookupProximity: Map[(ImagingTimepoint, ImagingTimepoint), ProximityComparable[InputRecord]] = @@ -197,13 +197,13 @@ object AssignTraceIds extends ScoptCliReaders, StrictLogging: TraceId.unsafe(NonnegativeInt(1) + maxRoiId.get) private[looptrace] def labelRecordsWithTraceId( - rules: NonEmptyList[TraceIdDefinitionAndFiltrationRule], + rules: NonEmptyList[TraceIdDefinitionRule], discardIfNotInGroupOfInterest: Boolean, pixels: Pixels3D, )(records: NonEmptyList[InputRecord]): NonEmptyList[OutputRecord] = /* Necessary imports and type aliases */ import AtLeast2.syntax.{ remove, toNes, toSet } - type TimepointExpectationLookup = NonEmptyMap[ImagingTimepoint, TraceIdDefinitionAndFiltrationRule] + type TimepointExpectationLookup = NonEmptyMap[ImagingTimepoint, TraceIdDefinitionRule] val lookupRecord: NonEmptyMap[RoiIndex, InputRecord] = records.map(r => r.index -> r).toNem val lookupRule: TimepointExpectationLookup = @@ -252,7 +252,7 @@ object AssignTraceIds extends ScoptCliReaders, StrictLogging: .flatMap{ r => lookupRule.apply(r.timepoint) } .toNel .fold(!discardIfNotInGroupOfInterest){ rules => - given Eq[TraceIdDefinitionAndFiltrationRule] = Eq.fromUniversalEquals + given Eq[TraceIdDefinitionRule] = Eq.fromUniversalEquals val nUniqueRules = rules.toList.toSet.size if nUniqueRules =!= 1 then throw new Exception( diff --git a/src/main/scala/ImagingRoundsConfiguration.scala b/src/main/scala/ImagingRoundsConfiguration.scala index 5448f451..3b73b30d 100644 --- a/src/main/scala/ImagingRoundsConfiguration.scala +++ b/src/main/scala/ImagingRoundsConfiguration.scala @@ -31,7 +31,7 @@ final case class ImagingRoundsConfiguration private( sequence: ImagingSequence, locusGrouping: Set[ImagingRoundsConfiguration.LocusGroup], // should be empty iff there are no locus rounds in the sequence proximityFilterStrategy: ImagingRoundsConfiguration.ProximityFilterStrategy, - private val maybeMergeRulesForTracing: Option[(NonEmptyList[ImagingRoundsConfiguration.TraceIdDefinitionAndFiltrationRule], Boolean)], + private val maybeMergeRulesForTracing: Option[(NonEmptyList[ImagingRoundsConfiguration.TraceIdDefinitionRule], Boolean)], // TODO: We could, by default, skip regional and blank imaging rounds (but do use repeats). tracingExclusions: Set[ImagingTimepoint], // Timepoints of imaging rounds to not use for tracing ): @@ -40,7 +40,7 @@ final case class ImagingRoundsConfiguration private( /** Simply take the rounds from the contained imagingRounds sequence. */ final def allRounds: NonEmptyList[ImagingRound] = sequence.allRounds - final def mergeRules: Option[NonEmptyList[ImagingRoundsConfiguration.TraceIdDefinitionAndFiltrationRule]] = + final def mergeRules: Option[NonEmptyList[ImagingRoundsConfiguration.TraceIdDefinitionRule]] = maybeMergeRulesForTracing.map(_._1) final def discardRoisNotInGroupsOfInterest: Boolean = maybeMergeRulesForTracing.fold(false)(_._2) @@ -128,7 +128,7 @@ object ImagingRoundsConfiguration extends LazyLogging: locusGrouping: Set[LocusGroup], proximityFilterStrategy: ProximityFilterStrategy, tracingExclusions: Set[ImagingTimepoint], - maybeMergeRules: Option[(NonEmptyList[TraceIdDefinitionAndFiltrationRule], Boolean)], + maybeMergeRules: Option[(NonEmptyList[TraceIdDefinitionRule], Boolean)], checkLocusTimepointCovering: Boolean, ): ErrMsgsOr[ImagingRoundsConfiguration] = { val knownTimes = sequence.allTimepoints @@ -213,7 +213,7 @@ object ImagingRoundsConfiguration extends LazyLogging: // Then, find repeated locus timepoints WITHIN each merge group. type GroupId = Int val repeatsByGroup: Map[GroupId, NonEmptyMap[ImagingTimepoint, AtLeast2[Set, ImagingTimepoint]]] = - def processOneRule = (r: TraceIdDefinitionAndFiltrationRule) => + def processOneRule = (r: TraceIdDefinitionRule) => r.mergeGroup.members.toList.foldRight(Map.empty[ImagingTimepoint, NonEmptySet[ImagingTimepoint]]){ (rt, acc) => locusTimesByRegional .get(rt) @@ -377,14 +377,14 @@ object ImagingRoundsConfiguration extends LazyLogging: .map(_.toSet) .toValidatedNel } - val maybeMergeRulesNel: ValidatedNel[String, Option[(NonEmptyList[TraceIdDefinitionAndFiltrationRule], Boolean)]] = + val maybeMergeRulesNel: ValidatedNel[String, Option[(NonEmptyList[TraceIdDefinitionRule], Boolean)]] = val sectionKey = "mergeRulesForTracing" data.get(sectionKey) match { case None | Some(ujson.Null) => logger.debug(s"No section '$sectionKey', ignoring") Validated.Valid(None) case Some(jsonData) => - TraceIdDefinitionAndFiltrationRulesSet.fromJson(jsonData).toValidated.map(_.some) + TraceIdDefinitionRulesSet.fromJson(jsonData).toValidated.map(_.some) } val checkLocusTimepointCoveringNel: ValidatedNel[String, Boolean] = data.get("checkLocusTimepointCovering") match { @@ -409,7 +409,7 @@ object ImagingRoundsConfiguration extends LazyLogging: locusGrouping: Set[LocusGroup], proximityFilterStrategy: ProximityFilterStrategy, tracingExclusions: Set[ImagingTimepoint], - maybeMergeRules: Option[(NonEmptyList[TraceIdDefinitionAndFiltrationRule], Boolean)], + maybeMergeRules: Option[(NonEmptyList[TraceIdDefinitionRule], Boolean)], checkLocusTimepointCoveringNel: Boolean, ): ImagingRoundsConfiguration = build( @@ -519,18 +519,18 @@ object ImagingRoundsConfiguration extends LazyLogging: end ProximityGroup /** How to redefine trace IDs and filter ROIs on the basis of proximity to one another */ - final case class TraceIdDefinitionAndFiltrationRule( + final case class TraceIdDefinitionRule( mergeGroup: ProximityGroup[EuclideanDistance.Threshold, ImagingTimepoint], requirement: RoiPartnersRequirementType, ) /** Helpers for working with the data type for trace ID definition and filtration */ - object TraceIdDefinitionAndFiltrationRulesSet: + object TraceIdDefinitionRulesSet: /** The key which maps to the collection of groups */ private val groupsKey = "groups" private val strictnessKey = "discardRoisNotInGroupsOfInterest" - def fromJson(json: ujson.Readable): EitherNel[String, (NonEmptyList[TraceIdDefinitionAndFiltrationRule], Boolean)] = + def fromJson(json: ujson.Readable): EitherNel[String, (NonEmptyList[TraceIdDefinitionRule], Boolean)] = Try{ read[Map[String, ujson.Value]](json) } .toEither .leftMap(e => NonEmptyList.one(s"Cannot read JSON as key-value pairs: ${e.getMessage}")) @@ -576,7 +576,7 @@ object ImagingRoundsConfiguration extends LazyLogging: case (Some(threshold), None) => NonEmptyList.one("Missing requirement type for ROI merge").asLeft case (None, Some(reqType)) => NonEmptyList.one("Missing threshold for ROI merge").asLeft case (Some(threshold), Some(reqType)) => - groups.map{ g => TraceIdDefinitionAndFiltrationRule( + groups.map{ g => TraceIdDefinitionRule( ProximityGroup(threshold, g), reqType, ) @@ -618,7 +618,7 @@ object ImagingRoundsConfiguration extends LazyLogging: AtLeast2.unsafe(uniques) ) } - end TraceIdDefinitionAndFiltrationRulesSet + end TraceIdDefinitionRulesSet /** Check list of items for nonemptiness. */ private def liftToNel[A](as: List[A], context: Option[String] = None): Either[String, NonEmptyList[A]] = diff --git a/src/test/scala/TestImagingRoundsConfigurationExamplesParsability.scala b/src/test/scala/TestImagingRoundsConfigurationExamplesParsability.scala index 85f10755..93027190 100644 --- a/src/test/scala/TestImagingRoundsConfigurationExamplesParsability.scala +++ b/src/test/scala/TestImagingRoundsConfigurationExamplesParsability.scala @@ -21,7 +21,7 @@ import at.ac.oeaw.imba.gerlich.looptrace.ImagingRoundsConfiguration.{ RoiPartnersRequirementType, SelectiveProximityPermission, SelectiveProximityProhibition, - TraceIdDefinitionAndFiltrationRule, + TraceIdDefinitionRule, UniversalProximityPermission, UniversalProximityProhibition, } @@ -218,7 +218,7 @@ class TestImagingRoundsConfigurationExamplesParsability extends AnyFunSuite with NonEmptyList.of(Set(6, 7), Set(8, 9)) .map(_.map(ImagingTimepoint.unsafe).pipe(AtLeast2.unsafe)) .map{ g => - TraceIdDefinitionAndFiltrationRule( + TraceIdDefinitionRule( ProximityGroup(expDistance, g), RoiPartnersRequirementType.Conjunctive, ) @@ -262,7 +262,7 @@ class TestImagingRoundsConfigurationExamplesParsability extends AnyFunSuite with check = (msg: String, exp: String) => msg.startsWith(exp) // Here we just to prefix check. ) - private def checkParseSuccess(configFile: os.Path, expectedMergeParseResult: (Boolean, NonEmptyList[TraceIdDefinitionAndFiltrationRule])) = + private def checkParseSuccess(configFile: os.Path, expectedMergeParseResult: (Boolean, NonEmptyList[TraceIdDefinitionRule])) = val (expFilter, expRules) = expectedMergeParseResult ImagingRoundsConfiguration.fromJsonFile(configFile) match { case Left(messages) => @@ -276,7 +276,7 @@ class TestImagingRoundsConfigurationExamplesParsability extends AnyFunSuite with val mergeRulesNel = conf.mergeRules match { case None => "No merge rules section".invalidNel case Some(obsRules) => - given Eq[TraceIdDefinitionAndFiltrationRule] = Eq.fromUniversalEquals + given Eq[TraceIdDefinitionRule] = Eq.fromUniversalEquals if obsRules === expRules then ().validNel else f"Observed rules ($obsRules) don't match expected ($expRules)".invalidNel diff --git a/src/test/scala/TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig.scala b/src/test/scala/TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig.scala index e8cc0823..aff51898 100644 --- a/src/test/scala/TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig.scala +++ b/src/test/scala/TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig.scala @@ -17,9 +17,9 @@ import at.ac.oeaw.imba.gerlich.gerlib.testing.instances.all.given import at.ac.oeaw.imba.gerlich.looptrace.ImagingRoundsConfiguration.{ ProximityGroup, RoiPartnersRequirementType, - TraceIdDefinitionAndFiltrationRulesSet, + TraceIdDefinitionRulesSet, } -import at.ac.oeaw.imba.gerlich.looptrace.ImagingRoundsConfiguration.TraceIdDefinitionAndFiltrationRule +import at.ac.oeaw.imba.gerlich.looptrace.ImagingRoundsConfiguration.TraceIdDefinitionRule import at.ac.oeaw.imba.gerlich.gerlib.numeric.NonnegativeReal import at.ac.oeaw.imba.gerlich.gerlib.imaging.ImagingTimepoint @@ -67,23 +67,23 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends forAll: (threshold: EuclideanDistance.Threshold, requirement: RoiPartnersRequirementType, groups: NonEmptyList[AtLeast2[Set, ImagingTimepoint]], strict: Boolean) => val groupsJson = groups.map(ids => "[" ++ ids.toList.map(_.show_).mkString(", ") ++ "]").mkString_(", ") val json = s"""{"discardRoisNotInGroupsOfInterest": $strict, "distanceThreshold": ${threshold.get.show_}, "requirementType": "${requirement}", "groups": [${groupsJson}]}""" - TraceIdDefinitionAndFiltrationRulesSet.fromJson(json) match { + TraceIdDefinitionRulesSet.fromJson(json) match { case Left(messages) => fail(s"${messages.length} error(s) decoding JSON: ${messages.mkString_("; ")}" ++ "\n" ++ json) case Right(parsedResult) => - val expGroups = groups.map{ g => TraceIdDefinitionAndFiltrationRule(ProximityGroup(threshold, g), requirement) } + val expGroups = groups.map{ g => TraceIdDefinitionRule(ProximityGroup(threshold, g), requirement) } parsedResult shouldEqual (expGroups, strict) } test("Simple single-group example parses as expected"): val strict = true val json = s"""{"discardRoisNotInGroupsOfInterest": $strict, "distanceThreshold": 5, "requirementType": "Conjunctive", "groups": [[9, 10]]}""" - TraceIdDefinitionAndFiltrationRulesSet.fromJson(json) match { + TraceIdDefinitionRulesSet.fromJson(json) match { case Left(messages) => fail(s"${messages.length} error(s) decoding JSON: ${messages.mkString_("; ")}" ++ "\n" ++ json) case Right(parsedResult) => import io.github.iltotore.iron.autoRefine - val expectation = TraceIdDefinitionAndFiltrationRule( + val expectation = TraceIdDefinitionRule( ProximityGroup( EuclideanDistance.Threshold(5: NonnegativeReal), AtLeast2.unsafe(Set(9, 10).map(ImagingTimepoint.unsafeLift)) @@ -99,15 +99,15 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends EuclideanDistance.Threshold(5: NonnegativeReal) val strict = true val json = s"""{"discardRoisNotInGroupsOfInterest": $strict, "distanceThreshold": ${threshold.get.show_}, "requirementType": "Conjunctive", "groups": [[1, 0], [9, 10]]}""" - TraceIdDefinitionAndFiltrationRulesSet.fromJson(json) match { + TraceIdDefinitionRulesSet.fromJson(json) match { case Left(messages) => fail(s"${messages.length} error(s) decoding JSON: ${messages.mkString_("; ")}" ++ "\n" ++ json) case Right(parsedResult) => - val exp1 = TraceIdDefinitionAndFiltrationRule( + val exp1 = TraceIdDefinitionRule( ProximityGroup(threshold, AtLeast2.unsafe(Set(1, 0).map(ImagingTimepoint.unsafeLift))), RoiPartnersRequirementType.Conjunctive, ) - val exp2 = TraceIdDefinitionAndFiltrationRule( + val exp2 = TraceIdDefinitionRule( ProximityGroup(threshold, AtLeast2.unsafe(Set(9, 10).map(ImagingTimepoint.unsafeLift))), RoiPartnersRequirementType.Conjunctive, ) @@ -115,7 +115,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("Without requirement type, no parse"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( s"""{"discardRoisNotInGroupsOfInterest": true, "distanceThreshold": 5, "groups": [[9, 10]]}""" ) match { case Left(messages) => @@ -125,7 +125,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("Without threshold, no parse"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( s"""{"discardRoisNotInGroupsOfInterest": true, "requirementType": "Conjunctive", "groups": [[9, 10]]}""" ) match { case Left(messages) => @@ -135,7 +135,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("Threshold must be nonnegative"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( s"""{"discardRoisNotInGroupsOfInterest": true, "distanceThreshold": -1, "requirementType": "Conjunctive", "groups": [[9, 10]]}""" ) match { case Left(messages) => @@ -145,7 +145,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("Each grouping member must have at least two members"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( s"""{"discardRoisNotInGroupsOfInterest": true, "distanceThreshold": 5, "requirementType": "Conjunctive", "groups": [[9, 10], [0]]}""" ) match { case Left(messages) => @@ -155,7 +155,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("No member may be repeated within a group"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( s"""{"discardRoisNotInGroupsOfInterest": true, "distanceThreshold": 5, "requirementType": "Conjunctive", "groups": [[9, 10, 9], [0, 1]]}""" ) match { case Left(messages) => @@ -165,7 +165,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("No member may be repeated between groups"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( s"""{"discardRoisNotInGroupsOfInterest": true, "distanceThreshold": 5, "requirementType": "Conjunctive", "groups": [[9, 10], [0, 9, 1]]}""" ) match { case Left(messages) => @@ -175,7 +175,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("Requirement type must be one of the fixed values"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( s"""{"discardRoisNotInGroupsOfInterest": true, "distanceThreshold": 5, "requirementType": "NotARealValue", "groups": [[9, 10]]}""" ) match { case Left(messages) => @@ -185,7 +185,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("With neither threshold nor requirement type, no parse"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( s"""{"discardRoisNotInGroupsOfInterest": true, "groups": [[9, 10]]}""" ) match { case Left(messages) => @@ -195,7 +195,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("With no groups, no parse"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( s"""{"discardRoisNotInGroupsOfInterest": true, "distanceThreshold": 5, "requirementType": "Conjunctive"}""" ) match { case Left(messages) => @@ -205,7 +205,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("With empty groups, no parse"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( s"""{"discardRoisNotInGroupsOfInterest": true, "distanceThreshold": 5, "requirementType": "Conjunctive", "groups": []}""" ) match { case Left(messages) => @@ -215,7 +215,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("With null groups, no parse"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( s"""{"discardRoisNotInGroupsOfInterest": true, "distanceThreshold": 5, "requirementType": "Conjunctive", "groups": null}""" ) match { case Left(messages) => @@ -225,7 +225,7 @@ class TestParseDifferentTimepointsRoiMergeSectionOfImagingRoundsConfig extends } test("Without specification of discard policy, parse fails"): - TraceIdDefinitionAndFiltrationRulesSet.fromJson( + TraceIdDefinitionRulesSet.fromJson( """{"distanceThreshold": 5, "requirementType": "Conjunctive", "groups": [[9, 10]]}""" ) match { case Left(messages) =>