Skip to content
Snippets Groups Projects
Unverified Commit 37a4f694 authored by BMItter's avatar BMItter Committed by GitHub
Browse files

V2 Configuration Options for Risk Calculation (EXPOSUREAPP-5217) (#2512)


* Drop exposure window when no risklevel is associated - wip

* improved logging in DefaultRiskLevels

* Adapted new trasmission risk level calc

* Added placeholder for transmission risk value mapping

* Adjusted calculation test, updated test data

* Updated path for appconfig v2

* Replaced Transmission Risk Level Multiplier with Transmission Risk Value Mapping

* cleanUp

* Use protobufs

* better readable risk calculation parameters

* Updated default config

* adjusted tests

* Return null instead of throwing an exception

* Removed obsolete exceptions

Co-authored-by: default avatarharambasicluka <64483219+harambasicluka@users.noreply.github.com>
parent 0d8118f0
No related branches found
No related tags found
No related merge requests found
Showing
with 320 additions and 201 deletions
......@@ -9,7 +9,6 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.appconfig.ConfigData
import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysSettings
import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
......@@ -109,31 +108,9 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
val backendParameters = appConfigProvider
.currentConfig
.map { it.toReadableString() }
.map { it.rawConfig.riskCalculationParameters.toString() }
.asLiveData()
private fun ConfigData.toReadableString(): String = StringBuilder()
.appendLine("Transmission RiskLevel Multiplier: $transmissionRiskLevelMultiplier")
.appendLine()
.appendLine("Minutes At Attenuation Filters:")
.appendLine(minutesAtAttenuationFilters)
.appendLine()
.appendLine("Minutes At Attenuation Weights:")
.appendLine(minutesAtAttenuationWeights)
.appendLine()
.appendLine("Transmission RiskLevel Encoding:")
.appendLine(transmissionRiskLevelEncoding)
.appendLine()
.appendLine("Transmission RiskLevel Filters:")
.appendLine(transmissionRiskLevelFilters)
.appendLine()
.appendLine("Normalized Time Per Exposure Window To RiskLevel Mapping:")
.appendLine(normalizedTimePerExposureWindowToRiskLevelMapping)
.appendLine()
.appendLine("Normalized Time Per Day To RiskLevel Mapping List:")
.appendLine(normalizedTimePerDayToRiskLevelMappingList)
.toString()
val additionalRiskCalcInfo = combine(
riskLevelStorage.latestAndLastSuccessful,
exposureDetectionTracker.latestSubmission()
......
No preview for this file type
12d0b93c0c02c6870ef75c173a53a8ffb9cab6828fbf22e751053329c425eef2
\ No newline at end of file
3d108b3fee7d1b4c227087c82bb804048de8d0542c3f2b26cf507a918201124d
\ No newline at end of file
......@@ -9,11 +9,11 @@ interface ExposureWindowRiskCalculationConfig {
val minutesAtAttenuationWeights: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>
val transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding
val transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter>
val transmissionRiskLevelMultiplier: Double
val normalizedTimePerExposureWindowToRiskLevelMapping:
List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
val normalizedTimePerDayToRiskLevelMappingList:
List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
val transmissionRiskValueMapping: List<RiskCalculationParametersOuterClass.TransmissionRiskValueMapping>
val diagnosisKeysDataMapping: DiagnosisKeysDataMapping
interface Mapper {
......
......@@ -6,6 +6,6 @@ import retrofit2.http.GET
interface AppConfigApiV2 {
@GET("/version/v1/app_config_android")
@GET("/version/v2/app_config_android")
suspend fun getApplicationConfiguration(): Response<ResponseBody>
}
......@@ -28,20 +28,15 @@ class ExposureWindowRiskCalculationConfigMapper @Inject constructor() :
val riskCalculationParameters = rawConfig.riskCalculationParameters
return ExposureWindowRiskCalculationContainer(
minutesAtAttenuationFilters = riskCalculationParameters
.minutesAtAttenuationFiltersList,
minutesAtAttenuationWeights = riskCalculationParameters
.minutesAtAttenuationWeightsList,
transmissionRiskLevelEncoding = riskCalculationParameters
.trlEncoding,
transmissionRiskLevelFilters = riskCalculationParameters
.trlFiltersList,
transmissionRiskLevelMultiplier = riskCalculationParameters
.transmissionRiskLevelMultiplier,
normalizedTimePerExposureWindowToRiskLevelMapping = riskCalculationParameters
.normalizedTimePerEWToRiskLevelMappingList,
normalizedTimePerDayToRiskLevelMappingList = riskCalculationParameters
.normalizedTimePerDayToRiskLevelMappingList,
minutesAtAttenuationFilters = riskCalculationParameters.minutesAtAttenuationFiltersList,
minutesAtAttenuationWeights = riskCalculationParameters.minutesAtAttenuationWeightsList,
transmissionRiskLevelEncoding = riskCalculationParameters.trlEncoding,
transmissionRiskLevelFilters = riskCalculationParameters.trlFiltersList,
normalizedTimePerExposureWindowToRiskLevelMapping =
riskCalculationParameters.normalizedTimePerEWToRiskLevelMappingList,
normalizedTimePerDayToRiskLevelMappingList =
riskCalculationParameters.normalizedTimePerDayToRiskLevelMappingList,
transmissionRiskValueMapping = riskCalculationParameters.transmissionRiskValueMappingList,
diagnosisKeysDataMapping = rawConfig.diagnosisKeysDataMapping()
)
}
......@@ -63,12 +58,14 @@ class ExposureWindowRiskCalculationConfigMapper @Inject constructor() :
override val minutesAtAttenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>,
override val minutesAtAttenuationWeights: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>,
override val transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding,
override val transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter>,
override val transmissionRiskLevelMultiplier: Double,
override val transmissionRiskLevelFilters:
List<RiskCalculationParametersOuterClass.TrlFilter>,
override val normalizedTimePerExposureWindowToRiskLevelMapping:
List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>,
override val normalizedTimePerDayToRiskLevelMappingList:
List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>,
override val transmissionRiskValueMapping:
List<RiskCalculationParametersOuterClass.TransmissionRiskValueMapping>,
override val diagnosisKeysDataMapping: DiagnosisKeysDataMapping
) : ExposureWindowRiskCalculationConfig
}
package de.rki.coronawarnapp.risk
import android.text.TextUtils
import com.google.android.gms.nearby.exposurenotification.ExposureWindow
import com.google.android.gms.nearby.exposurenotification.Infectiousness
import com.google.android.gms.nearby.exposurenotification.ReportType
......@@ -114,8 +113,9 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
return null
}
val transmissionRiskValue: Double =
transmissionRiskLevel * appConfig.transmissionRiskLevelMultiplier
val transmissionRiskValue: Double = appConfig.transmissionRiskValueMapping
.find { it.transmissionRiskLevel == transmissionRiskLevel }
?.transmissionRiskValue ?: 0.0
Timber.d("%s's transmissionRiskValue is: %s", exposureWindow, transmissionRiskValue)
......@@ -135,8 +135,12 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
)
if (riskLevel == null) {
Timber.e("Exposure Window: $exposureWindow could not be mapped to a risk level")
throw NormalizedTimePerExposureWindowToRiskLevelMappingMissingException()
Timber.d(
"%s dropped due to risk level filter is %s",
exposureWindow,
riskLevel
)
return null
}
Timber.d("%s's riskLevel is: %s", exposureWindow, riskLevel)
......@@ -158,13 +162,13 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
Timber.d(
"uniqueDates: %s",
{ TextUtils.join(System.lineSeparator(), uniqueDatesMillisSinceEpoch) }
uniqueDatesMillisSinceEpoch
)
val exposureHistory = uniqueDatesMillisSinceEpoch.map {
val exposureHistory = uniqueDatesMillisSinceEpoch.mapNotNull {
aggregateRiskPerDate(appConfig, it, exposureWindowResultMap)
}
Timber.d("exposureHistory size: ${exposureHistory.size}")
Timber.d("exposureHistory size: %d", exposureHistory.size)
// 6. Determine `Total Risk`
val totalRiskLevel =
......@@ -180,43 +184,43 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW
}
Timber.d("totalRiskLevel: ${totalRiskLevel.name} (${totalRiskLevel.ordinal})")
Timber.d("totalRiskLevel: %s (%d)", totalRiskLevel.name, totalRiskLevel.ordinal)
// 7. Determine `Date of Most Recent Date with Low Risk`
val mostRecentDateWithLowRisk =
exposureHistory.mostRecentDateForRisk(ProtoRiskLevel.LOW)
Timber.d("mostRecentDateWithLowRisk: $mostRecentDateWithLowRisk")
Timber.d("mostRecentDateWithLowRisk: %s", mostRecentDateWithLowRisk)
// 8. Determine `Date of Most Recent Date with High Risk`
val mostRecentDateWithHighRisk =
exposureHistory.mostRecentDateForRisk(ProtoRiskLevel.HIGH)
Timber.d("mostRecentDateWithHighRisk: $mostRecentDateWithHighRisk")
Timber.d("mostRecentDateWithHighRisk: %s", mostRecentDateWithHighRisk)
// 9. Determine `Total Minimum Distinct Encounters With Low Risk`
val totalMinimumDistinctEncountersWithLowRisk = exposureHistory
.sumBy { it.minimumDistinctEncountersWithLowRisk }
Timber.d("totalMinimumDistinctEncountersWithLowRisk: $totalMinimumDistinctEncountersWithLowRisk")
Timber.d("totalMinimumDistinctEncountersWithLowRisk: %d", totalMinimumDistinctEncountersWithLowRisk)
// 10. Determine `Total Minimum Distinct Encounters With High Risk`
val totalMinimumDistinctEncountersWithHighRisk = exposureHistory
.sumBy { it.minimumDistinctEncountersWithHighRisk }
Timber.d("totalMinimumDistinctEncountersWithHighRisk: $totalMinimumDistinctEncountersWithHighRisk")
Timber.d("totalMinimumDistinctEncountersWithHighRisk: %d", totalMinimumDistinctEncountersWithHighRisk)
// 11. Determine `Number of Days With Low Risk`
val numberOfDaysWithLowRisk =
exposureHistory.numberOfDaysForRisk(ProtoRiskLevel.LOW)
Timber.d("numberOfDaysWithLowRisk: $numberOfDaysWithLowRisk")
Timber.d("numberOfDaysWithLowRisk: %d", numberOfDaysWithLowRisk)
// 12. Determine `Number of Days With High Risk`
val numberOfDaysWithHighRisk =
exposureHistory.numberOfDaysForRisk(ProtoRiskLevel.HIGH)
Timber.d("numberOfDaysWithHighRisk: $numberOfDaysWithHighRisk")
Timber.d("numberOfDaysWithHighRisk: %d", numberOfDaysWithHighRisk)
return AggregatedRiskResult(
totalRiskLevel = totalRiskLevel,
......@@ -243,7 +247,7 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
appConfig: ExposureWindowRiskCalculationConfig,
dateMillisSinceEpoch: Long,
exposureWindowsAndResult: Map<ExposureWindow, RiskResult>
): AggregatedRiskPerDateResult {
): AggregatedRiskPerDateResult? {
// 1. Group `Exposure Windows by Date`
val exposureWindowsAndResultForDate = exposureWindowsAndResult
.filter { it.key.dateMillisSinceEpoch == dateMillisSinceEpoch }
......@@ -252,31 +256,40 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
val normalizedTime = exposureWindowsAndResultForDate.values
.sumOf { it.normalizedTime }
Timber.d("Aggregating result for date $dateMillisSinceEpoch - ${Instant.ofEpochMilli(dateMillisSinceEpoch)}")
Timber.d(
"Aggregating result for date %d - %s",
dateMillisSinceEpoch,
Instant.ofEpochMilli(dateMillisSinceEpoch)
)
// 3. Determine `Risk Level per Date`
val riskLevel = try {
appConfig.normalizedTimePerDayToRiskLevelMappingList
.filter { it.normalizedTimeRange.inRange(normalizedTime) }
.map { it.riskLevel }
.first()
} catch (e: Exception) {
throw NormalizedTimePerDayToRiskLevelMappingMissingException()
val riskLevel = appConfig.normalizedTimePerDayToRiskLevelMappingList
.filter { it.normalizedTimeRange.inRange(normalizedTime) }
.map { it.riskLevel }
.firstOrNull()
if (riskLevel == null) {
Timber.d(
"No Risk Level is associated with date %d - %s",
dateMillisSinceEpoch,
Instant.ofEpochMilli(dateMillisSinceEpoch)
)
return null
}
Timber.d("riskLevel: ${riskLevel.name} (${riskLevel.ordinal})")
Timber.d("riskLevel: %s (%d)", riskLevel.name, riskLevel.ordinal)
// 4. Determine `Minimum Distinct Encounters With Low Risk per Date`
val minimumDistinctEncountersWithLowRisk =
exposureWindowsAndResultForDate.minimumDistinctEncountersForRisk(ProtoRiskLevel.LOW)
Timber.d("minimumDistinctEncountersWithLowRisk: $minimumDistinctEncountersWithLowRisk")
Timber.d("minimumDistinctEncountersWithLowRisk: %d", minimumDistinctEncountersWithLowRisk)
// 5. Determine `Minimum Distinct Encounters With High Risk per Date`
val minimumDistinctEncountersWithHighRisk =
exposureWindowsAndResultForDate.minimumDistinctEncountersForRisk(ProtoRiskLevel.HIGH)
Timber.d("minimumDistinctEncountersWithHighRisk: $minimumDistinctEncountersWithHighRisk")
Timber.d("minimumDistinctEncountersWithHighRisk: %d", minimumDistinctEncountersWithHighRisk)
return AggregatedRiskPerDateResult(
dateMillisSinceEpoch = dateMillisSinceEpoch,
......@@ -293,17 +306,6 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
.size
companion object {
open class RiskLevelMappingMissingException(msg: String) : Exception(msg)
class NormalizedTimePerExposureWindowToRiskLevelMappingMissingException : RiskLevelMappingMissingException(
"Failed to map the normalized Time per Exposure Window to a Risk Level"
)
class NormalizedTimePerDayToRiskLevelMappingMissingException : RiskLevelMappingMissingException(
"Failed to map the normalized Time per Day to a Risk Level"
)
class UnknownReportTypeException : Exception(
"The Report Type returned by the ENF is not known"
)
......
......@@ -37,7 +37,7 @@ class DefaultAppConfigSanityCheck : BaseTest() {
fun `current default matches checksum`() {
val config = context.assets.open(configName).readBytes()
val sha256 = context.assets.open(checkSumName).readBytes().toString(Charsets.UTF_8)
sha256 shouldBe "12d0b93c0c02c6870ef75c173a53a8ffb9cab6828fbf22e751053329c425eef2"
sha256 shouldBe "3d108b3fee7d1b4c227087c82bb804048de8d0542c3f2b26cf507a918201124d"
config.toSHA256() shouldBe sha256
}
......
......@@ -20,6 +20,7 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import testhelpers.BaseIOTest
import java.io.File
import java.io.FileNotFoundException
class DefaultAppConfigSourceTest : BaseIOTest() {
@MockK private lateinit var context: Context
......@@ -81,6 +82,7 @@ class DefaultAppConfigSourceTest : BaseIOTest() {
@Test
fun `exceptions when getting the default config are rethrown`() = runBlockingTest {
every { assetManager.open("default_app_config_android.bin") } throws FileNotFoundException("default_app_config_android.bin does not exist")
val instance = createInstance()
shouldThrowAny {
......
......@@ -75,6 +75,6 @@ class AppConfigApiTest : BaseIOTest() {
val request = webServer.takeRequest(5, TimeUnit.SECONDS)!!
request.method shouldBe "GET"
request.path shouldBe "/version/v1/app_config_android"
request.path shouldBe "/version/v2/app_config_android"
}
}
......@@ -14,6 +14,7 @@ import de.rki.coronawarnapp.nearby.windows.entities.configuration.DefaultRiskCal
import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonMinutesAtAttenuationFilter
import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonMinutesAtAttenuationWeight
import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonNormalizedTimeToRiskLevelMapping
import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonTransmissionRiskValueMapping
import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonTrlFilter
import de.rki.coronawarnapp.risk.DefaultRiskLevels
import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
......@@ -261,8 +262,14 @@ class ExposureWindowsCalculationTest : BaseTest() {
result.append(logRange(filter.dropIfTrlInRange, "Drop If Trl In Range"))
}
result.append("\n").append("◦ Transmission Risk Level Multiplier: ${config.transmissionRiskLevelMultiplier}")
result.append("\n").append("-------------------------------------------- ⚙ -").append("\n")
result.append("\n").appendLine("◦ Transmission Risk Value Mapping (${config.transmissionRiskValueMapping.size})")
for (mapping in config.transmissionRiskValueMapping) {
result.append("\t").appendLine("⇥ Mapping")
result.append("\t\t").append("↳ transmissionRiskLevel: ").appendLine(mapping.transmissionRiskLevel)
result.append("\t\t").append("↳ transmissionRiskValue: ").appendLine(mapping.transmissionRiskValue)
}
result.appendLine("-------------------------------------------- ⚙ -")
debugLog(result.toString(), LogLevel.NONE)
}
......@@ -364,8 +371,6 @@ class ExposureWindowsCalculationTest : BaseTest() {
}
every { testConfig.normalizedTimePerExposureWindowToRiskLevelMapping } returns normalizedTimePerExposureWindowToRiskLevelMapping
every { testConfig.transmissionRiskLevelMultiplier } returns json.transmissionRiskLevelMultiplier
val trlEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding = mockk()
every { trlEncoding.infectiousnessOffsetHigh } returns json.trlEncoding.infectiousnessOffsetHigh
every { trlEncoding.infectiousnessOffsetStandard } returns json.trlEncoding.infectiousnessOffsetStandard
......@@ -385,6 +390,18 @@ class ExposureWindowsCalculationTest : BaseTest() {
trlFilters.add(filter)
}
every { testConfig.transmissionRiskLevelFilters } returns trlFilters
val transmissionRiskValueMapping =
mutableListOf<RiskCalculationParametersOuterClass.TransmissionRiskValueMapping>()
for (jsonMapping: JsonTransmissionRiskValueMapping in json.transmissionRiskValueMapping) {
val mapping: RiskCalculationParametersOuterClass.TransmissionRiskValueMapping = mockk()
mapping.run {
every { transmissionRiskLevel } returns jsonMapping.transmissionRiskLevel
every { transmissionRiskValue } returns jsonMapping.transmissionRiskValue
}
transmissionRiskValueMapping += mapping
}
every { testConfig.transmissionRiskValueMapping } returns transmissionRiskValueMapping
}
private fun jsonToExposureWindow(json: JsonWindow): ExposureWindow {
......
......@@ -11,10 +11,10 @@ data class DefaultRiskCalculationConfiguration(
val normalizedTimePerDayToRiskLevelMapping: List<JsonNormalizedTimeToRiskLevelMapping>,
@SerializedName("normalizedTimePerEWToRiskLevelMapping")
val normalizedTimePerEWToRiskLevelMapping: List<JsonNormalizedTimeToRiskLevelMapping>,
@SerializedName("transmissionRiskLevelMultiplier")
val transmissionRiskLevelMultiplier: Double,
@SerializedName("trlEncoding")
val trlEncoding: JsonTrlEncoding,
@SerializedName("trlFilters")
val trlFilters: List<JsonTrlFilter>
val trlFilters: List<JsonTrlFilter>,
@SerializedName("transmissionRiskValueMapping")
val transmissionRiskValueMapping: List<JsonTransmissionRiskValueMapping>
)
package de.rki.coronawarnapp.nearby.windows.entities.configuration
import com.google.gson.annotations.SerializedName
data class JsonTransmissionRiskValueMapping(
@SerializedName("transmissionRiskLevel")
val transmissionRiskLevel: Int,
@SerializedName("transmissionRiskValue")
val transmissionRiskValue: Double
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment