AttentionMatrix.java

package org.opentrafficsim.road.gtu.lane.perception.mental.channel;

import org.djutils.exceptions.Throw;

import Jama.Matrix;

/**
 * This class describes attention over channels, based on task demand per channel. Transition probabilities are based on demand
 * per channel, where drivers are assumed to keep perceiving the same channel by the demand of that channel alone. When total
 * demand is above 1, this means that the probability of switching to another channel is reduced. All transition probabilities
 * together result in an overall steady-state, which describes what fraction of time is spent on what channel.
 * <p>
 * Copyright (c) 2024-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
 * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
 * </p>
 * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
 */
public class AttentionMatrix
{

    /** Mental task demand, i.e. desired fraction of time for perception, per channel. */
    private final double[] demand;

    /** Attention, i.e. fraction of time, per channel. */
    private double[] attention;

    /** Anticipation reliance per channel. */
    private double[] anticipationReliance;

    /**
     * Constructor which pre-calculates attention distribution assuming drivers are serial mono-taskers. The probability of
     * staying on a task is the task demand of the task, while the probability of switching is the complement. The task that
     * will then be switched to is selected by weighting them by their task demand. This creates a transition matrix in a Markov
     * chain. The steady-state of this Markov chain is the attention distribution.
     * @param demand level of mental task demand per channel.
     * @throws IllegalArgumentException when a demand value is below 0 or larger than or equal to 1
     */
    public AttentionMatrix(final double[] demand)
    {
        int n = demand.length;
        this.demand = new double[n];
        System.arraycopy(demand, 0, this.demand, 0, n);
        this.attention = new double[n];
        this.anticipationReliance = new double[n];

        double demandSum = 0.0;
        for (int i = 0; i < n; i++)
        {
            Throw.when(demand[i] < 0.0, IllegalArgumentException.class, "Demand must be >= 0");
            Throw.when(demand[i] >= 1.0, IllegalArgumentException.class, "Demand must be < 1");
            demandSum += demand[i];
        }
        if (demandSum == 0.0)
        {
            return;
        }
        if (demandSum <= 1.0)
        {
            this.attention = this.demand;
            return;
        }

        /*
         * matrix is the transition matrix in a Markov chain describing the probability of the next perception glance to be
         * towards channel j, given previous channel i.
         */
        Matrix matrix = new Matrix(n, n);
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
            {
                /*
                 * As we need a left-eigenvector (v*P = 1*v, v=eigenvector, P=matrix), we pre-transpose the data setting at (j,
                 * i). Note that left-eigenvectors are the transposed right-eigenvectors of matrix'. We do not care for how it
                 * is transposed.
                 */
                if (i == j)
                {
                    // probability to keep perceiving the same channel is the demand of the channel
                    matrix.set(j, i, demand[i]);
                }
                else if (demandSum > demand[i])
                {
                    /*
                     * The probability of a switch to another channel is 1 - TD(i). The relative probabilities of the other
                     * channels to be switched to, is proportional to the demand in these channels TD(j). These are normalized
                     * by the total sum of demand minus the demand of the channel we switch from (i.e. the sum of demand of the
                     * other channels), and scaled by the probability to switch 1 - TD(i).
                     */
                    matrix.set(j, i, (1 - demand[i]) * demand[j] / (demandSum - demand[i]));
                }
                else
                {
                    // there is only one channel with task demand, do not switch
                    matrix.set(j, i, 0.0);
                }
            }
        }

        /*
         * We use Jama to find the eigenvector of the transition matrix pertaining to the eigenvalue 1. Each Markov transition
         * matrix has an eigenvalue 1, and the pertaining eigenvector is the steady-state. This steady state is the distribution
         * of attention (in time) over the channels.
         */
        var ed = matrix.eig();
        double[] eigenValues = ed.getRealEigenvalues();
        // find the eigenvalue closest to 1 (these values are not highly exact)
        int eigenIndex = 0;
        double dMin = 1.0;
        for (int i = 0; i < n; i++)
        {
            double di = Math.abs(eigenValues[i] - 1.0);
            if (di < dMin)
            {
                dMin = di;
                eigenIndex = i;
            }
        }
        // obtain the eigenvector pertaining to the eigenvalue of 1
        double[][] v = ed.getV().getArray();
        double sumEigenVector = 0.0;
        for (int i = 0; i < n; i++)
        {
            this.attention[i] = v[i][eigenIndex];
            sumEigenVector += this.attention[i];
        }
        // normalize so it sums to 1
        for (int i = 0; i < n; i++)
        {
            this.attention[i] = this.attention[i] / sumEigenVector;
        }

        /*
         * Anticipation reliance per channel is the difference between the steady state (actual proportion of time we perceive a
         * channel) and the desired proportion of time to perceive a channel.
         */
        for (int i = 0; i < n; i++)
        {
            this.anticipationReliance[i] = this.demand[i] - this.attention[i];
        }
    }

    /**
     * Returns the fraction of time that is spent on channel <i>i</i>.
     * @param i index of channel.
     * @return fraction of time that is spent on channel <i>i</i>.
     */
    public double getAttention(final int i)
    {
        return this.attention[i];
    }

    /**
     * Returns the level of anticipation reliance for channel <i>i</i>. This is the fraction of time that is reduced from
     * perceiving channel <i>i</i>, relative to the desired fraction of time to perceive channel <i>i</i>.
     * @param i index of channel.
     * @return level of anticipation reliance for channel <i>i</i>.
     */
    public double getAnticipationReliance(final int i)
    {
        return this.anticipationReliance[i];
    }

    /**
     * Returns the deterioration of channel <i>i</i>. This is the anticipation reliance for channel <i>i</i>, divided by the
     * desired level of attention for channel <i>i</i>. This value is an indication of perception delay for the channel.
     * <p>
     * If demand for the channel is 0, this method returns 1.
     * @param i index of channel.
     * @return fraction of anticipation reliance over desired attention for channel <i>i</i>.
     */
    public double getDeterioration(final int i)
    {
        return this.demand[i] == 0.0 ? 1.0 : this.anticipationReliance[i] / this.demand[i];
    }

}