View Javadoc
1   /*
2    * Copyright (C) 2019 Alberto Irurueta Carro (alberto@irurueta.com)
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.irurueta.navigation.gnss;
17  
18  import com.irurueta.algebra.AlgebraException;
19  import com.irurueta.navigation.LockedException;
20  import com.irurueta.navigation.NotReadyException;
21  import com.irurueta.units.Time;
22  import com.irurueta.units.TimeConverter;
23  import com.irurueta.units.TimeUnit;
24  
25  import java.util.ArrayList;
26  import java.util.Collection;
27  
28  /**
29   * Calculates position, velocity, clock offset and clock drift using an
30   * unweighted iterated least squares estimator along with a Kalman filter
31   * to smooth results.
32   * This implementation is based on the equations defined in "Principles of GNSS, Inertial, and Multi-sensor
33   * Integrated Navigation Systems, Second Edition" and on the companion software available at:
34   * <a href="https://github.com/ymjdz/MATLAB-Codes/blob/master/GNSS_Kalman_Filter.m">
35   *     https://github.com/ymjdz/MATLAB-Codes/blob/master/GNSS_Kalman_Filter.m
36   * </a>
37   */
38  public class GNSSKalmanFilteredEstimator {
39  
40      /**
41       * Internal estimator to compute least squares solution for GNSS measurements.
42       */
43      private final GNSSLeastSquaresPositionAndVelocityEstimator lsEstimator =
44              new GNSSLeastSquaresPositionAndVelocityEstimator();
45  
46      /**
47       * Listener to notify events raised by this instance.
48       */
49      private GNSSKalmanFilteredEstimatorListener listener;
50  
51      /**
52       * Minimum epoch interval expressed in seconds (s) between consecutive
53       * propagations or measurements.
54       * Attempting to propagate results using Kalman filter or updating measurements when
55       * intervals are lass than this value, will be ignored.
56       */
57      private double epochInterval;
58  
59      /**
60       * GNSS Kalman filter configuration parameters (usually obtained through calibration).
61       */
62      private GNSSKalmanConfig config;
63  
64      /**
65       * GNSS measurements of a collection of satellites.
66       */
67      private Collection<GNSSMeasurement> measurements;
68  
69      /**
70       * Current estimation containing user ECEF position, user ECEF velocity, clock offset
71       * and clock drift.
72       */
73      private GNSSEstimation estimation;
74  
75      /**
76       * Current Kalman filter state containing current GNSS estimation along with
77       * Kalman filter covariance error matrix.
78       */
79      private GNSSKalmanState state;
80  
81      /**
82       * Timestamp expressed in seconds since epoch time when Kalman filter state
83       * was last propagated.
84       */
85      private Double lastStateTimestamp;
86  
87      /**
88       * Indicates whether this estimator is running or not.
89       */
90      private boolean running;
91  
92      /**
93       * Constructor.
94       */
95      public GNSSKalmanFilteredEstimator() {
96      }
97  
98      /**
99       * Constructor.
100      *
101      * @param config GNSS Kalman filter configuration parameters (usually obtained
102      *               through calibration).
103      */
104     public GNSSKalmanFilteredEstimator(final GNSSKalmanConfig config) {
105         this.config = new GNSSKalmanConfig(config);
106     }
107 
108     /**
109      * Constructor.
110      *
111      * @param epochInterval minimum epoch interval expressed in seconds (s) between
112      *                      consecutive propagations or measurements.
113      * @throws IllegalArgumentException if provided epoch interval is negative.
114      */
115     public GNSSKalmanFilteredEstimator(final double epochInterval) {
116         try {
117             setEpochInterval(epochInterval);
118         } catch (final LockedException ignore) {
119             // never happens
120         }
121     }
122 
123     /**
124      * Constructor.
125      *
126      * @param listener listener to notify events raised by this instance.
127      */
128     public GNSSKalmanFilteredEstimator(final GNSSKalmanFilteredEstimatorListener listener) {
129         this.listener = listener;
130     }
131 
132     /**
133      * Constructor.
134      *
135      * @param config        GNSS Kalman filter configuration parameters (usually
136      *                      obtained through calibration).
137      * @param epochInterval minimum epoch interval expressed in seconds (s) between
138      *                      consecutive propagations or measurements.
139      * @throws IllegalArgumentException if provided epoch interval is negative.
140      */
141     public GNSSKalmanFilteredEstimator(final GNSSKalmanConfig config, final double epochInterval) {
142         this(epochInterval);
143         this.config = new GNSSKalmanConfig(config);
144     }
145 
146     /**
147      * Constructor.
148      *
149      * @param config   GNSS Kalman filter configuration parameters (usually
150      *                 obtained through calibration).
151      * @param listener listener to notify events raised by this instance.
152      */
153     public GNSSKalmanFilteredEstimator(
154             final GNSSKalmanConfig config, final GNSSKalmanFilteredEstimatorListener listener) {
155         this(config);
156         this.listener = listener;
157     }
158 
159     /**
160      * Constructor.
161      *
162      * @param epochInterval minimum epoch interval expressed in seconds (s) between
163      *                      consecutive propagations or measurements.
164      * @param listener      listener to notify events raised by this instance.
165      * @throws IllegalArgumentException if provided epoch interval is negative.
166      */
167     public GNSSKalmanFilteredEstimator(
168             final double epochInterval, final GNSSKalmanFilteredEstimatorListener listener) {
169         this(epochInterval);
170         this.listener = listener;
171     }
172 
173     /**
174      * Constructor.
175      *
176      * @param config        GNSS Kalman filter configuration parameters (usually obtained
177      *                      through calibration).
178      * @param epochInterval minimum epoch interval expressed in seconds (s) between
179      *                      consecutive propagations or measurements.
180      * @param listener      listener to notify events raised by this instance.
181      * @throws IllegalArgumentException if provided epoch interval is negative.
182      */
183     public GNSSKalmanFilteredEstimator(
184             final GNSSKalmanConfig config, final double epochInterval,
185             final GNSSKalmanFilteredEstimatorListener listener) {
186         this(config, epochInterval);
187         this.listener = listener;
188     }
189 
190     /**
191      * Constructor.
192      *
193      * @param epochInterval minimum epoch interval between consecutive
194      *                      propagations or measurements.
195      * @throws IllegalArgumentException if provided epoch interval is negative.
196      */
197     public GNSSKalmanFilteredEstimator(final Time epochInterval) {
198         this(TimeConverter.convert(epochInterval.getValue().doubleValue(), epochInterval.getUnit(), TimeUnit.SECOND));
199     }
200 
201     /**
202      * Constructor.
203      *
204      * @param config        GNSS Kalman filter configuration parameters (usually
205      *                      obtained through calibration).
206      * @param epochInterval minimum epoch interval between consecutive propagations
207      *                      or measurements.
208      * @throws IllegalArgumentException if provided epoch interval is negative.
209      */
210     public GNSSKalmanFilteredEstimator(final GNSSKalmanConfig config, final Time epochInterval) {
211         this(config, TimeConverter.convert(epochInterval.getValue().doubleValue(), epochInterval.getUnit(),
212                 TimeUnit.SECOND));
213     }
214 
215     /**
216      * Constructor.
217      *
218      * @param epochInterval minimum epoch interval between consecutive propagations
219      *                      or measurements.
220      * @param listener      listener to notify events raised by this instance.
221      * @throws IllegalArgumentException if provided epoch interval is negative.
222      */
223     public GNSSKalmanFilteredEstimator(final Time epochInterval, final GNSSKalmanFilteredEstimatorListener listener) {
224         this(epochInterval);
225         this.listener = listener;
226     }
227 
228     /**
229      * Constructor.
230      *
231      * @param config        GNSS Kalman filter configuration parameters (usually
232      *                      obtained through calibration).
233      * @param epochInterval minimum epoch interval between consecutive propagations
234      *                      or measurements.
235      * @param listener      listener to notify events raised by this instance.
236      * @throws IllegalArgumentException if provided epoch interval is negative.
237      */
238     public GNSSKalmanFilteredEstimator(
239             final GNSSKalmanConfig config, final Time epochInterval,
240             final GNSSKalmanFilteredEstimatorListener listener) {
241         this(config, epochInterval);
242         this.listener = listener;
243     }
244 
245     /**
246      * Gets listener to notify events raised by this instance.
247      *
248      * @return listener to notify events raised by this instance.
249      */
250     public GNSSKalmanFilteredEstimatorListener getListener() {
251         return listener;
252     }
253 
254     /**
255      * Sets listener to notify events raised by this instance.
256      *
257      * @param listener listener to notify events raised by this instance.
258      * @throws LockedException if this estimator is already running.
259      */
260     public void setListener(final GNSSKalmanFilteredEstimatorListener listener) throws LockedException {
261         if (running) {
262             throw new LockedException();
263         }
264 
265         this.listener = listener;
266     }
267 
268     /**
269      * Gets minimum epoch interval expressed in seconds (s) between consecutive
270      * propagations or measurements expressed in seconds.
271      * Attempting to propagate results using Kalman filter or updating measurements
272      * when intervals are less than this value, will be ignored.
273      *
274      * @return minimum epoch interval between consecutive propagations or
275      * measurements.
276      */
277     public double getEpochInterval() {
278         return epochInterval;
279     }
280 
281     /**
282      * Sets minimum epoch interval expressed in seconds (s) between consecutive
283      * propagations or measurements expressed in seconds.
284      * Attempting to propagate results using Kalman filter or updating measurements
285      * when intervals are less than this value, will be ignored.
286      *
287      * @param epochInterval minimum epoch interval expressed in seconds (s) between
288      *                      consecutive propagations or measurements.
289      * @throws LockedException          if this estimator is already running.
290      * @throws IllegalArgumentException if provided epoch interval is negative.
291      */
292     public void setEpochInterval(final double epochInterval) throws LockedException {
293         if (running) {
294             throw new LockedException();
295         }
296 
297         if (epochInterval < 0.0) {
298             throw new IllegalArgumentException();
299         }
300 
301         this.epochInterval = epochInterval;
302     }
303 
304     /**
305      * Gets minimum epoch interval between consecutive propagations or measurements.
306      * Attempting to propagate results using Kalman filter or updating measurements
307      * when intervals are less than this value, will be ignored.
308      *
309      * @param result instance where minimum epoch interval will be stored.
310      */
311     public void getEpochIntervalAsTime(final Time result) {
312         result.setValue(epochInterval);
313         result.setUnit(TimeUnit.SECOND);
314     }
315 
316     /**
317      * Gets minimum epoch interval between consecutive propagations or measurements.
318      * Attempting to propagate results using Kalman filter or updating measurements
319      * when intervals are less than this value, will be ignored.
320      *
321      * @return minimum epoch interval.
322      */
323     public Time getEpochIntervalAsTime() {
324         return new Time(epochInterval, TimeUnit.SECOND);
325     }
326 
327     /**
328      * Sets minimum epoch interval between consecutive propagations or measurements.
329      * Attempting to propagate results using Kalman filter or updating measurements
330      * when intervals are less than this value, will be ignored.
331      *
332      * @param epochInterval minimum epoch interval.
333      * @throws LockedException          if this estimator is already running.
334      * @throws IllegalArgumentException if provided epoch interval is negative.
335      */
336     public void setEpochInterval(final Time epochInterval) throws LockedException {
337         final var epochIntervalSeconds = TimeConverter.convert(epochInterval.getValue().doubleValue(),
338                 epochInterval.getUnit(), TimeUnit.SECOND);
339         setEpochInterval(epochIntervalSeconds);
340     }
341 
342     /**
343      * Gets GNSS Kalman configuration parameters (usually obtained through
344      * calibration).
345      *
346      * @param result instance where GNSS Kalman configuration parameters will be
347      *               stored.
348      * @return true if result instance is updated, false otherwise.
349      */
350     public boolean getConfig(final GNSSKalmanConfig result) {
351         if (config != null) {
352             result.copyFrom(config);
353             return true;
354         } else {
355             return false;
356         }
357     }
358 
359     /**
360      * Gets GNSS Kalman configuration parameters (usually obtained through
361      * calibration).
362      *
363      * @return GNSS Kalman configuration parameters.
364      */
365     public GNSSKalmanConfig getConfig() {
366         return config;
367     }
368 
369     /**
370      * Sets GNSS Kalman configuration parameters (usually obtained through
371      * calibration).
372      *
373      * @param config GNSS Kalman configuration parameters to be set.
374      * @throws LockedException if this estimator is already running.
375      */
376     public void setConfig(final GNSSKalmanConfig config) throws LockedException {
377         if (running) {
378             throw new LockedException();
379         }
380 
381         this.config = new GNSSKalmanConfig(config);
382     }
383 
384     /**
385      * Gets last updated GNSS measurements of a collection of satellites.
386      *
387      * @return last updated GNSS measurements of a collection of satellites.
388      */
389     public Collection<GNSSMeasurement> getMeasurements() {
390         if (measurements == null) {
391             return null;
392         }
393 
394         final var result = new ArrayList<GNSSMeasurement>();
395         for (final var measurement : measurements) {
396             result.add(new GNSSMeasurement(measurement));
397         }
398         return result;
399     }
400 
401     /**
402      * Gets current estimation containing user ECEF position, user ECEF velocity,
403      * clock offset and clock drift.
404      *
405      * @return current estimation containing user ECEF position, user ECEF velocity,
406      * clock offset and clock drift.
407      */
408     public GNSSEstimation getEstimation() {
409         return estimation != null ? new GNSSEstimation(estimation) : null;
410     }
411 
412     /**
413      * Gets current estimation containing user ECEF position, user ECEF velocity,
414      * clock offset and clock drift.
415      * This method does not update result instance if no estimation is available.
416      *
417      * @param result instance where estimation will be stored.
418      * @return true if result estimation was updated, false otherwise.
419      */
420     public boolean getEstimation(final GNSSEstimation result) {
421         if (estimation != null) {
422             result.copyFrom(estimation);
423             return true;
424         } else {
425             return false;
426         }
427     }
428 
429     /**
430      * Gets current Kalman filter state containing current GNSS estimation along with
431      * Kalman filter covariance error matrix.
432      *
433      * @return current Kalman filter state containing current GNSS estimation along
434      * with Kalman filter covariance error matrix.
435      */
436     public GNSSKalmanState getState() {
437         return state != null ? new GNSSKalmanState(state) : null;
438     }
439 
440     /**
441      * Gets current Kalman filter state containing current GNSS estimation along with
442      * Kalman filter covariance error matrix.
443      * This method does not update result instance if no state is available.
444      *
445      * @param result instance where state will be stored.
446      * @return true if result state was updated, false otherwise.
447      */
448     public boolean getState(final GNSSKalmanState result) {
449         if (state != null) {
450             result.copyFrom(state);
451             return true;
452         } else {
453             return false;
454         }
455     }
456 
457     /**
458      * Gets timestamp expressed in seconds since epoch time when Kalman filter state
459      * was last propagated.
460      *
461      * @return timestamp expressed in seconds since epoch time when Kalman filter
462      * state was last propagated.
463      */
464     public Double getLastStateTimestamp() {
465         return lastStateTimestamp;
466     }
467 
468     /**
469      * Gets timestamp since epoch time when Kalman filter state was last propagated.
470      *
471      * @param result instance where timestamp since epoch time when Kalman filter
472      *               state was last propagated will be stored.
473      * @return true if result instance is updated, false otherwise.
474      */
475     public boolean getLastStateTimestampAsTime(final Time result) {
476         if (lastStateTimestamp != null) {
477             result.setValue(lastStateTimestamp);
478             result.setUnit(TimeUnit.SECOND);
479             return true;
480         } else {
481             return false;
482         }
483     }
484 
485     /**
486      * Gets timestamp since epoch time when Kalman filter state was last propagated.
487      *
488      * @return timestamp since epoch time when Kalman filter state was last
489      * propagated.
490      */
491     public Time getLastStateTimestampAsTime() {
492         return lastStateTimestamp != null ? new Time(lastStateTimestamp, TimeUnit.SECOND) : null;
493     }
494 
495     /**
496      * Indicates whether this estimator is running or not.
497      *
498      * @return true if this estimator is running, false otherwise.
499      */
500     public boolean isRunning() {
501         return running;
502     }
503 
504     /**
505      * Indicates whether provided measurements are ready to
506      * be used for an update.
507      *
508      * @param measurements measurements to be checked.
509      * @return true if estimator is ready, false otherwise.
510      */
511     public static boolean isUpdateMeasurementsReady(final Collection<GNSSMeasurement> measurements) {
512         return GNSSLeastSquaresPositionAndVelocityEstimator.isValidMeasurements(measurements);
513     }
514 
515     /**
516      * Updates GNSS measurements of this estimator when new satellite measurements
517      * are available.
518      * Calls to this method will be ignored if interval between provided timestamp
519      * and last timestamp when Kalman filter was updated is less than epoch interval.
520      *
521      * @param measurements GNSS measurements to be updated.
522      * @param timestamp    timestamp since epoch time when GNSS measurements were
523      *                     updated.
524      * @return true if measurements were updated, false otherwise.
525      * @throws LockedException   if this estimator is already running.
526      * @throws NotReadyException if estimator is not ready for measurements updates.
527      * @throws GNSSException     if estimation fails due to numerical instabilities.
528      */
529     public boolean updateMeasurements(
530             final Collection<GNSSMeasurement> measurements, final Time timestamp)
531             throws LockedException, NotReadyException, GNSSException {
532         return updateMeasurements(measurements, TimeConverter.convert(
533                 timestamp.getValue().doubleValue(), timestamp.getUnit(), TimeUnit.SECOND));
534     }
535 
536     /**
537      * Updates GNSS measurements of this estimator when new satellite measurements
538      * are available.
539      * Call to this method will be ignored if interval between provided timestamp
540      * and last timestamp when Kalman filter was updated is less than epoch interval.
541      *
542      * @param measurements GNSS measurements to be updated.
543      * @param timestamp    timestamp expressed in seconds since epoch time when
544      *                     GNSS measurements were updated.
545      * @return true if measurements were updated, false otherwise.
546      * @throws LockedException   if this estimator is already running.
547      * @throws NotReadyException if estimator is not ready for measurements updates.
548      * @throws GNSSException     if estimation fails due to numerical instabilities.
549      */
550     public boolean updateMeasurements(
551             final Collection<GNSSMeasurement> measurements, final double timestamp)
552             throws LockedException, NotReadyException, GNSSException {
553 
554         if (running) {
555             throw new LockedException();
556         }
557 
558         if (!isUpdateMeasurementsReady(measurements)) {
559             throw new NotReadyException();
560         }
561 
562         if (lastStateTimestamp != null && timestamp - lastStateTimestamp <= epochInterval) {
563             return false;
564         }
565 
566         try {
567             running = true;
568 
569             if (listener != null) {
570                 listener.onUpdateStart(this);
571             }
572 
573             this.measurements = new ArrayList<>(measurements);
574 
575             lsEstimator.setMeasurements(this.measurements);
576             lsEstimator.setPriorPositionAndVelocityFromEstimation(estimation);
577             if (estimation != null) {
578                 lsEstimator.estimate(estimation);
579             } else {
580                 estimation = lsEstimator.estimate();
581             }
582 
583             if (listener != null) {
584                 listener.onUpdateEnd(this);
585             }
586 
587         } finally {
588             running = false;
589         }
590 
591         propagate(timestamp);
592 
593         return true;
594     }
595 
596     /**
597      * Indicates whether this estimator is ready for state propagations.
598      *
599      * @return true if estimator is ready, false otherwise.
600      */
601     public boolean isPropagateReady() {
602         return config != null && estimation != null;
603     }
604 
605     /**
606      * Propagates Kalman filter state held by this estimator at provided
607      * timestamp.
608      * Call to this method will be ignored if interval between provided timestamp
609      * and last timestamp when Kalman filter was updated is less than epoch interval.
610      *
611      * @param timestamp timestamp since epoch to propagate state.
612      * @return true if state was propagated, false otherwise.
613      * @throws LockedException   if this estimator is already running.
614      * @throws NotReadyException if estimator is not ready for measurements updates.
615      * @throws GNSSException     if estimation fails due to numerical instabilities.
616      */
617     public boolean propagate(final Time timestamp) throws LockedException, NotReadyException, GNSSException {
618         return propagate(TimeConverter.convert(timestamp.getValue().doubleValue(), timestamp.getUnit(),
619                 TimeUnit.SECOND));
620     }
621 
622     /**
623      * Propagates Kalman filter state held by this estimator at provided
624      * timestamp.
625      * Call to this method will be ignored if interval between provided timestamp
626      * and last timestamp when Kalman filter was updated is less than epoch interval.
627      *
628      * @param timestamp timestamp expressed in seconds since epoch to propagate state.
629      * @return true if state was propagated, false otherwise.
630      * @throws LockedException   if this estimator is already running.
631      * @throws NotReadyException if estimator is not ready for measurements updates.
632      * @throws GNSSException     if estimation fails due to numerical instabilities.
633      */
634     public boolean propagate(final double timestamp) throws LockedException, NotReadyException, GNSSException {
635 
636         if (running) {
637             throw new LockedException();
638         }
639 
640         if (!isPropagateReady()) {
641             throw new NotReadyException();
642         }
643 
644         final var propagationInterval = lastStateTimestamp != null ? timestamp - lastStateTimestamp : 0.0;
645         if (lastStateTimestamp != null && propagationInterval <= epochInterval) {
646             return false;
647         }
648 
649         try {
650             running = true;
651 
652             if (listener != null) {
653                 listener.onPropagateStart(this);
654             }
655 
656             if (state == null) {
657                 state = GNSSKalmanInitializer.initialize(estimation, config);
658             }
659 
660             GNSSKalmanEpochEstimator.estimate(measurements, propagationInterval, state, config, state);
661             lastStateTimestamp = timestamp;
662 
663             state.getEstimation(estimation);
664 
665             if (listener != null) {
666                 listener.onPropagateEnd(this);
667             }
668 
669         } catch (final AlgebraException e) {
670             throw new GNSSException(e);
671         } finally {
672             running = false;
673         }
674 
675         return true;
676     }
677 
678     /**
679      * Resets this estimator.
680      *
681      * @throws LockedException if this estimator is already running.
682      */
683     public void reset() throws LockedException {
684         if (running) {
685             throw new LockedException();
686         }
687 
688         running = true;
689         measurements = null;
690         estimation = null;
691         state = null;
692         lastStateTimestamp = null;
693 
694         if (listener != null) {
695             listener.onReset(this);
696         }
697 
698         running = false;
699     }
700 }