AlternatingSampler.java

/* Copyright 2002-2024 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.estimation.measurements.gnss;

import org.hipparchus.util.FastMath;

/** Sampler for generating long integers between two limits in an alternating pattern.
 * <p>
 * Given a center a and a radius r, this class will generate integers kᵢ such
 * that a - r ≤ kᵢ ≤ a + r. The generation order will start from the middle
 * (i.e. k₀ is the long integer closest to a) and go towards the boundaries,
 * alternating between values lesser than a and values greater than a.
 * For example, with a = 17.3 and r = 5.2, it will generate: k₀ = 17, k₁ = 18,
 * k₂ = 16, k₃ = 19, k₄ = 15, k₅ = 20, k₆ = 14, k₇ = 21, k₈ = 13, k₉ = 22.
 * </p>
 * <p>
 * There are no hard limits to the generation, i.e. in the example above, the
 * generator will happily generate k₁₀ = 12, k₁₁ = 23, k₁₂ = 11... In fact, if
 * there are no integers at all between {@code a - r} and {@code a + r}, even
 * the initial k₀ that is implicitly generated at construction will be out of
 * range. The {@link #inRange()} method can be used to check if the last generator
 * is still producing numbers within the initial range or if it has already
 * started generating out of range numbers.
 * </p>
 * <p>
 * If there are integers between {@code a - r} and {@code a + r}, it is guaranteed
 * that they will all be generated once before {@link #inRange()} starts returning
 * {@code false}.
 * </p>
 * <p>
 * This allows to explore the range for one integer ambiguity starting
 * with the most probable values (closest to a) and continuing with
 * values less probable.
 * </p>
 * @see <a href="https://www.researchgate.net/publication/2790708_The_LAMBDA_method_for_integer_ambiguity_estimation_implementation_aspects">
 * The LAMBDA method for integer ambiguity estimation: implementation aspects</a>
 * @see <a href="https://oeis.org/A001057">
 * A001057: Canonical enumeration of integers: interleaved positive and negative integers with zero prepended.</a>
 * @author Luc Maisonobe
 * @since 10.0
 */
class AlternatingSampler {

    /** Range midpoint. */
    private final double a;

    /** Offset with respect to A001057. */
    private final long offset;

    /** Sign with respect to A001057. */
    private final long sign;

    /** Minimum number to generate. */
    private long min;

    /** Maximum number to generate. */
    private long max;

    /** Previous generated number in A001057. */
    private long k1;

    /** Current generated number in A001057. */
    private long k0;

    /** Current generated number. */
    private long current;

    /** Simple constructor.
     * <p>
     * A first initial integer is already generated as a side effect of
     * construction, so {@link #getCurrent()} can be called even before
     * calling {@link #generateNext()}. If there are no integers at
     * all between {@code a - r} and {@code a + r}, then this initial
     * integer will already be out of range.
     * </p>
     * @param a range midpoint
     * @param r range radius
     */
    AlternatingSampler(final double a, final double r) {

        this.a      = a;
        this.offset = (long) FastMath.rint(a);
        this.sign   = offset <= a ? +1 : -1;
        setRadius(r);

        this.k1      = 0;
        this.k0      = 0;
        this.current = offset;
    }

    /** Reset the range radius.
     * <p>
     * Resetting radius is allowed during sampling, it simply changes
     * the boundaries used when calling {@link #inRange()}. Resetting
     * the radius does not change the sampling itself, neither the
     * {@link #getCurrent() current} value nor the {@link #generateNext()
     * next generated} ones.
     * </p>
     * <p>
     * A typical use case for calling {@link #setRadius(double)} during
     * sampling is to reduce sampling interval. It is used to shrink
     * the search ellipsoid on the fly in LAMBDA-based methods in order
     * to speed-up search.
     * </p>
     * @param r range radius
     */
    public void setRadius(final double r) {
        this.min = (long) FastMath.ceil(a - r);
        this.max = (long) FastMath.floor(a + r);
    }

    /** Get the range midpoint.
     * @return range midpoint
     */
    public double getMidPoint() {
        return a;
    }

    /** Get current value.
     * @return current value
     */
    public long getCurrent() {
        return current;
    }

    /** Check if the current value is within range.
     * @return true if current value is within range
     */
    public boolean inRange() {
        return min <= current && current <= max;
    }

    /** Generate next value.
     */
    public void generateNext() {

        // apply A001057 recursion
        final long k2 = k1;
        k1 = k0;
        k0 = 1 - (k1 << 1) - k2;

        // take offset and sign into account
        current = offset + sign * k0;

    }

}