Seize.java

package nl.tudelft.simulation.dsol.formalisms.flow;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;

import org.djutils.event.EventType;
import org.djutils.exceptions.Throw;
import org.djutils.metadata.MetaData;
import org.djutils.metadata.ObjectDescriptor;

import nl.tudelft.simulation.dsol.simtime.SimTime;
import nl.tudelft.simulation.dsol.simulators.DevsSimulatorInterface;
import nl.tudelft.simulation.dsol.statistics.SimPersistent;
import nl.tudelft.simulation.dsol.statistics.SimTally;

/**
 * The Seize flow block requests a certain amount of capacity from a resource and keeps the entity within the flow block's
 * storage until the resource is actually claimed. Note that the Seize block has a storage in which the Entity is waiting, while
 * the Resource has a queue in which the request is waiting. This sounds like we store the same information twice. This is,
 * however, not the case. (1) Multiple Seize blocks can share the same Resource, each holding their own entities that make the
 * request, where the total set of requests is stored at the Resource. (2) A Seize could potentially request access to two
 * resources, where the entity is held at the Seize block until both are available. Each Resource keeps and grants its own
 * requests. So, there is an n:m relationship between Seize blocks and Resources, each have their own queue / storage.
 * <p>
 * Copyright (c) 2002-2025 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
 * for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The DSOL
 * project is distributed under a three-clause BSD-style license, which can be found at
 * <a href="https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
 * </p>
 * @author <a href="https://www.linkedin.com/in/peterhmjacobs">Peter Jacobs </a>
 * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
 * @param <T> the time type
 */
public abstract class Seize<T extends Number & Comparable<T>> extends FlowBlock<T, Seize<T>> implements CapacityRequestor<T>
{
    /** Storage for waiting entities with their arrival time. */
    protected final Set<StoredEntity<T>> storage = Collections.synchronizedSet(new HashSet<>());

    /** persistent statistic for the number in store. */
    private SimPersistent<T> numberStoredStatistic = null;

    /** tally statistic for the time-in-storage of the entities. */
    private SimTally<T> storageTimeStatistic = null;

    /** NUMBER_STORED_EVENT is fired when the queue length changes. */
    public static final EventType NUMBER_STORED_EVENT = new EventType(new MetaData("NUMBER_STORED_EVENT",
            "Number of entities stored", new ObjectDescriptor("numberStored", "Number entities stored", Integer.class)));

    /** STORAGE_TIME is fired when an entity leaves the storage. */
    public static final EventType STORAGE_TIME_EVENT = new EventType(new MetaData("STORAGE_TIME_EVENT", "Storage time",
            new ObjectDescriptor("storageTime", "Storage time (as a double)", Double.class)));

    /**
     * Constructor for Seize flow object.
     * @param id the id of the FlowObject
     * @param simulator on which behavior is scheduled
     */
    public Seize(final String id, final DevsSimulatorInterface<T> simulator)
    {
        super(id, simulator);
    }

    /**
     * Return the storage for waiting entities with their arrival time.
     * @return the storage for waiting entities with their arrival time
     */
    public Set<StoredEntity<T>> getStorage()
    {
        return this.storage;
    }

    /**
     * Return the resource that is claimed by entities in this Seize block.
     * @return the resource that is claimed by entities in this Seize block
     */
    public abstract Resource<T, ?> getResource();

    /**
     * Turn on the default statistics for this Seize block.
     * @return the Seize instance for method chaining
     */
    public Seize<T> setDefaultStatistics()
    {
        if (!hasDefaultStatistics())
        {
            super.setDefaultFlowBlockStatistics();
            this.numberStoredStatistic = new SimPersistent<>("Seize.NumberStored:" + getBlockNumber(),
                    getId() + " nr entities stored", getSimulator().getModel(), this, NUMBER_STORED_EVENT);
            this.numberStoredStatistic.initialize();
            fireTimedEvent(NUMBER_STORED_EVENT, this.storage.size(), getSimulator().getSimulatorTime());
            this.storageTimeStatistic = new SimTally<>("Seize.StorageTime:" + getBlockNumber(),
                    getId() + " entity storage time", getSimulator().getModel(), this, STORAGE_TIME_EVENT);
            getResource().setDefaultStatistics();
        }
        return this;
    }

    /**
     * Return whether statistics are turned on for this Storage block.
     * @return whether statistics are turned on for this Storage block.
     */
    public boolean hasDefaultStatistics()
    {
        return this.numberStoredStatistic != null;
    }

    /**
     * Return the persistent statistic for the number of entities in store.
     * @return the persistent statistic for the number of entities in store
     */
    public SimPersistent<T> getNumberStoredStatistic()
    {
        return this.numberStoredStatistic;
    }

    /**
     * Return the tally statistic for the time-in-storage of the entities.
     * @return the tally statistic for the time-in-storage of the entities
     */
    public SimTally<T> getStorageTimeStatistic()
    {
        return this.storageTimeStatistic;
    }

    /**
     * Resource with floating point capacity.
     * @param <T> the time type
     */
    public static class DoubleCapacity<T extends Number & Comparable<T>> extends Seize<T>
            implements CapacityRequestor.DoubleCapacity<T>
    {
        /** The resource that is claimed by entities in this Seize block. */
        protected final Resource.DoubleCapacity<T> resource;

        /** The fixed amount of resource requested by each entity. */
        private double fixedCapacityClaim;

        /** The flexible, possibly entity-dependent, amount of resource requested by an entity. */
        private ToDoubleFunction<Entity<T>> capacityClaimFunction;

        /**
         * Constructor for Seize flow object with floating point capacity.
         * @param id the id of the FlowObject
         * @param simulator on which behavior is scheduled
         * @param resource that is claimed in this Seize block
         */
        public DoubleCapacity(final String id, final DevsSimulatorInterface<T> simulator,
                final Resource.DoubleCapacity<T> resource)
        {
            super(id, simulator);
            this.resource = resource;
            setFixedCapacityClaim(1.0);
        }

        /**
         * Set a fixed capacity claim. The alternative is a capacity claim calculated by a function.
         * @param fixedCapacityClaim the fixed claim that every entity makes for a fixed capacity
         * @return the object for method chaining
         */
        public Seize.DoubleCapacity<T> setFixedCapacityClaim(final double fixedCapacityClaim)
        {
            Throw.when(fixedCapacityClaim < 0.0, IllegalArgumentException.class, "capacity cannot be < 0");
            this.fixedCapacityClaim = fixedCapacityClaim;
            this.capacityClaimFunction = (entity) ->
            {
                return this.fixedCapacityClaim;
            };
            return this;
        }

        /**
         * Set an entity-specific capacity claim as indicated by a function.
         * @param capacityClaimFunction the function that calculates the needed capacity
         * @return the object for method chaining
         */
        public Seize.DoubleCapacity<T> setFlexibleCapacityClaim(final ToDoubleFunction<Entity<T>> capacityClaimFunction)
        {
            Throw.whenNull(capacityClaimFunction, "capacityClaimFunction cannot be null");
            this.fixedCapacityClaim = Double.NaN;
            this.capacityClaimFunction = capacityClaimFunction;
            return this;
        }

        /**
         * Return the resource that is claimed by entities in this Seize block.
         * @return the resource that is claimed by entities in this Seize block
         */
        @Override
        public Resource.DoubleCapacity<T> getResource()
        {
            return this.resource;
        }

        /**
         * Return the fixed capacity claim, or NaN when the capacity claim is calculated by a provided function.
         * @return the fixed capacity claim, or NaN when the capacity claim is calculated by a provided function
         */
        public double getFixedCapacityClaim()
        {
            return this.fixedCapacityClaim;
        }

        /**
         * Receive an object that requests an amount of units from a resource.
         * @param entity the object
         * @param requestedCapacity the requested capacity
         */
        protected synchronized void receiveEntity(final Entity<T> entity, final double requestedCapacity)
        {
            super.receiveEntity(entity);
            var storedEntity = new StoredEntity<T>(entity, requestedCapacity, getSimulator().getSimulatorTime());
            synchronized (this.storage)
            {
                this.storage.add(storedEntity);
            }
            this.fireTimedEvent(Seize.NUMBER_STORED_EVENT, this.storage.size(), getSimulator().getSimulatorTime());
            getResource().requestCapacity(entity, requestedCapacity, this);
        }

        @Override
        public void receiveEntity(final Entity<T> entity)
        {
            double capacityClaim = this.capacityClaimFunction.applyAsDouble(entity);
            Throw.when(capacityClaim < 0.0, IllegalArgumentException.class, "capacity cannot be < 0");
            this.receiveEntity(entity, capacityClaim);
        }

        @Override
        public void receiveRequestedCapacity(final Entity<T> entity, final double capacityClaim,
                final Resource.DoubleCapacity<T> resource)
        {
            for (StoredEntity<T> storedEntity : this.storage)
            {
                if (storedEntity.entity().equals(entity))
                {
                    synchronized (this.storage)
                    {
                        this.storage.remove(storedEntity);
                    }
                    this.fireTimedEvent(Seize.NUMBER_STORED_EVENT, this.storage.size(), getSimulator().getSimulatorTime());
                    T delay = SimTime.minus(getSimulator().getSimulatorTime(), storedEntity.storeTime());
                    this.fireTimedEvent(Seize.STORAGE_TIME_EVENT, delay.doubleValue(), getSimulator().getSimulatorTime());
                    this.releaseEntity(storedEntity.entity());
                    return;
                }
            }
        }
    }

    /**
     * Resource with integer capacity.
     * @param <T> the time type
     */
    public static class IntegerCapacity<T extends Number & Comparable<T>> extends Seize<T>
            implements CapacityRequestor.IntegerCapacity<T>
    {
        /** The resource that is claimed by entities in this Seize block. */
        protected final Resource.IntegerCapacity<T> resource;

        /** The fixed amount of resource requested by each entity. */
        private int fixedCapacityClaim;

        /** The flexible, possibly entity-dependent, amount of resource requested by an entity. */
        private ToIntFunction<Entity<T>> capacityClaimFunction;

        /**
         * Constructor for Seize flow object with floating point capacity.
         * @param id the id of the FlowObject
         * @param simulator on which behavior is scheduled
         * @param resource that is claimed in this Seize block
         */
        public IntegerCapacity(final String id, final DevsSimulatorInterface<T> simulator,
                final Resource.IntegerCapacity<T> resource)
        {
            super(id, simulator);
            this.resource = resource;
            setFixedCapacityClaim(1);
        }

        /**
         * Set a fixed capacity claim. The alternative is a capacity claim calculated by a function.
         * @param fixedCapacityClaim the fixed claim that every entity makes for a fixed capacity
         * @return the object for method chaining
         */
        public Seize.IntegerCapacity<T> setFixedCapacityClaim(final int fixedCapacityClaim)
        {
            Throw.when(fixedCapacityClaim < 0.0, IllegalArgumentException.class, "capacity cannot be < 0");
            this.fixedCapacityClaim = fixedCapacityClaim;
            this.capacityClaimFunction = (entity) ->
            {
                return this.fixedCapacityClaim;
            };
            return this;
        }

        /**
         * Set an entity-specific capacity claim as indicated by a function.
         * @param capacityClaimFunction the function that calculates the needed capacity
         * @return the object for method chaining
         */
        public Seize.IntegerCapacity<T> setFlexibleCapacityClaim(final ToIntFunction<Entity<T>> capacityClaimFunction)
        {
            Throw.whenNull(capacityClaimFunction, "capacityClaimFunction cannot be null");
            this.fixedCapacityClaim = -1;
            this.capacityClaimFunction = capacityClaimFunction;
            return this;
        }

        /**
         * Return the resource that is claimed by entities in this Seize block.
         * @return the resource that is claimed by entities in this Seize block
         */
        @Override
        public Resource.IntegerCapacity<T> getResource()
        {
            return this.resource;
        }

        /**
         * Return the fixed capacity claim, or -1 when the capacity claim is calculated by a provided function.
         * @return the fixed capacity claim, or -1 when the capacity claim is calculated by a provided function
         */
        public int getFixedCapacityClaim()
        {
            return this.fixedCapacityClaim;
        }

        /**
         * Receive an object that requests an amount of units from a resource.
         * @param entity the object
         * @param requestedCapacity the requested capacity
         */
        protected synchronized void receiveEntity(final Entity<T> entity, final int requestedCapacity)
        {
            super.receiveEntity(entity);
            var storedEntity = new StoredEntity<T>(entity, requestedCapacity, getSimulator().getSimulatorTime());
            synchronized (this.storage)
            {
                this.storage.add(storedEntity);
            }
            this.fireTimedEvent(Seize.NUMBER_STORED_EVENT, this.storage.size(), getSimulator().getSimulatorTime());
            getResource().requestCapacity(entity, requestedCapacity, this);
        }

        @Override
        public void receiveEntity(final Entity<T> entity)
        {
            int capacityClaim = this.capacityClaimFunction.applyAsInt(entity);
            Throw.when(capacityClaim < 0.0, IllegalArgumentException.class, "capacity cannot be < 0");
            this.receiveEntity(entity, capacityClaim);
        }

        @Override
        public void receiveRequestedCapacity(final Entity<T> entity, final int capacityClaim,
                final Resource.IntegerCapacity<T> resource)
        {
            for (StoredEntity<T> storedEntity : this.storage)
            {
                if (storedEntity.entity().equals(entity))
                {
                    synchronized (this.storage)
                    {
                        this.storage.remove(storedEntity);
                    }
                    this.fireTimedEvent(Seize.NUMBER_STORED_EVENT, this.storage.size(), getSimulator().getSimulatorTime());
                    T delay = SimTime.minus(getSimulator().getSimulatorTime(), storedEntity.storeTime());
                    this.fireTimedEvent(Seize.STORAGE_TIME_EVENT, delay.doubleValue(), getSimulator().getSimulatorTime());
                    this.releaseEntity(storedEntity.entity());
                    return;
                }
            }
        }
    }

    /**
     * The stored entity.
     * @param <T> the time type
     * @param entity the entity making the capacity request
     * @param amount the amount of resources that the entity requested
     * @param storeTime the time of storage
     */
    static record StoredEntity<T extends Number & Comparable<T>>(Entity<T> entity, Number amount, T storeTime)
    {
    }
}