Skip to content

Commit 6b6188a

Browse files
committed
**feat(genomics): Add coverage expectation profiles with confidence evaluation**
- Introduced `coverage_expectation_profile` table with schema for per-test-type variant calling confidence thresholds. - Added seed data for test types like `WGS`, `BIG_Y_700`, `MT_FULL_SEQUENCE`, and others. - Created `CoverageExpectationProfile` model, repository, and service for CRUD operations and confidence assessment. - Implemented `CoverageExpectationService` for evaluating variant calling confidence and coverage adequacy. - Wrote unit tests to validate confidence tier logic, metrics inputs, and service behavior.
1 parent 84e9794 commit 6b6188a

8 files changed

Lines changed: 870 additions & 0 deletions

File tree

app/models/dal/DatabaseSchema.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ object DatabaseSchema {
8282
val validationServices = TableQuery[ValidationServicesTable]
8383
val testTypeDefinition = TableQuery[TestTypeTable]
8484
val testTypeTargetRegions = TableQuery[TestTypeTargetRegionTable]
85+
val coverageExpectationProfiles = TableQuery[CoverageExpectationProfileTable]
8586

8687
// Consolidated variant schema (replaces variant + variant_alias)
8788
val variantsV2 = TableQuery[VariantV2Table]
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package models.dal.domain.genomics
2+
3+
import models.dal.MyPostgresProfile
4+
import models.dal.MyPostgresProfile.api.*
5+
import models.domain.genomics.CoverageExpectationProfile
6+
import slick.lifted.{ProvenShape, Tag}
7+
8+
import java.time.LocalDateTime
9+
10+
class CoverageExpectationProfileTable(tag: Tag)
11+
extends MyPostgresProfile.api.Table[CoverageExpectationProfile](tag, "coverage_expectation_profile") {
12+
13+
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
14+
def testTypeId = column[Int]("test_type_id")
15+
def contigName = column[String]("contig_name")
16+
def variantClass = column[String]("variant_class")
17+
def minDepthHigh = column[Double]("min_depth_high")
18+
def minDepthMedium = column[Double]("min_depth_medium")
19+
def minDepthLow = column[Double]("min_depth_low")
20+
def minCoveragePct = column[Option[Double]]("min_coverage_pct")
21+
def minMappingQuality = column[Option[Double]]("min_mapping_quality")
22+
def minCallablePct = column[Option[Double]]("min_callable_pct")
23+
def notes = column[Option[String]]("notes")
24+
def createdAt = column[LocalDateTime]("created_at")
25+
def updatedAt = column[LocalDateTime]("updated_at")
26+
27+
override def * : ProvenShape[CoverageExpectationProfile] = (
28+
id.?,
29+
testTypeId,
30+
contigName,
31+
variantClass,
32+
minDepthHigh,
33+
minDepthMedium,
34+
minDepthLow,
35+
minCoveragePct,
36+
minMappingQuality,
37+
minCallablePct,
38+
notes,
39+
createdAt,
40+
updatedAt
41+
).mapTo[CoverageExpectationProfile]
42+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package models.domain.genomics
2+
3+
import play.api.libs.json.{Json, OFormat}
4+
5+
import java.time.LocalDateTime
6+
7+
case class CoverageExpectationProfile(
8+
id: Option[Int] = None,
9+
testTypeId: Int,
10+
contigName: String,
11+
variantClass: String = "SNP",
12+
minDepthHigh: Double,
13+
minDepthMedium: Double,
14+
minDepthLow: Double,
15+
minCoveragePct: Option[Double] = None,
16+
minMappingQuality: Option[Double] = None,
17+
minCallablePct: Option[Double] = None,
18+
notes: Option[String] = None,
19+
createdAt: LocalDateTime = LocalDateTime.now(),
20+
updatedAt: LocalDateTime = LocalDateTime.now()
21+
) {
22+
23+
def confidenceForDepth(actualDepth: Double): String = {
24+
if (actualDepth >= minDepthHigh) "high"
25+
else if (actualDepth >= minDepthMedium) "medium"
26+
else if (actualDepth >= minDepthLow) "low"
27+
else "insufficient"
28+
}
29+
}
30+
31+
object CoverageExpectationProfile {
32+
implicit val format: OFormat[CoverageExpectationProfile] = Json.format[CoverageExpectationProfile]
33+
}
34+
35+
sealed trait VariantClass {
36+
def dbValue: String
37+
}
38+
39+
object VariantClass {
40+
case object SNP extends VariantClass { val dbValue = "SNP" }
41+
case object STR extends VariantClass { val dbValue = "STR" }
42+
case object INDEL extends VariantClass { val dbValue = "INDEL" }
43+
44+
def fromString(s: String): Option[VariantClass] = s.toUpperCase match {
45+
case "SNP" => Some(SNP)
46+
case "STR" => Some(STR)
47+
case "INDEL" => Some(INDEL)
48+
case _ => None
49+
}
50+
}
51+
52+
case class VariantCallingConfidence(
53+
contigName: String,
54+
variantClass: String,
55+
depthConfidence: String,
56+
coverageAdequate: Boolean,
57+
mappingQualityAdequate: Boolean,
58+
callableBasesAdequate: Boolean,
59+
overallConfidence: String,
60+
details: Map[String, String] = Map.empty
61+
)
62+
63+
object VariantCallingConfidence {
64+
implicit val format: OFormat[VariantCallingConfidence] = Json.format[VariantCallingConfidence]
65+
66+
val HIGH = "high"
67+
val MEDIUM = "medium"
68+
val LOW = "low"
69+
val INSUFFICIENT = "insufficient"
70+
}
71+
72+
case class SampleCoverageAssessment(
73+
testTypeCode: String,
74+
testTypeDisplayName: String,
75+
isChipBased: Boolean,
76+
confidences: Seq[VariantCallingConfidence],
77+
overallConfidence: String
78+
)
79+
80+
object SampleCoverageAssessment {
81+
implicit val format: OFormat[SampleCoverageAssessment] = Json.format[SampleCoverageAssessment]
82+
}

app/modules/BaseModule.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,5 +166,9 @@ class BaseModule extends AbstractModule {
166166
bind(classOf[HaplogroupAncestralStrRepository])
167167
.to(classOf[HaplogroupAncestralStrRepositoryImpl])
168168
.asEagerSingleton()
169+
170+
bind(classOf[CoverageExpectationProfileRepository])
171+
.to(classOf[CoverageExpectationProfileRepositoryImpl])
172+
.asEagerSingleton()
169173
}
170174
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package repositories
2+
3+
import jakarta.inject.Inject
4+
import models.dal.DatabaseSchema
5+
import models.domain.genomics.CoverageExpectationProfile
6+
import play.api.Logging
7+
import play.api.db.slick.DatabaseConfigProvider
8+
9+
import scala.concurrent.{ExecutionContext, Future}
10+
11+
trait CoverageExpectationProfileRepository {
12+
def findByTestTypeId(testTypeId: Int): Future[Seq[CoverageExpectationProfile]]
13+
def findByTestTypeAndContig(testTypeId: Int, contigName: String): Future[Seq[CoverageExpectationProfile]]
14+
def findByTestTypeContigAndClass(testTypeId: Int, contigName: String, variantClass: String): Future[Option[CoverageExpectationProfile]]
15+
def create(profile: CoverageExpectationProfile): Future[CoverageExpectationProfile]
16+
def update(profile: CoverageExpectationProfile): Future[Boolean]
17+
def delete(id: Int): Future[Boolean]
18+
}
19+
20+
class CoverageExpectationProfileRepositoryImpl @Inject()(
21+
dbConfigProvider: DatabaseConfigProvider
22+
)(implicit ec: ExecutionContext)
23+
extends BaseRepository(dbConfigProvider)
24+
with CoverageExpectationProfileRepository
25+
with Logging {
26+
27+
import models.dal.MyPostgresProfile.api.*
28+
29+
private val profiles = DatabaseSchema.domain.genomics.coverageExpectationProfiles
30+
31+
override def findByTestTypeId(testTypeId: Int): Future[Seq[CoverageExpectationProfile]] =
32+
runQuery(profiles.filter(_.testTypeId === testTypeId).result)
33+
34+
override def findByTestTypeAndContig(testTypeId: Int, contigName: String): Future[Seq[CoverageExpectationProfile]] =
35+
runQuery(profiles.filter(p => p.testTypeId === testTypeId && p.contigName === contigName).result)
36+
37+
override def findByTestTypeContigAndClass(testTypeId: Int, contigName: String, variantClass: String): Future[Option[CoverageExpectationProfile]] =
38+
runQuery(profiles.filter(p =>
39+
p.testTypeId === testTypeId && p.contigName === contigName && p.variantClass === variantClass
40+
).result.headOption)
41+
42+
override def create(profile: CoverageExpectationProfile): Future[CoverageExpectationProfile] =
43+
runQuery(
44+
(profiles returning profiles.map(_.id) into ((p, id) => p.copy(id = Some(id)))) += profile
45+
)
46+
47+
override def update(profile: CoverageExpectationProfile): Future[Boolean] = profile.id match {
48+
case None => Future.successful(false)
49+
case Some(id) =>
50+
runQuery(
51+
profiles.filter(_.id === id)
52+
.map(p => (p.minDepthHigh, p.minDepthMedium, p.minDepthLow, p.minCoveragePct, p.minMappingQuality, p.minCallablePct, p.notes))
53+
.update((profile.minDepthHigh, profile.minDepthMedium, profile.minDepthLow, profile.minCoveragePct, profile.minMappingQuality, profile.minCallablePct, profile.notes))
54+
).map(_ > 0)
55+
}
56+
57+
override def delete(id: Int): Future[Boolean] =
58+
runQuery(profiles.filter(_.id === id).delete).map(_ > 0)
59+
}

0 commit comments

Comments
 (0)