1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.server.uwb.correction.primers;
17 
18 import static com.android.server.uwb.correction.math.MathHelper.F_HALF_PI;
19 import static com.android.server.uwb.correction.math.MathHelper.F_PI;
20 import static com.android.server.uwb.correction.math.MathHelper.MS_PER_SEC;
21 import static com.android.server.uwb.correction.math.MathHelper.normalizeRadians;
22 
23 import static java.lang.Math.abs;
24 import static java.lang.Math.exp;
25 import static java.lang.Math.max;
26 import static java.lang.Math.min;
27 import static java.lang.Math.signum;
28 import static java.lang.Math.toDegrees;
29 
30 import android.os.Build;
31 import android.util.Log;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 
36 import com.android.server.uwb.correction.math.MathHelper;
37 import com.android.server.uwb.correction.math.Pose;
38 import com.android.server.uwb.correction.math.SphericalVector;
39 import com.android.server.uwb.correction.math.SphericalVector.Annotated;
40 import com.android.server.uwb.correction.pose.IPoseSource;
41 
42 import java.util.ArrayDeque;
43 import java.util.Queue;
44 
45 /**
46  * Tracks the correlation between azimuth and pose yaw to determine if the controlee has gone
47  * to the other side of the device. This mirrors the azimuth value if:
48  * - The prediction crosses the ±90° threshold.
49  * - The covariance between pose yaw and azimuth exceeds a certain threshold.
50  */
51 public class BackAzimuthPrimer implements IPrimer {
52     static final String TAG = "BackAzimuthPrimer";
53     /** The decay rate of the low pass filter. (0-1, decay per sample) */
54     private static final double FOM_DECAY = 0.1;
55     /** How quickly the FOM falls when using mirrored predictions. */
56     private static final int MINIMUM_DETERMINATIONS = 4;
57 
58     private static final boolean sDebug;
59     // Keep sample time measurements reasonable to prevent div/0 or overflows.
60     private static final long MIN_TIME_MS = 5; // Shortest allowed time between ranging samples
61     private static final long MAX_TIME_MS = 5000; // Longest allowed time between ranging samples
62 
63     static {
64         sDebug = (Build.TYPE != null && Build.TYPE.equals("userdebug"))
65                 || System.getProperty("DEBUG") != null;
66     }
67 
68     private final boolean mMaskRawAzimuthWhenBackfacing;
69     private final float mDiscrepancyCoefficient;
70     private final int mWindowSize;
71     private final float mStdDev;
72     private final float mNormalThresholdRadPerSec;
73     private final float mMirrorThresholdRadPerSec;
74     private final Queue<Float> mScoreHistory;
75     private final Queue<Float> mDiscrepancyHistory = new ArrayDeque<>();
76     private boolean mMirrored = false;
77     private float mLastAzimuthPrediction;
78     // This initial value causes the first sample to have the least effect.
79     private long mLastSampleTimeMs = Long.MIN_VALUE;
80     private Pose mLastPose;
81     private SphericalVector mLastInput;
82     private int mDeterminationCount = 0;
83     private double mFomFilterValue = MINIMUM_FOM;
84     private double mLastGoodReferenceTimeMs;
85 
86     /**
87      * Creates a new instance of the BackAzimuthPrimer class.
88      *
89      * @param normalThresholdRadPerSec     How many radians per second of correlated rotation are
90      *                                     necessary to force a non-mirrored azimuth.
91      * @param mirrorThresholdRadPerSec     How many radians per second of correlated rotation are
92      *                                     necessary to force a mirrored azimuth.
93      * @param windowSize                   The size of the moving window filter for determining
94      *                                     correlation.
95      * @param maskRawAzimuthWhenBackfacing If true, readings from the back will be replaced with
96      *                                     predictions. If false, azimuth readings will be mirrored
97      *                                     front-to-back.
98      * @param stdDev                       Controls the width of the curve used to judge if the
99      *                                     readings are acting like
100      *                                     unmirrored or mirrored readings.
101      * @param discrepancyCoefficient       The coefficient of how much the typical forward
102      *                                     prediction
103      *                                     error (rads per sample) should count against the forward
104      *                                     score (rads per second). For
105      *                                     example, a value of 0.5 will cause the score to be 5
106      *                                     degrees lower if the typical front
107      *                                     prediction error is 10.
108      */
BackAzimuthPrimer(float normalThresholdRadPerSec, float mirrorThresholdRadPerSec, int windowSize, boolean maskRawAzimuthWhenBackfacing, float stdDev, float discrepancyCoefficient )109     public BackAzimuthPrimer(float normalThresholdRadPerSec,
110             float mirrorThresholdRadPerSec,
111             int windowSize,
112             boolean maskRawAzimuthWhenBackfacing,
113             float stdDev,
114             float discrepancyCoefficient
115     ) {
116         mNormalThresholdRadPerSec = normalThresholdRadPerSec;
117         mMirrorThresholdRadPerSec = mirrorThresholdRadPerSec;
118         mMaskRawAzimuthWhenBackfacing = maskRawAzimuthWhenBackfacing;
119         mDiscrepancyCoefficient = discrepancyCoefficient;
120         mScoreHistory = new ArrayDeque<>();
121         mWindowSize = windowSize;
122         mStdDev = stdDev;
123     }
124 
125     /**
126      * Uses pose information to disambiguate the input azimuth.
127      *
128      * @param input      The original UWB reading.
129      * @param prediction The previous filtered UWB result adjusted by the pose change since then.
130      * @param poseSource A pose source that may indicate phone orientation.
131      * @param timeMs     When the input occurred, in ms since boot.
132      * @return A replacement value for the UWB vector that has been corrected for the situation.
133      */
134     @SuppressWarnings("ConstantConditions") /* Unboxing longs in mCaptureTimes */
135     @Override
prime( @onNull SphericalVector.Annotated input, @Nullable SphericalVector prediction, @Nullable IPoseSource poseSource, long timeMs)136     public SphericalVector.Annotated prime(
137             @NonNull SphericalVector.Annotated input,
138             @Nullable SphericalVector prediction,
139             @Nullable IPoseSource poseSource,
140             long timeMs) {
141         if (!input.hasAzimuth || poseSource == null || prediction == null) {
142             // Can't perform correction if there is no azimuth data, no pose information,
143             // or no prediction.
144             return input;
145         }
146 
147         long timeDeltaMs = min(MAX_TIME_MS, max(MIN_TIME_MS, timeMs - mLastSampleTimeMs));
148         float timeScale = (float) MathHelper.MS_PER_SEC / timeDeltaMs;
149         mLastSampleTimeMs = timeMs;
150 
151         // Mirror if the pose simply rotated the azimuth past the 90-degree mark.
152         if (abs(prediction.azimuth) > F_HALF_PI
153                 && abs(mLastAzimuthPrediction) <= F_HALF_PI) {
154             // Prediction went from forward to backward-facing.
155             mMirrored = true;
156             flipScoreHistory();
157         } else if (abs(prediction.azimuth) <= F_HALF_PI
158                 && abs(mLastAzimuthPrediction) > F_HALF_PI) {
159             // Prediction went from backward to forward-facing.
160             mMirrored = false;
161             flipScoreHistory();
162         }
163         mLastAzimuthPrediction = prediction.azimuth;
164         // Because the prediction is influenced by mirroring itself and can have a significant
165         //  delay due to the filter, we will not be using the prediction to guess front/back.
166 
167         // Get coordinates for normal and mirrored azimuth versions of the input.
168         // input.vector may be >90deg due to previous primers, so front/back is forced here.
169         SphericalVector normalInput = forceAzimuth(input, false);
170         SphericalVector mirrorInput = forceAzimuth(input, true);
171 
172         Pose newPose = poseSource.getPose();
173         if (mLastPose == null || newPose == null || mLastPose == newPose || mLastInput == null) {
174             // Can't do anything without pose deltas and input history.
175             mLastPose = newPose;
176             mLastInput = normalInput;
177             return input;
178         }
179 
180         // Pose transform for theorizing how the previous reading might have changed.
181         // Note that we're using a full pose transform instead of just azimuth changes, as
182         // the phone may have rolled or performed other movements that aren't just azimuth.
183         Pose deltaPose = Pose.compose(newPose.inverted(), mLastPose);
184 
185         // Theorize, based on the old location, what the new location should be for mirrored and
186         // unmirrored inputs.
187         SphericalVector normalTheory = transformSpherical(mLastInput, deltaPose);
188         SphericalVector mirrorTheory = transformSpherical(mirrorAzimuth(mLastInput), deltaPose);
189 
190         // Compute how many radians of pose change have affected the azimuth. More movement means
191         // more certainty can be applied to the score.
192         float azimuthDeltaFromPoseRad =
193                 normalizeRadians(abs(normalTheory.azimuth - mLastInput.azimuth));
194 
195         // Judge how well the front and back predictions did.
196         float normalDifference = abs(normalizeRadians(normalTheory.azimuth - normalInput.azimuth));
197         float mirrorDifference = abs(normalizeRadians(mirrorTheory.azimuth - mirrorInput.azimuth));
198         // Note that one of these predictions will be perfect if the input itself is a prediction,
199         // which FovPrimer might do. Carrying this detail in SphericalVector.Sparse may provide an
200         // opportunity to ignore scoring when the input is predicted.
201         float normalAccuracy = bell(normalDifference, mStdDev);
202         float mirrorAccuracy = bell(mirrorDifference, mStdDev);
203 
204         // Score by the user's pose-induced azimuth change.
205         float scoreRadPerSec = (normalAccuracy - mirrorAccuracy) // score per sample
206                 * azimuthDeltaFromPoseRad // convert to score radians per sample
207                 * timeScale; // convert to score radians per second
208 
209         // Bias the score toward the back based on UWB noise.
210         float scoreRadPerSecBiased = biasScore(scoreRadPerSec, normalDifference, mirrorDifference);
211 
212         mLastInput = normalInput;
213         mLastPose = newPose;
214 
215         mScoreHistory.offer(scoreRadPerSecBiased);
216 
217         double typScore = 0;
218         if (mScoreHistory.size() > mWindowSize) {
219             mScoreHistory.poll();
220             // Get the median score.
221             typScore = mScoreHistory.stream().mapToDouble(Float::doubleValue).sorted()
222                     .skip(mWindowSize / 2).limit(1 + (mWindowSize % 2)).average().getAsDouble();
223 
224             // Finally, the mirroring decision.
225             if (typScore > mNormalThresholdRadPerSec) {
226                 mMirrored = false;
227                 if (mDeterminationCount < MINIMUM_DETERMINATIONS) {
228                     mDeterminationCount++;
229                 }
230             } else if (typScore < -mMirrorThresholdRadPerSec) {
231                 mMirrored = true;
232                 if (mDeterminationCount < MINIMUM_DETERMINATIONS) {
233                     mDeterminationCount++;
234                 }
235             }
236         }
237 
238         if (sDebug) {
239             Log.d(TAG,
240                     String.format(
241                             "time %4d, pose % 6.1f, nd % 6.1f (%3d%%), md % 6.1f (%3d%%), "
242                                     + "rawSco % 5.1f, sco % 5.1f, aggSco % 5.1f, %s",
243                             timeDeltaMs,
244                             toDegrees(azimuthDeltaFromPoseRad),
245                             toDegrees(normalDifference), (int) (normalAccuracy * 100),
246                             toDegrees(mirrorDifference), (int) (mirrorAccuracy * 100),
247                             toDegrees(scoreRadPerSec),
248                             toDegrees(scoreRadPerSecBiased),
249                             toDegrees(typScore),
250                             mMirrored ? "mirr" : "norm"
251                     ));
252         }
253 
254         SphericalVector result = input;
255 
256         if (mMirrored && mMaskRawAzimuthWhenBackfacing) {
257             // Replace angles with prediction. The mMaskRawAzimuthWhenBackfacing setting will be set
258             // when through-device readings are poor or not predictably mirrored.
259             result = SphericalVector.fromRadians(
260                     prediction.azimuth,
261                     prediction.elevation,
262                     input.distance);
263         }
264 
265         result = forceAzimuth(result, mMirrored);
266 
267         Annotated annotatedResult = new Annotated(
268                 result,
269                 true,
270                 input.hasElevation,
271                 input.hasDistance).copyFomFrom(input);
272 
273         updateFom(annotatedResult, normalAccuracy, mirrorAccuracy);
274 
275         return annotatedResult;
276     }
277 
278     /**
279      * Changes the azimuthFom of the annotated result to reflect 3 conditions:
280      * 1. How long the filter has been using predicted values
281      * 2. How well-correlated yaw and azimuth are (normal or mirror Accuracy)
282      * 3. Whether or not a number of initial good determinations have been made.
283      * @param annotatedResult The reading whose azimuthFom will be updated.
284      * @param normalAccuracy The computed accuracy of correlation for the normal case.
285      * @param mirrorAccuracy The computed accuracy of correlation for the mirrored case.
286      */
updateFom(Annotated annotatedResult, float normalAccuracy, float mirrorAccuracy)287     private void updateFom(Annotated annotatedResult, float normalAccuracy, float mirrorAccuracy) {
288         double newFom;
289         if (mMirrored) {
290             newFom = mirrorAccuracy;
291             if (mMaskRawAzimuthWhenBackfacing) {
292                 // We've gone totally to predictions. Tweak the FOM based on how fresh our data is.
293                 double elapsedMs = mLastSampleTimeMs - mLastGoodReferenceTimeMs;
294                 double fom = max(1 - elapsedMs / MS_PER_SEC * FALLOFF_FOM_PER_SEC, MINIMUM_FOM);
295                 annotatedResult.azimuthFom *= fom;
296             }
297         } else {
298             newFom = normalAccuracy;
299 
300             // This brings the FOM back up for subsequent estimations
301             mLastGoodReferenceTimeMs = mLastSampleTimeMs;
302         }
303         mFomFilterValue = mFomFilterValue * (1 - FOM_DECAY) + (newFom * FOM_DECAY);
304         annotatedResult.azimuthFom *= mFomFilterValue;
305 
306         if (mDeterminationCount < MINIMUM_DETERMINATIONS) {
307             // If we haven't actually seen good evidence of front or back, our certainty is up to
308             // 50% less, depending on how many determinations have been made.
309             annotatedResult.azimuthFom *=
310                     0.5 + ((double) mDeterminationCount) / MINIMUM_DETERMINATIONS / 2;
311         }
312     }
313 
314     /** Flips the score history, for when azimuth goes from front to behind or vice-versa. */
flipScoreHistory()315     private void flipScoreHistory() {
316         for (int x = 0; x < mScoreHistory.size(); x++) {
317             Float sh = mScoreHistory.poll();
318             mScoreHistory.offer(sh == null ? null : -sh);
319         }
320     }
321 
322     /**
323      * Biases a score toward the back based on the accuracy of front-facing predictions.
324      *
325      * @param scoreRadPerSec   The score to bias.
326      * @param normalDifference The difference between the front-based prediction and front-based
327      *                         reading.
328      * @param mirrorDifference The difference between the back-based prediction and back-based
329      *                         reading.
330      * @return A new, adjusted version of the input score.
331      */
biasScore(float scoreRadPerSec, float normalDifference, float mirrorDifference)332     private float biasScore(float scoreRadPerSec, float normalDifference, float mirrorDifference) {
333         // Discrepancy measures how much the forward-facing values are off. They will be WAY off
334         // if the UWB signal is noisy, and slightly off if the signal is in the back. Rear azimuths
335         // are usually both.
336         mDiscrepancyHistory.offer(normalDifference);
337 
338         if (mDiscrepancyHistory.size() > mWindowSize) {
339             mDiscrepancyHistory.poll();
340             float avgDiscrepancyRad =
341                     (float) mDiscrepancyHistory.stream().mapToDouble(Float::doubleValue).average()
342                             .getAsDouble();
343 
344             // Average discrepancy is multiplied by the configurable coefficient to bias the
345             //  score toward the back.
346             return scoreRadPerSec - avgDiscrepancyRad * mDiscrepancyCoefficient;
347         }
348         return scoreRadPerSec;
349     }
350 
351     /**
352      * Applies a pose delta (a transform) to a spherical coordinate.
353      *
354      * @param input     The spherical vector to transform.
355      * @param deltaPose The pose object representing how to transform the input.
356      * @return A new SphericalVector representing the input transformed by the delta pose.
357      */
transformSpherical(SphericalVector input, Pose deltaPose)358     private SphericalVector transformSpherical(SphericalVector input, Pose deltaPose) {
359         return SphericalVector.fromCartesian(deltaPose.transformPoint(input.toCartesian()));
360     }
361 
362     /**
363      * Mirrors the azimuth front-to-back or back-to-front.
364      *
365      * @param vector The SphericalVector to mirror.
366      * @return A mirrored version of the SphericalVector.
367      */
368     @NonNull
mirrorAzimuth(SphericalVector vector)369     private SphericalVector mirrorAzimuth(SphericalVector vector) {
370         return SphericalVector.fromRadians(
371                 signum(vector.azimuth) * (F_PI - abs(vector.azimuth)),
372                 vector.elevation,
373                 vector.distance);
374     }
375 
376     /**
377      * Forces the azimuth to be front or back, mirroring it as necessary.
378      *
379      * @param vector The SphericalVector to force to a direction.
380      * @param back   If true, forces the SphericalVector's azimuth to be back, otherwise forward.
381      * @return A version of the SphericalVector that is facing the specified direction.
382      */
383     @NonNull
forceAzimuth(SphericalVector vector, boolean back)384     private SphericalVector forceAzimuth(SphericalVector vector, boolean back) {
385         if (back == abs(vector.azimuth) < F_HALF_PI) {
386             return mirrorAzimuth(vector);
387         }
388         return vector;
389     }
390 
391     /**
392      * Plots x on a bell curve with a magnitude of 1. This is a gaussian curve(φ) multiplied by
393      * 1/φ(0) so that bell(0, n) = 1.
394      *
395      * @param x      The x value of the normal curve.
396      * @param stdDev The standard deviation to use for the initial normal curve.
397      * @return A value along a gaussian curve scaled by 1/φ(0).
398      */
bell(float x, float stdDev)399     private float bell(float x, float stdDev) {
400         float variance = stdDev * stdDev;
401         return (float) exp(-(x * x / (2 * variance)));
402     }
403 }
404