Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.1.4 の ExposureSummary.HighestRiskScore の部分の差分解説 #31

Open
moonmile opened this issue Sep 25, 2020 · 3 comments
Open

Comments

@moonmile
Copy link
Member

moonmile commented Sep 25, 2020

ちょっと謎な if (userData.ExposureSummary.HighestRiskScore >= config.MinimumRiskScore) なところを推測するための記録として。

image

@moonmile
Copy link
Member Author

現象としては、

  • DetectExposuressAsync で summary?.MatchKeyCount > 0 を返しているが
  • GetExposureInfoAsync で返すExposureInfo が zero

なので、「接触可能性アリ」の通知は出るが、アプリでは「陽性者との接触確認する」と 0 という現象と思われる。

ExposureSummary.HighestRiskScore >= config.MinimumRiskScore のチェックを入れると

  • DetectExposuressAsync で summary?.MatchKeyCount > 0 を返しているが
  • ExposureSummary.HighestRiskScore >= config.MinimumRiskScore のチェックで、GetExposureInfoAsync は呼び出さない
  • GetExposureInfoAsync を呼び出さないので通知はでない。

ってことになるけど、iOS 側で EN の履歴を見れば「1件あります」という現象は続くと思われる。

EN API

@moonmile
Copy link
Member Author

v1.1.4 の修正解析

以下は、Exposure Notification - Apple Developer の Source Code から、EN api 関係のコードをダウンロードしてチェック。

コードを引用して解説します。

ポイント

in ENExposureDetectionDaemonSession.m

これにより、TEKとの接触はあったが、すべてのリスク値が最小リスク値を下回る場合に、以下の現象となる。

  • _matchedKeyCount > 0 となる
  • detectExposures の返すサマリで、summary.matchedKeyCount > 0 となる
  • getExposureInfo を呼び出すと最小リスク値を上回るTEKがないので、exposureInfoArray は空になる。

これを v1.1.4 では以下のように修正する。

  • _matchedKeyCount > 0 となる
  • detectExposures の返すサマリで、summary.matchedKeyCount > 0 となる
  • ExposureSummary.HighestRiskScore > config.MinimumRiskScore として、最小リスク値が超えているものがあるかをチェックする。
  • getExposureInfo を呼び出すと最小リスク値を上回るTEKがあるので、exposureInfoArray は必ず1以上になる。

これにより、通知で接触可能性アリと出るが、アプリでは陽性者数がゼロ件である、という現象はなくなる。

原因

detectExposures が返す summary.matchedKeyCount がリスク計算を考慮したもの、と勘違いが原因である。
これは Xamarin.ExposureNotification 内部も同様。おそらく、ドイツとカナダの issue も同じ。

v1.1.3 以前の現象

  • 通知あり、アプリで陽性接触あり → リスク値を超えるので「濃厚接触アリ」と診断
  • 通知あり、アプリで陽性接触なし → リスク値を下回るので「問題なし」と診断

v1.1.4 以降の現象

  • TEKのマッチあり、最小リスク値を超える → 通知あり、アプリで陽性接触あり → リスク値を超えるので「濃厚接触アリ」と診断
  • TEKのマッチあり、最初リスク値以下 → 通知なし、アプリで陽性接触なし → リスク値を下回るので「問題なし」と診断

おそらく「接触チェックの記録」のカウントは、addFile の _matchedKeyCount のままのため、最小リスク値は考慮されずカウントは「1」になると考えられる。
これは、v1.1.4 以降で

  • 通知はでない、しかし「接触チェックの記録」を見ると、カウント「1」である。

という現象が発生すると思う。
この場合、リスク値を超えてはいないので「問題なし」の診断となる。

ENExposureDetectionDaemonSession.m より抜粋

/*
 *  Find matches for an the TEKs contained with the provided ENFile.
 *  Returnes NO if the matching process encounters an error, else returns YES.
 */
- (BOOL)addFile:(ENFile *)mainFile
{
    __block NSError *error = nil;

    uint64_t fileMatchCount = 0;
    for( ;; )
    {
        @autoreleasepool
        {
            NSMutableArray<ENTemporaryExposureKey *> *tekArray = [[NSMutableArray <ENTemporaryExposureKey *> alloc] init];
            check_compile_time_code( TEKBatchSize > 0 );
            uint32_t tekCount = 0;

            // Match the TEKs in batches to reduce the peak memory usage

            for( ; tekCount < TEKBatchSize; ++tekCount )
            {
                ENTemporaryExposureKey *key = [mainFile readTEKAndReturnError:&error];
                if( !key ) break;
                [tekArray addObject:key];
            }
            if( tekCount == 0 ) break;
            // マッチした数を返す。リスク値は考慮されない
            fileMatchCount += [_databaseQuerySession matchCountForKeys:tekArray attenuationThreshold:0xFF error:&error];
            if( error ) break;
        }
    }

    // 単純に、TEK がマッチした値を _matchedKeyCount に保存しておく
    _matchedKeyCount += fileMatchCount;

    if( error )
    {
        return NO;
    }
    return YES;
}
- (ENExposureDetectionSummary *)generateSummary
{
    // Process all the cached info to create the summary.

    __block uint32_t attenuationDurationSums[ 3 ] = { 0, 0, 0 };
    __block uint32_t *attenuationDurationSumsPtr = attenuationDurationSums;
    __block uint64_t minimumRiskScoreSkipped = 0;
    ENRiskScore minimumRiskScore = _configuration.minimumRiskScore;
    double minimumRiskScoreFullRange = _configuration.minimumRiskScoreFullRange;
    __block ENRiskScore maximumRiskScore = 0;
    __block double maximumRiskScoreFullRange = 0;
    __block CFAbsoluteTime mostRecentExposureTime = 0;
    __block double riskScoreSumFullRange = 0;

    CFAbsoluteTime nowTime = CFAbsoluteTimeGetCurrent();
    [_databaseQuerySession enumerateCachedExposureInfo:
    ^( NSArray <ENExposureInfo *> * _Nullable inExposureInfoBatch, NSError * _Nullable __unused inError )
    {
        for( ENExposureInfo *exposureInfo in inExposureInfoBatch )
        {

            // Determine if this exposure is the most recent exposure
            NSTimeInterval exposureTime = exposureInfo.date.timeIntervalSinceReferenceDate;
            if( exposureTime > mostRecentExposureTime ) mostRecentExposureTime = exposureTime;

            // Filter out any exposures with a risk score below the configured minimum
            double riskScoreFullRange = [self estimateRiskWithExposureInfo:exposureInfo referenceTime:nowTime
                transmissionRiskLevel:nil];
            ENRiskScore clampedRiskScore = (ENRiskScore) Clamp( riskScoreFullRange, ENRiskScoreMin, ENRiskScoreMax );
            // 最小リスク値以下のものは、戻り値に含めない
            if( ( clampedRiskScore < minimumRiskScore ) || ( riskScoreFullRange < minimumRiskScoreFullRange ) )
            {
                // 最小リスク値を下回る場合は、スキップする
                ++minimumRiskScoreSkipped;
                continue;
            }

            // Update the maximum seen risk score
            if( clampedRiskScore > maximumRiskScore ) maximumRiskScore = clampedRiskScore;
            if( riskScoreFullRange > maximumRiskScoreFullRange ) maximumRiskScoreFullRange = riskScoreFullRange;
            riskScoreSumFullRange += riskScoreFullRange;

            // Accumulate the calculated attenuation durations
            NSArray <NSNumber *> *attenuationDurations = exposureInfo.attenuationDurations;
            if( attenuationDurations.count >= countof( attenuationDurationSums ) )
            {
                for( size_t i = 0; i < countof( attenuationDurationSums ); ++i )
                {
                    uint32_t durationSum = attenuationDurationSumsPtr[ i ];
                    if( durationSum >= ENDurationMaxSeconds ) continue;
                    durationSum += attenuationDurations[ i ].unsignedIntValue;
                    if( durationSum > ENDurationMaxSeconds ) durationSum = ENDurationMaxSeconds;
                    attenuationDurationSumsPtr[ i ] = durationSum;
                }
            }
        }
    }];

    // Round the attenuation durations
    NSInteger daysSinceLastExposure = ( mostRecentExposureTime > 0 )
        ? ( (NSInteger)( ( nowTime - mostRecentExposureTime ) / kSecondsPerDay ) )
        : 0;
    attenuationDurationSums[ 0 ] = RoundUp( attenuationDurationSums[ 0 ], ENDurationIncrement );
    attenuationDurationSums[ 1 ] = RoundUp( attenuationDurationSums[ 1 ], ENDurationIncrement );
    attenuationDurationSums[ 2 ] = RoundUp( attenuationDurationSums[ 2 ], ENDurationIncrement );

    // Generate the summary
    ENExposureDetectionSummary *summary = [[ENExposureDetectionSummary alloc] init];
    summary.attenuationDurations =
    @[
        @(attenuationDurationSums[ 0 ]),
        @(attenuationDurationSums[ 1 ]),
        @(attenuationDurationSums[ 2 ])
    ];
    summary.daysSinceLastExposure = daysSinceLastExposure;
    // マッチカウントは、addFile で保持している _matchedKeyCount を返す
    // ゆえに全てのTEKで最小リスク値を
    summary.matchedKeyCount = _matchedKeyCount;
    summary.maximumRiskScore = maximumRiskScore;
    summary.maximumRiskScoreFullRange = maximumRiskScoreFullRange;
    summary.riskScoreSumFullRange = riskScoreSumFullRange;

    return summary;
}
- (NSArray<ENExposureInfo *> *)exposureInfo
{
    // Get the ENExposureInfo objects from the database.

    __block NSInteger databaseTotal = 0;
    __block ENRiskScore minimumRiskScore = _configuration.minimumRiskScore;
    double minimumRiskScoreFullRange = _configuration.minimumRiskScoreFullRange;
    __block uint64_t minimumRiskScoreSkipped = 0;

    CFAbsoluteTime nowTime = CFAbsoluteTimeGetCurrent();
    NSMutableArray <ENExposureInfo *> *exposureInfoArray = [[NSMutableArray <ENExposureInfo *> alloc] init];

    [_databaseQuerySession enumerateCachedExposureInfo:
    ^( NSArray <ENExposureInfo *> * _Nullable inExposureInfoBatch, NSError * _Nullable __unused inError )
    {
        databaseTotal += inExposureInfoBatch.count;
        for( ENExposureInfo *exposureInfo in inExposureInfoBatch )
        {
            // Filter out any exposures with a risk score below the configured minimum
            ENRiskLevel transmissionRiskLevel = 0;
            double riskScoreFullRange = [self estimateRiskWithExposureInfo:exposureInfo referenceTime:nowTime
                transmissionRiskLevel:&transmissionRiskLevel];
            ENRiskScore clampedRiskScore = (ENRiskScore) Clamp( riskScoreFullRange, ENRiskScoreMin, ENRiskScoreMax );
            // ここの条件式は、generateSummary と全く同じ
            if( ( clampedRiskScore < minimumRiskScore ) || ( riskScoreFullRange < minimumRiskScoreFullRange ) )
            {
                // 最小リスク値であればスキップする
                ++minimumRiskScoreSkipped;
                continue;
            }

            // Round the attenuation durations
            uint32_t duration = (uint32_t) exposureInfo.duration;
            duration = RoundUp( duration, ENDurationIncrement );
            exposureInfo.duration = Min( duration, ENDurationMaxSeconds );

            NSMutableArray <NSNumber *> *filteredAttenuationDurations = [[NSMutableArray <NSNumber *> alloc] init];
            for( NSNumber *attenuationDuration in exposureInfo.attenuationDurations )
            {
                duration = attenuationDuration.unsignedIntValue;
                duration = RoundUp( duration, ENDurationIncrement );
                duration = Min( duration, ENDurationMaxSeconds );
                [filteredAttenuationDurations addObject:@(duration)];
            }

            // Populate the final ENExposureInfo
            exposureInfo.attenuationDurations = filteredAttenuationDurations;
            exposureInfo.totalRiskScore = clampedRiskScore;
            exposureInfo.totalRiskScoreFullRange = riskScoreFullRange;
            exposureInfo.transmissionRiskLevel = transmissionRiskLevel;
            // exposureInfoArray に保存しておく
            [exposureInfoArray addObject:exposureInfo];
        }
    }];
    // exposureInfoArray を返す
    return exposureInfoArray;
}

BuildingAnAppToNotifyUsersOfCOVID19Exposure の readme より

Server.shared.getExposureConfiguration { result in
    switch result {
    case let .success(configuration):
        ExposureManager.shared.manager.detectExposures(configuration: configuration, diagnosisKeyURLs: localURLs) { summary, error in
            if let error = error {
                finish(.failure(error))
                return
            }
            // ここで Summary.HighestRiskScore をチェックせずに、
            // getExposureInfo を使うのが、ミスリード?
            let userExplanation = NSLocalizedString("USER_NOTIFICATION_EXPLANATION", comment: "User notification")
            ExposureManager.shared.manager.getExposureInfo(summary: summary!, userExplanation: userExplanation) { exposures, error in
                    if let error = error {
                        finish(.failure(error))
                        return
                    }
                    let newExposures = exposures!.map { exposure in
                        Exposure(date: exposure.date,
                                 duration: exposure.duration,
                                 totalRiskScore: exposure.totalRiskScore,
                                 transmissionRiskLevel: exposure.transmissionRiskLevel)
                    }
                    finish(.success((newExposures, nextDiagnosisKeyFileIndex + localURLs.count)))
            }
        }
        
    case let .failure(error):
        finish(.failure(error))
    }
}

@moonmile
Copy link
Member Author

これにより、通知で接触可能性アリと出るが、アプリでは陽性者数がゼロ件である、という現象はなくなる。

の予定であるが、v1.1.4 でも出るらしい。ぐぬぬ。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant