PeerCache.java

/* Copyright 2022-2026 Thales Alenia Space
 * 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.frames;

import org.hipparchus.CalculusFieldElement;
import org.hipparchus.Field;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.FieldAbsoluteDate;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.StampedLock;
import java.util.function.Function;
import java.util.function.Supplier;

/** Cache for frame transforms.
 * <p>
 * This class is thread-safe.
 * </p>
 * @author Luc Maisonobe
 * @since 13.1
 */
class PeerCache {

    /** Origin frame. */
    private final Frame origin;

    /** Cache for transforms with peer frame. */
    private volatile CachedTransformProvider cache;

    /** Lock for peer frame cache. */
    private final StampedLock lock;

    /** Cache for transforms with peer frame. */
    private volatile Map<Field<? extends CalculusFieldElement<?>>, FieldCachedTransformProvider<?>> fieldCaches;

    /** create an instance not associated with any peer.
     * @param origin origin frame
     */
    PeerCache(final Frame origin) {
        this.origin      = origin;
        this.cache       = null;
        this.fieldCaches = null;
        this.lock        = new StampedLock();
    }

    /** Associate a cache with a peer frame, caching transforms.
     * <p>
     * The cache is a LRU cache (Least Recently Used), so entries remain in
     * the cache if they are used frequently, and only older entries
     * that have not been accessed for a while will be expunged.
     * </p>
     * <p>
     * If a peer was already associated with this frame, it will be overridden.
     * </p>
     * <p>
     * Peering is unidirectional, i.e. if frameA is peered with frameB,
     * then frameB may be peered with another frameC or no frame at all.
     * This allows several frames to be peered with a pivot one (typically
     * Earth frame and many topocentric frames all peered with one inertial frame).
     * </p>
     * @param peer peer frame (if null, cache is cleared)
     * @param cacheSize number of transforms kept in the date-based cache
     */
    public void setPeerCaching(final Frame peer, final int cacheSize) {
        final long lockId = lock.writeLock();
        try {
            if (peer == null) {
                // clear peering
                cache       = null;
                fieldCaches = null;
            } else {
                // caching for regular dates
                cache       = createCache(peer, cacheSize);
                // caching for field dates
                fieldCaches = new ConcurrentHashMap<>();
            }
        } finally {
            lock.unlockWrite(lockId);
        }
    }

    /** Get the peer associated to this frame.
     * @return peer associated with this frame, null if not peered at all
     */
    Frame getPeer() {
        if (cache == null) {
            return null;
        } else {
            return optimisticRead(this::getPeerDestination);
        }
    }

    private Frame getPeerDestination() {
        return cache == null ? null : cache.getDestination();
    }

    /** Get the cached transform provider associated with this destination.
     * @param destination destination frame to which we want to transform vectors
     * @return cached transform provider, or null if destination is not the instance peer
     */
    CachedTransformProvider getCachedTransformProvider(final Frame destination) {
        if (cache == null)  {
            return null;
        } else {
            return optimisticRead(() -> getProvider(destination));
        }
    }

    private CachedTransformProvider getProvider(final Frame destination) {
        final Frame peerDestination = getPeerDestination();
        if (peerDestination == null || peerDestination != destination) {
            return null;
        } else {
            return cache;
        }
    }

    /** Get the cached transform provider associated with this destination.
     * @param <T> the type of the field elements
     * @param destination destination frame to which we want to transform vectors
     * @param field field elements belong to
     * @return cached transform provider, or null if destination is not the instance peer
     */
    <T extends CalculusFieldElement<T>> FieldCachedTransformProvider<T> getCachedTransformProvider(final Frame destination,
                                                                                                   final Field<T> field) {
        if (cache == null) {
            return null;
        } else {
            return optimisticRead(() -> getFieldProvider(destination, field));
        }
    }

    private <T> T optimisticRead(final Supplier<T> readFunction) {
        final long optimisticLockId = lock.tryOptimisticRead();
        final T optimisticResult = readFunction.get();
        if (lock.validate(optimisticLockId)) {
            return optimisticResult;
        } else {
            final long heavyLockId = lock.readLock();
            try {
                return readFunction.get();
            } finally {
                lock.unlockRead(heavyLockId);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private <T extends CalculusFieldElement<T>> FieldCachedTransformProvider<T> getFieldProvider(final Frame destination, final Field<T> field) {
        final CachedTransformProvider provider = getProvider(destination);
        if (provider == null) {
            return null;
        } else {
            return (FieldCachedTransformProvider<T>) fieldCaches.computeIfAbsent(field, f -> createCache(destination,
                                                                                  provider.getCacheSize(),
                                                                                  field));
        }
    }

    /** Create cache.
     * @param peer peer frame
     * @param cacheSize number of transforms kept in the date-based cache
     * @return built cache
     * @since 13.0.3
     */
    private CachedTransformProvider createCache(final Frame peer, final int cacheSize) {
        final Function<AbsoluteDate, Transform> fullGenerator =
                date -> origin.getTransformTo(peer,
                                              Transform.IDENTITY,
                                              frame -> frame.getTransformProvider().getTransform(date),
                                              (t1, t2) -> new Transform(date, t1, t2),
                                              Transform::getInverse);
        final Function<AbsoluteDate, KinematicTransform> kinematicGenerator =
                date -> origin.getTransformTo(peer,
                                              KinematicTransform.getIdentity(),
                                              frame -> frame.getTransformProvider().getTransform(date),
                                              (t1, t2) -> KinematicTransform.compose(date, t1, t2),
                                              KinematicTransform::getInverse);
        final Function<AbsoluteDate, StaticTransform> staticGenerator =
                date -> origin.getTransformTo(peer,
                                              StaticTransform.getIdentity(),
                                              frame -> frame.getTransformProvider().getTransform(date),
                                              (t1, t2) -> StaticTransform.compose(date, t1, t2),
                                              StaticTransform::getInverse);
        return new CachedTransformProvider(origin, peer,
                                           fullGenerator, kinematicGenerator, staticGenerator,
                                           cacheSize);
    }

    /** Create field cache.
     * @param <T> type of the field elements
     * @param peer peer frame
     * @param cacheSize number of transforms kept in the date-based cache
     * @param field field elements belong to
     * @return built cache
     * @since 13.0.3
     */
    private <T extends CalculusFieldElement<T>> FieldCachedTransformProvider<T>
        createCache(final Frame peer, final int cacheSize, final Field<T> field) {
        final Function<FieldAbsoluteDate<T>, FieldTransform<T>> fullGenerator =
                d -> origin.getTransformTo(peer,
                                           FieldTransform.getIdentity(field),
                                           frame -> frame.getTransformProvider().getTransform(d),
                                           (FieldTransform<T> t1, FieldTransform<T> t2) -> new FieldTransform<>(d, t1, t2),
                                           FieldTransform::getInverse);
        final Function<FieldAbsoluteDate<T>, FieldKinematicTransform<T>> kinematicGenerator =
                d -> origin.getTransformTo(peer,
                                           FieldKinematicTransform.getIdentity(field),
                                           frame -> frame.getTransformProvider().getTransform(d),
                                           (t1, t2) -> FieldKinematicTransform.compose(d, t1, t2),
                                           FieldKinematicTransform::getInverse);
        final Function<FieldAbsoluteDate<T>, FieldStaticTransform<T>> staticGenerator =
                d -> origin.getTransformTo(peer,
                                           FieldStaticTransform.getIdentity(field),
                                           frame -> frame.getTransformProvider().getTransform(d),
                                           (t1, t2) -> FieldStaticTransform.compose(d, t1, t2),
                                           FieldStaticTransform::getInverse);
        return new FieldCachedTransformProvider<>(origin, peer,
                                                  fullGenerator, kinematicGenerator, staticGenerator,
                                                  cacheSize);
    }

}