All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.unibo.alchemist.model.cognitive.steering.SinglePrevalent.kt Maven / Gradle / Ivy

There is a newer version: 35.0.0
Show newest version
/*
 * Copyright (C) 2010-2023, Danilo Pianini and contributors
 * listed, for each module, in the respective subproject's build.gradle.kts file.
 *
 * This file is part of Alchemist, and is distributed under the terms of the
 * GNU General Public License, with a linking exception,
 * as described in the file LICENSE in the Alchemist distribution's top directory.
 */

package it.unibo.alchemist.model.cognitive.steering

import it.unibo.alchemist.model.Node
import it.unibo.alchemist.model.cognitive.NavigationAction
import it.unibo.alchemist.model.cognitive.SteeringAction
import it.unibo.alchemist.model.cognitive.SteeringStrategy
import it.unibo.alchemist.model.cognitive.actions.NavigationAction2D
import it.unibo.alchemist.model.cognitive.steering.SinglePrevalent.ExponentialSmoothing
import it.unibo.alchemist.model.environments.Euclidean2DEnvironmentWithGraph
import it.unibo.alchemist.model.geometry.ConvexPolygon
import it.unibo.alchemist.model.geometry.Vector
import it.unibo.alchemist.model.positions.Euclidean2DPosition

private typealias SteeringActions = List>

/**
 * A [SteeringStrategy] in which one action is prevalent. Only [NavigationAction]s can be prevalent, because
 * they guarantee to navigate the environment consciously (e.g. without getting stuck in obstacles). The
 * purpose of this strategy is to linearly combine the potentially contrasting forces to which the node
 * is subject, while maintaining that warranty. Such forces are combined as follows:
 * let f be the prevalent force,
 * - if f leads the node outside the room (= environment's area) he/she is into, no combination is performed
 * and f is used as it is. This because crossing doors can be a thorny issue, and we don't want to introduce
 * disturbing forces.
 * - Otherwise, a linear combination is performed: f is assigned unitary weight, all other forces are assigned
 * weight w equal to the maximum value in [0,1] so that the resulting force:
 * - forms with f an angle smaller than or equal to the specified [toleranceAngle],
 * - doesn't lead the node outside the current room.
 * The idea is to decrease the intensity of non-prevalent forces until the resulting one enters some tolerance
 * sector defined by both the tolerance angle and the current room's boundary. With a suitable tolerance angle
 * this allows to steer the node towards the target defined by the prevalent force, while using a trajectory
 * which takes into account other urges as well.
 * Finally, an exponential smoothing with the given [alpha] is applied to the resulting force in order to decrease
 * oscillatory movements (this also known as shaking behavior).
 *
 * @param T concentration type
 * @param N type of nodes of the environment's graph.
 */
class SinglePrevalent(
    environment: Euclidean2DEnvironmentWithGraph<*, T, N, *>,
    node: Node,
    private val prevalent: SteeringActions.() -> NavigationAction2D,
    /**
     * Tolerance angle in radians.
     */
    private val toleranceAngle: Double = DEFAULT_TOLERANCE_ANGLE,
    /**
     * Alpha value for the [ExponentialSmoothing].
     */
    private val alpha: Double = DEFAULT_ALPHA,
    /**
     * Function computing the maximum distance the node can walk.
     */
    private val maxWalk: () -> Double,
    /**
     * When the node is subject to contrasting forces the resulting one may be small in magnitude.
     * This parameter allows to specify a minimum magnitude for the resulting force computed as
     * [maxWalk] * [maxWalkRatio]
     */
    private val maxWalkRatio: Double = DEFAULT_MAX_WALK_RATIO,
    /**
     * To determine weight w so that the resulting force satisfies the conditions described above, such
     * quantity is initially set to 1.0 and then iteratively decreased by delta until a suitable weight
     * has been found. In other words, the time complexity for computing w is O(1 / delta). This can be
     * reduced to O(1) in the future.
     */
    private val delta: Double = DEFAULT_DELTA,
) : Weighted(environment, node, { 0.0 }) {

    /**
     * Default values for the parameters.
     */
    companion object {
        /**
         * On average, it was observed that this value allows the pedestrian not to get stuck in obstacles.
         */
        const val DEFAULT_TOLERANCE_ANGLE = Math.PI / 4

        /**
         * Empirically found to produce a good smoothing while leaving enough freedom of movement to the pedestrian
         * (e.g. to perform sudden changes of direction).
         */
        const val DEFAULT_ALPHA = 0.5

        /**
         * Empirically found to produce natural movements.
         */
        const val DEFAULT_MAX_WALK_RATIO = 0.3

        /**
         * Good trade-off between efficiency and accuracy.
         */
        const val DEFAULT_DELTA = 0.05
    }

    private val expSmoothing = ExponentialSmoothing(alpha)

    override fun computeNextPosition(actions: SteeringActions): Euclidean2DPosition =
        with(actions.prevalent()) {
            val prevalentForce = this.nextPosition()
            val leadsOutsideCurrentRoom: Euclidean2DPosition.() -> Boolean = {
                checkNotNull(currentRoom) { "currentRoom should be defined" }
                    .let { !it.containsBoundaryIncluded(pedestrianPosition + this) }
            }
            if (prevalentForce == environment.origin ||
                currentRoom == null ||
                prevalentForce.leadsOutsideCurrentRoom()
            ) {
                return prevalentForce
            }
            val otherForces = (actions - this).map { it.nextPosition() }
            val isInToleranceSector: Euclidean2DPosition.() -> Boolean = {
                magnitude > 0.0 && angleBetween(prevalentForce) <= toleranceAngle && !leadsOutsideCurrentRoom()
            }
            var othersWeight = 1.0
            var resulting = combine(prevalentForce, otherForces, othersWeight)
            while (!resulting.isInToleranceSector() && othersWeight >= 0) {
                othersWeight -= delta
                resulting = combine(prevalentForce, otherForces, othersWeight)
            }
            resulting = resulting.takeIf { othersWeight > 0 } ?: prevalentForce
            (expSmoothing.apply(resulting).takeIf { !it.leadsOutsideCurrentRoom() } ?: resulting)
                .coerceIn(maxWalk() * maxWalkRatio, maxWalk())
        }

    /**
     * Linearly combines the forces assigning [othersWeight] to [others] and unitary weight to [prevalent].
     */
    private fun > combine(prevalent: V, others: List, othersWeight: Double): V =
        (others.map { it * othersWeight } + prevalent).reduce { acc, force -> acc + force }

    /**
     * Exponential smoothing is a trivial way of smoothing signals.
     * Let s(t) be the smoothed signal at time t, given a discrete signal g:
     * s(t) = alpha * g(t) + (1 - alpha) * s(t-1)
     * s(0) = g(0)
     */
    private class ExponentialSmoothing>(
        private val alpha: Double,
    ) {

        init {
            require(alpha in 0.0..1.0) { "alpha should be in [0,1]" }
        }

        private var previous: V? = null

        /**
         * Applies the smoothing to the given force.
         */
        fun apply(current: V): V {
            val new = previous?.let { current.times(alpha) + it.times(1 - alpha) } ?: current
            previous = new
            return new
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy