# Source code for pypdevsbbl.domain.industry

    #  Copyright 2020 Modelling, Simulation and Design Lab (MSDL) at
    #  McGill University and the University of Antwerp (http://msdl.cs.mcgill.ca/)
    #
    #  Licensed 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.

    """Domain-specific building block for industrial purposes.

    """
    from pypdevsbbl.generic.queues import SimpleQueue
    from pypdevsbbl.generic.transformers import Transformer
    from pypdevs.DEVS import CoupledDEVS, AtomicDEVS
    from pypdevs.infinity import INFINITY

    class ConveyorBelt(CoupledDEVS):
        """A conveyor belt with items moving over it.

        Basically, a conveyor can be seen as a glorified queue that uses
        speed for timing purposes. Internally, there is a
        :class:`pypdevsbbl.generic.queues.SimpleQueue` and a
        :class:`pypdevsbbl.generic.transformers.Transformer` that have been
        called ``<name>-queue`` and ``<name>-transformer`` respectively,
        where ``<name>`` indicates the current block's name.

        .. image:: ../paper/images/blocks/industry/conveyor-belt.svg
    		:align: center

        Args:
            name (str):         The name of the block.
            v (numeric):        The conveyor speed.
            L (numeric):        The length of the conveyor.
            K (numeric):        The capacity of the conveyor. Defaults to
                                ``INFINITY``.

        Input Ports:
            enter (any):        The items that enter the conveyor belt.
            velocity (numeric): A new velocity at which the conveyor must run.

        Output Ports:
            leave (any):        The items that leave the conveyor belt.
            overflow (any):     The items that leave the conveyor because of
                                overflow.
        """
        def __init__(self, name, v, L, K=INFINITY):
            super(ConveyorBelt, self).__init__(name)

            self.L = L
            self.enter = self.addInPort("enter")
            self.velocity = self.addInPort("velocity")
            self.leave = self.addOutPort("leave")
            self.overflow = self.addOutPort("overflow")

            self._queue = self.addSubModel(SimpleQueue("%s-queue" % name, K=K, dr=self.L / v))
            self._transformer = self.addSubModel(Transformer("%s-transformer" % name, lambda i, _, __: self.L / i))

            self.connectPorts(self.enter, self._queue.enqueue)
            self.connectPorts(self.velocity, self._transformer.input)
            self.connectPorts(self._transformer.transformed, self._queue.dr)
            self.connectPorts(self._queue.dequeue, self.leave)
            self.connectPorts(self._queue.overflow, self.overflow)

    from pypdevsbbl.extra.spatial import distance
    import numpy as np
    class Transporter(AtomicDEVS):
        """Transports an item linearly over a path.

        When not :attr:`chained`, the item is picked up at the start of
        the path before being transported to the end of the path, where
        it is dropped off. Next, we return over a straight line to the
        beginning and start over again.

        When :attr:`chained`, instead of returning to the beginning, we
        move to the next position where items can be picked up and start
        over again.

        .. image:: ../paper/images/blocks/industry/transporter.svg
    		:align: center

        Args:
            name (str):             The name of the block.
            path (iter):            An ordered set of :class:`pypdevsbbl.extra.spatial.Coordinate`
                                    objects, indicative of each point in the path. It is
                                    assumed there is a full stop between all points in the
                                    path. There must be at least one point.
            vmax (numeric):         The maximal velocity of the transporter.
            a (numeric):            Strictly positive value that indicates the acceletarion
                                    of the transporter. Must not be ``INFINITY``. Defaults
                                    to 1.
            d (numeric):            Strictly positive value that indicates the deceleration
                                    of the transporter. Must not be ``INFINITY``. Defaults
                                    to 1.
            dist (def):             Distance function to use. As arguments, it takes two
                                    :class:`pypdevsbbl.extra.spatial.Coordinate` objects and it
                                    returns a numeric value, indicative of the distance.
                                    See also :class:`pypdevsbbl.extra.spatial.distance`. Defaults
                                    to :meth:`pypdevsbbl.extra.spatial.distance.euclidean`.
            chained (bool):         Indicator that the block is chained. Defaults to ``False``.
            start (Coordinate):     The starting point of the transporter. It can be used to
                                    indicate a depository where the transporter starts. When
                                    ``None``, the beginning of the path is assumed. If
                                    :attr:`chained` is ``True``, this must be set to indicate
                                    the first block and/or its offset. All other blocks in the
                                    chain **must** have this value set to ``None``. Defaults
                                    to ``None``.
            passthrough (bool):     Can be used to indicate that this transporter will only
                                    follow a path and not pick up any items. In a chain, this
                                    allows for more complex motion to the next pickup point.
                                    Only useful within that context and is therefore only set
                                    if :attr:`chained` is ``True``.
            eps (numeric):          The maximal distance from a point the transformer may be to
                                    be assumed at that location. Defaults to ``1e-6``.

        State:
            item (any):             The item that's being carried.
            input (any):            The new item that arrives.
            elapsed (numeric):      The elapsed time since last event.
            duration (numeric):     The duration until a next event is scheduled.

        Input Ports:
            input (any):            A new item that wants to be transported.
            previous (Coordinate):  The end location in a chained transformer.

        Output Ports:
            restock (bool):         A simple message that indicates the block can accept a new
                                    item for transportation.
            blocked (any):          The items that don't have permission to enter.
            output (any):           The items that are dropped off.
            next (Coordinate):      The end location in a chained transformer.
        """
        def __init__(self, name, path, vmax, a = 1, d = 1, dist = distance.euclidean,
                     chained=False, start = None, passthrough=False, eps=1e-6):
            assert len(path) > 0 and 0 < a < INFINITY and 0 < d < INFINITY
            super(Transporter, self).__init__(name)
            self.chained = chained
            self.path = path
            self.dist = dist
            self.eps = eps

            self.vmax = vmax
            self.a = a
            self.d = d

            self.passthrough = passthrough and chained

            dur = self.dist(start, path[0]) if start is not None else 0.0
            if self.chained and start is None:
                dur = INFINITY

            self.state = {
                "item": None,
                "input": None,
                "elapsed": 0.0,
                "duration": dur
            }

            self.input = self.addInPort("input")
            self.previous = self.addInPort("previous")

            self.restock = self.addOutPort("restock")
            self.blocked = self.addOutPort("blocked")
            self.output = self.addOutPort("output")
            self.next = self.addOutPort("next")

        def duration(self):
            """Actual computation of the duration of the full path."""
            time = 0.0
            a = self.a
            d = self.d
            I = (1.0 / a) + (1.0 / d)
            vmax = self.vmax
            for i in range(len(self.path[:-1])):
                dx = self.dist(self.path[i], self.path[i + 1])
                v = np.sqrt((2 * a * d * dx) / (a + d))
                v = np.min(v, vmax)
                xc = dx - np.power(v, 2) * (d + a) / (2 * a * d)
                time += (v * I) + (xc / v)
            return time

        def timeAdvance(self):
            if self.state["input"] is not None:
                return 0.0
            if self.state["item"] is not None or self.state["duration"] > self.eps:
                return max(0.0, self.state["duration"] - self.state["elapsed"])
            return INFINITY

        def outputFnc(self):
            res = {}
            if self.state["item"] is not None or self.passthrough:
                if self.state["input"] is not None:
                    res[self.blocked] = self.state["input"]
                else:
                    res[self.output] = self.state["item"]
                    if self.chained:
                        res[self.next] = self.path[-1]
            else:
                if self.state["input"] is not None and self.state["duration"] > self.eps:
                    res[self.blocked] = self.state["input"]
                else:
                    res[self.restock] = True
            return res

        def intTransition(self):
            if self.state["item"] is not None or self.passthrough:
                if self.state["input"] is None: # OUTPUT
                    self.state["item"] = None
                    self.state["elapsed"] = 0.0
                    if self.chained:
                        self.state["duration"] = INFINITY
                    else:
                        self.state["duration"] = self.dist(self.path[0], self.path[-1])
            else:
                if self.state["input"] is not None:
                    self.state["item"] = self.state["input"]
                    self.state["elapsed"] = 0.0
                    self.state["duration"] = self.duration()
            self.state["input"] = None
            return self.state

        def extTransition(self, inputs):
            self.state["elapsed"] += self.elapsed
            if self.input in inputs:
                self.state["input"] = inputs[self.input]
            if self.previous in inputs:
                last = inputs[self.previous]
                if self.state["duration"] == INFINITY:
                    self.state["duration"] = self.dist(last, self.path[0])
            return self.state