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 }