1   /* Copyright 2002-2026 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.estimation.measurements.gnss;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.stream.Collectors;
23  
24  import org.hipparchus.linear.MatrixUtils;
25  import org.hipparchus.linear.QRDecomposer;
26  import org.hipparchus.linear.RealMatrix;
27  import org.hipparchus.linear.RealVector;
28  import org.hipparchus.util.FastMath;
29  import org.orekit.errors.OrekitIllegalArgumentException;
30  import org.orekit.errors.OrekitMessages;
31  import org.orekit.utils.drivers.ParameterDriver;
32  
33  /** Class for solving integer ambiguity problems.
34   * @see LambdaMethod
35   * @author Luc Maisonobe
36   * @since 10.0
37   */
38  public class AmbiguitySolver {
39  
40      /** Drivers for ambiguity drivers. */
41      private final List<ParameterDriver> ambiguityDrivers;
42  
43      /** Solver for the underlying Integer Least Square problem. */
44      private final IntegerLeastSquareSolver solver;
45  
46      /** Acceptance test to use. */
47      private final AmbiguityAcceptance acceptance;
48  
49      /** Simple constructor.
50       * @param ambiguityDrivers drivers for ambiguity parameters
51       * @param solver solver for the underlying Integer Least Square problem
52       * @param acceptance acceptance test to use
53       * @see LambdaMethod
54       */
55      public AmbiguitySolver(final List<ParameterDriver> ambiguityDrivers,
56                             final IntegerLeastSquareSolver solver,
57                             final AmbiguityAcceptance acceptance) {
58          this.ambiguityDrivers = ambiguityDrivers;
59          this.solver           = solver;
60          this.acceptance       = acceptance;
61      }
62  
63      /** Get all the ambiguity parameters drivers.
64       * @return all ambiguity parameters drivers
65       */
66      public List<ParameterDriver> getAllAmbiguityDrivers() {
67          return Collections.unmodifiableList(ambiguityDrivers);
68      }
69  
70      /** Get the ambiguity parameters drivers that have not been fixed yet.
71       * @return ambiguity parameters drivers that have not been fixed yet
72       */
73      protected List<ParameterDriver> getFreeAmbiguityDrivers() {
74          return ambiguityDrivers.
75                          stream().
76                          filter(d -> {
77                              if (d.isSelected()) {
78                                  // in order to make the code generic and compatible with pDriver having
79                                  // 1 or several values driven getValue is called with a "random date"
80                                  // it should be OK as we take the near number
81                                  final double near   = FastMath.rint(d.getValue());
82                                  final double gapMin = near - d.getMinValue();
83                                  final double gapMax = d.getMaxValue() - near;
84                                  return FastMath.max(FastMath.abs(gapMin), FastMath.abs(gapMax)) > 1.0e-15;
85                              } else {
86                                  return false;
87                              }
88                          }).
89                          collect(Collectors.toList());
90      }
91  
92      /** Get ambiguity indirection array for ambiguity parameters drivers that have not been fixed yet.
93       * @param startIndex start index for measurements parameters in global covariance matrix
94       * @param measurementsParametersDrivers measurements parameters drivers in global covariance matrix order
95       * @return indirection array between full covariance matrix and ambiguity covariance matrix
96       */
97      protected int[] getFreeAmbiguityIndirection(final int startIndex,
98                                                  final List<ParameterDriver> measurementsParametersDrivers) {
99  
100         // set up indirection array
101         final List<ParameterDriver> freeDrivers = getFreeAmbiguityDrivers();
102         final List<String> measurementsPDriversNames = new ArrayList<>();
103         final int totalValuesToEstimate = freeDrivers.size();
104         for (ParameterDriver measDriver : measurementsParametersDrivers) {
105             measurementsPDriversNames.add(measDriver.getName());
106         }
107 
108         final int[] indirection = new int[totalValuesToEstimate];
109         int nb = 0;
110         for (ParameterDriver freeDriver : freeDrivers) {
111 
112             indirection[nb] = -1;
113 
114             for (int k = 0; k < measurementsPDriversNames.size(); ++k) {
115                 if (freeDriver.getName().equals(measurementsPDriversNames.get(k))) {
116                     indirection[nb] = startIndex + k;
117                     break;
118                 }
119             }
120 
121             if (indirection[nb] < 0) {
122                 // the parameter was not found
123                 final StringBuilder builder = new StringBuilder();
124                 for (final String driverName : measurementsPDriversNames) {
125                     if (!builder.isEmpty()) {
126                         builder.append(", ");
127                     }
128                     builder.append(driverName);
129                 }
130                 throw new OrekitIllegalArgumentException(OrekitMessages.UNSUPPORTED_PARAMETER_NAME,
131                                                          freeDriver.getName(), builder.toString());
132             }
133             nb++;
134         }
135 
136         return indirection;
137 
138     }
139 
140     /** Un-fix an integer ambiguity (typically after a phase cycle slip).
141      * @param ambiguityDriver driver for the ambiguity to un-fix
142      */
143     public void unFixAmbiguity(final ParameterDriver ambiguityDriver) {
144         ambiguityDriver.setMinValue(Double.NEGATIVE_INFINITY);
145         ambiguityDriver.setMaxValue(Double.POSITIVE_INFINITY);
146     }
147 
148     /** Fix integer ambiguities.
149      * @param startIndex start index for measurements parameters in global covariance matrix
150      * @param measurementsParametersDrivers measurements parameters drivers in global covariance matrix order
151      * @param covariance global covariance matrix
152      * @return list of newly fixed ambiguities (ambiguities already fixed before the call are not counted)
153      */
154     public List<ParameterDriver> fixIntegerAmbiguities(final int startIndex,
155                                                        final List<ParameterDriver> measurementsParametersDrivers,
156                                                        final RealMatrix covariance) {
157 
158         // set up Integer Least Square problem
159         final List<ParameterDriver> ambiguities      = getAllAmbiguityDrivers();
160 
161         // construct float ambiguities array
162         final double[] floatAmbiguities = new double[ambiguities.size()];
163         int floatAmbRank = 0;
164         for (ParameterDriver pDriver : ambiguities) {
165             floatAmbiguities[floatAmbRank++] = pDriver.getValue();
166         }
167 
168         final int[]                 indirection      = getFreeAmbiguityIndirection(startIndex, measurementsParametersDrivers);
169         // solve the ILS problem
170         final IntegerLeastSquareSolution[] candidates =
171                         solver.solveILS(acceptance.numberOfCandidates(), floatAmbiguities, indirection, covariance);
172 
173         // FIXME A cleaner way is:
174         //     1°/ Add a getName() method in IntegerLeastSquareSolver interface
175         //     2°/ Add static name attribute to IntegerBootstrapping, LAMBDA and ModifiedLAMBDA classes
176         if (solver instanceof IntegerBootstrapping && candidates.length == 0) {
177             return Collections.emptyList();
178         }
179 
180         // check number of candidates
181         if (candidates.length < acceptance.numberOfCandidates()) {
182             return Collections.emptyList();
183         }
184 
185         // check acceptance
186         final IntegerLeastSquareSolution bestCandidate = acceptance.accept(candidates);
187         if (bestCandidate == null) {
188             return Collections.emptyList();
189         }
190 
191         // fix the ambiguities
192         final long[] fixedAmbiguities = bestCandidate.getSolution();
193         final List<ParameterDriver> fixedDrivers = new ArrayList<>(indirection.length);
194         final int nb = measurementsParametersDrivers.size();
195         for (int i = 0; i < nb; ++i) {
196             final ParameterDriver driver = measurementsParametersDrivers.get(indirection[i] - startIndex);
197             driver.setMinValue(fixedAmbiguities[i]);
198             driver.setMaxValue(fixedAmbiguities[i]);
199             fixedDrivers.add(driver);
200         }
201 
202         // Update the others parameter drivers accordingly to the fixed integer ambiguity
203         // Covariance matrix between integer ambiguity and the other parameter driver
204         final RealMatrix Qab = getCovMatrix(covariance, indirection);
205 
206         final RealVector X = new QRDecomposer(1.0e-10).decompose(getAmbiguityMatrix(covariance, indirection)).solve(MatrixUtils.createRealVector(floatAmbiguities).
207                                                                                                            subtract(MatrixUtils.createRealVector(toDoubleArray(fixedAmbiguities.length, fixedAmbiguities))));
208         final RealVector Y =  Qab.preMultiply(X);
209 
210         int entry = 0;
211         for (int i = startIndex + 1; i < covariance.getColumnDimension(); i++) {
212             if (!belongTo(indirection, i)) {
213                 final ParameterDriver driver = measurementsParametersDrivers.get(i - startIndex);
214                 driver.setValue(driver.getValue() - Y.getEntry(entry++ - startIndex));
215             }
216         }
217 
218         return fixedDrivers;
219 
220     }
221 
222    /** Get the covariance matrix between the integer ambiguities and the other parameter driver.
223     * @param cov global covariance matrix
224     * @param indirection array of the position of integer ambiguity parameter driver
225     * @return covariance matrix.
226     */
227     private RealMatrix getCovMatrix(final RealMatrix cov, final int[] indirection) {
228         final RealMatrix Qab = MatrixUtils.createRealMatrix(indirection.length, cov.getColumnDimension());
229         int index = 0;
230         int iter  = 0;
231         while (iter < indirection.length) {
232             // Loop on column dimension
233             for (int j = 0; j < cov.getColumnDimension(); j++) {
234                 if (!belongTo(indirection, j)) {
235                     Qab.setEntry(index, 0, cov.getEntry(index, 0));
236                 }
237             }
238             index++;
239             iter++;
240         }
241         return Qab;
242     }
243 
244      /** Return the matrix of the ambiguity from the global covariance matrix.
245       * @param cov global covariance matrix
246       * @param indirection array of the position of the ambiguity within the global covariance matrix
247       * @return matrix of ambiguities covariance
248       */
249     private RealMatrix getAmbiguityMatrix(final RealMatrix cov, final int[] indirection) {
250         final RealMatrix Qa = MatrixUtils.createRealMatrix(indirection.length, indirection.length);
251         for (int i = 0; i < indirection.length; i++) {
252             Qa.setEntry(i, i, cov.getEntry(indirection[i], indirection[i]));
253             for (int j = 0; j < i; j++) {
254                 Qa.setEntry(i, j, cov.getEntry(indirection[i], indirection[j]));
255                 Qa.setEntry(j, i, cov.getEntry(indirection[i], indirection[j]));
256             }
257         }
258         return Qa;
259     }
260 
261     /** Compute whether or not the integer pos belongs to the indirection array.
262      * @param indirection array of the position of ambiguities within the global covariance matrix
263      * @param pos integer for which we want to know if it belong to the indirection array.
264      * @return true if it belongs.
265      */
266     private boolean belongTo(final int[] indirection, final int pos) {
267         for (int j : indirection) {
268             if (pos == j) {
269                 return true;
270             }
271         }
272         return false;
273     }
274 
275     /** Transform an array of long to an array of double.
276      * @param size size of the destination array
277      * @param longArray source array
278      * @return the destination array
279      */
280     private double[] toDoubleArray(final int size, final long[] longArray) {
281         // Initialize double array
282         final double[] doubleArray = new double[size];
283         // Copy the elements
284         for (int index = 0; index < size; index++) {
285             doubleArray[index] = longArray[index];
286         }
287         return doubleArray;
288     }
289 
290 }