Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Put requests for different anonymous resource coming frome same component #31

Closed
PhilippWillms opened this issue May 8, 2022 · 3 comments

Comments

@PhilippWillms
Copy link

PhilippWillms commented May 8, 2022

I observed today in the trace, that if a component triggers 2 one put-request to anonymous resources, one of them will always fail.
However, analyzing the code I could identify a check which prevents the successful behavior.

`

 def honor_all(self):
     for r in self._requests:
        if r._honor_only_first and r._requesters[0] != self:
            return []
        self_prio = self.priority(r._requesters)
        if r._honor_only_highest_priority and self_prio != r._requesters._head.successor.priority:
            return []
        if self._requests[r] > 0:
            if self._requests[r] > (r._capacity - r._claimed_quantity + 1e-8):
                return []
        else:
            if -self._requests[r] > r._claimed_quantity + 1e-8:
                 return []
    return list(self._requests.keys())`

Maybe it is a numerical issue, but commenting the check if -self._requests[r] > r._claimed_quantity + 1e-8 out fixes the issue.

After that the trace will contain the following lines (R31 is the component, p31 and p32 are anonymous resources).

R31 put (request) 2.0 to p31 priority=inf
276 R31 claim -2.0 from p31
276 R31 request honor p31 scheduled for 0.300 @ 276+ mode=output
279 R31 put (request) 8.0 to p32 priority=inf
279 R31 claim -8.0 from p32
279 R31 request honor p32 scheduled for 0.300 @ 279+ mode=output

@salabim
Copy link
Owner

salabim commented May 9, 2022

Could you include the code that demonstrates this behaviour and generates the trace?

@PhilippWillms
Copy link
Author

Hello,

I tried to put a minimum example, and indeed the erroneous behavior is reproducible. Running the code below with salabim 22.0.2, the trace shows even both put requests fail, not only one.

I am happy to join a 1:1 chat to improve the simulation model design if this helps to overcome the issue.

line# time current component action information


221 default environment initialize
221 main create
221 0.000 main current
20 p22 create capacity=1000 anonymous
20 p31 create capacity=1000 anonymous
20 p32 create capacity=1000 anonymous
248 R31 create
248 R31 activate scheduled for 0.000 @ 149 process=process
141 jobsatmachine.0 create
142 idle create value = False
251 p31-process create data component
261 job1 create
261 job1 activate scheduled for 0.000 @ 126 process=process
267 main run +15.000 scheduled for 15.000 @ 267+
149 0.000 R31 current
155 standby @ 155+ mode=Idle
126 0.000 job1 current
131 job1 enter jobsatmachine.0
134 job1 passivate @ 134+
155+ 0.000 R31 current (standby)
156 idle set value = True
167 job1 activate scheduled for 0.000 @ 134+
171 idle set value = False
173 R31 request 100 from p22 priority=inf
173 R31 claim 100 from p22
173 R31 request honor p22 scheduled for 0.000 @ 173+ mode=Idle
182 R31 hold +0.050 scheduled for 0.050 @ 182+ mode=In production
134+ 0.000 job1 current
134+ job1 ended
182+ 0.050 R31 current
190 R31 put (request) 20.0 to p31 priority=inf
190 R31 request scheduled for inf @ 190+ mode=output
193 R31 request failed
193 R31 put (request) 80.0 to p32 priority=inf
193 R31 request scheduled for inf @ 193+ mode=output
206 idle set value = True
209 job1 leave jobsatmachine.0
155 R31 request failed
155 standby @ 155+ mode=Idle
267+ 15.000 main current

import gym
import salabim as sim


class JobsAtMachine(sim.Queue):
    pass


"""Helper class to separate intermediates and main ingredients"""


class Material(sim.Resource):

    def __init__(self, name, initial_stock=0, low_limit_stock=0, max_limit_stock=1000, storable=True):
        """Initialize the Material as an anonymous resource as per the Salabim Gas station example"""

        super().__init__(
            name=name,
            capacity=max_limit_stock,
            anonymous=True,
            preemptive=False,
            honor_only_first=False,
            honor_only_highest_priority=False,
            monitor=True
            # env = 'simEnv' -> default automatically taken
        )

        self.demand = 0
        self.requirement = 0
        self._min_stock = low_limit_stock
        self._max_stock = max_limit_stock
        self._current_stock = initial_stock
        self.storable = True
        if (max_limit_stock == 0):
            self.storable = False

        monitor_name = 'stock' + self.name()
        self.current_stock_mon = sim.Monitor(
            monitor_name, monitor=True, level=True, initial_tally=initial_stock, type="float")

    def changeStockSimple(self, quantity):
        """Change stock quantity as per parameter (sign-specific) and trigger monitor"""
        self._current_stock += quantity
        self.current_stock_mon.tally(self._current_stock)
        
    def getCurrentStock(self):
        """Get current stock"""
        return self._current_stock


"""
    A batch process has certain processing time, which results in "duration" for
    the salabim environment. The output product is the leading characteristic.
    Execution is limited to a certain production line.
"""


class BatchProcess(sim.Component):

    def setup(self, main_product, side_product, fraction_main_product, batch_proc_time,
              ingredient_one, ingredient_two, fraction_ingred_one, resource):

        self.main_product = main_product
        self.side_product = side_product
        self.fraction_main_product = fraction_main_product
        self.duration = batch_proc_time
        self.ingredient_one = ingredient_one
        self.ingredient_two = ingredient_two
        self.fraction_ingred_one = fraction_ingred_one
        self.resource = resource
        
"""
    A job connects a BatchProcess() object with a target output quantity
"""


class Job(sim.Component):

    def setup(self, batch_process):
        self.batch_process = batch_process
        self.completed = False
        self.successor = None

    def setJobQuantity(self, quantity):
        self.targetQuantity = quantity
        
    def inventoryAvailable(self):
        """Inventory check is done by the job itself as it holds all recipe information"""

        # Simple case: just one input component
        if (self.batch_process.ingredient_two is None):
            input_quantity_1 = self.targetQuantity * \
                self.batch_process.fraction_ingred_one
            if (input_quantity_1 <= self.batch_process.ingredient_one.getCurrentStock()):
                return input_quantity_1, 0
            else:
                return -1, -1

        # Two input components
        if (self.batch_process.ingredient_two is not None):
            input_quantity_1 = self.targetQuantity * \
                self.batch_process.fraction_ingred_one
            input_quantity_2 = self.targetQuantity * \
                (1 - self.batch_process.fraction_ingred_one)

            if (input_quantity_1 <= self.batch_process.ingredient_one.getCurrentStock()) & (input_quantity_2 <= self.batch_process.ingredient_two.getCurrentStock()):
                return input_quantity_1, input_quantity_2
            else:
                return -1, -1

    def getProductionYield(self):
        """Calculate yield based on job quantity and output fractions"""

        new_quantity_main = self.targetQuantity * \
            self.batch_process.fraction_main_product
        if (self.batch_process.side_product is not None):
            side_quantity = self.targetQuantity * \
                (1 - self.batch_process.fraction_main_product)
        else:
            side_quantity = 0
        return new_quantity_main, side_quantity

    def process(self):
        """Overwrite inherited method of salabim.Component
        Put the job to the resource queue and activate it to get work done.
        Passivate the job until it will be called by the resource.
        """
        self.enter(self.batch_process.resource.jobs)
        if (self.batch_process.resource.ispassive()):
            self.batch_process.resource.activate(keep_request=True)
        yield self.passivate()
        

class Resource(sim.Component):

    def setup(self):
        self.occupied = False
        self.jobs = JobsAtMachine()  # Queue of batch jobs
        self.idle = sim.State('idle', value=False)
        self._resource_display_height = -1

    def set_batch_sizes(self, min_batch_size, max_batch_size):
        self.min_batch_size = min_batch_size
        self.max_batch_size = max_batch_size

    def process(self):
        """Simulation logic """
        while True:

            # Idle state if no jobs available and not occupied
            if self.occupied == False & self.jobs.length() == 0:
                yield self.standby(mode="Idle")
                self.idle.set()

            # If there is at least one job, check inventory status and start production
            if not self.occupied & self.jobs.length() > 0:

                for job in self.jobs:

                    req1, req2 = job.inventoryAvailable()

                    if (req1 + req2 > 0):

                        job.activate()

                        # Occupy resources for duration time -> process duration
                        self.occupied = True
                        self.idle.set(False)

                        self.request((job.batch_process.ingredient_one, req1))
                        job.batch_process.ingredient_one.changeStockSimple(
                            req1 * (-1))

                        if (req2 > 0):
                            self.request((job.batch_process.ingredient_two, req2))
                            job.batch_process.ingredient_two.changeStockSimple(
                                req2 * (-1))

                        yield self.hold(job.batch_process.duration, mode="In production")

                        # Stock creation after production finish
                        output1, output2 = job.getProductionYield()

                        if (output2 > 0):   # main and side product output
                           
                            # Perform stock booking on material level
                            self.put((job.batch_process.main_product, output1), mode="output")
                            job.batch_process.main_product.changeStockSimple(output1)
                            
                            self.put((job.batch_process.side_product, output2), mode="output")
                            job.batch_process.side_product.changeStockSimple(output2)

                        else:   # only main product
                        
                            # Perform stock booking on material level
                            job.batch_process.main_product.changeStockSimple(
                                output1)
                            
                            self.put((job.batch_process.main_product, output1), mode="output")
                        
                        # Release resource
                        self.occupied = False
                        self.idle.set(True)
                        # Set job completion for resource queue handling
                        job.completed = True
                        job.leave(self.jobs)

        # After process method ended, component becomes passive


class Env(gym.Env):

    def __init__(self, *args, **kwargs):

        # Create simulation environment, which is object-wise also the reference for
        # all components and resources

        self.simEnv = sim.Environment(
            trace=True,
            random_seed=None,
            set_numpy_random_seed=True,
            time_unit='days',
            name=None,
            print_trace_header=True,
            isdefault_env=True,
            retina=False,
            do_reset=None,
            blind_animation=False
        )
        self.simEnv.animate(False)

        self.materials = []
        self.resources = []
        self.processes = []
        self.demands = []
        self.jobs = []
        
        inputMat = Material("p22", initial_stock=500)
        self.materials.append(inputMat)
        output1 = Material("p31")
        self.materials.append(output1)
        output2 = Material("p32")
        self.materials.append(output2)
        
        resource = Resource("R31")
        self.resources.append(resource)
        
        byProductProcess = BatchProcess("p31-process", main_product=output1,
                                 side_product=output2,
                                 fraction_main_product=0.2,
                                 batch_proc_time=0.05,
                                 ingredient_one=inputMat,
                                 ingredient_two=None,
                                 fraction_ingred_one=1,
                                 resource=resource)
        self.processes.append(resource)
        
        job = Job("job1", batch_process=byProductProcess)
        job.setJobQuantity(100)
        self.jobs.append(job)

    def runSimulation(self, lastDay, animate):
        """Execute the simulation"""
        self.simEnv.run(till=lastDay)
        self.simEnv.quit()
       
  
def main():
    """Main method running the sim"""
    test = Env()
    test.runSimulation(lastDay=15, animate=False)

if __name__ == "__main__":
    main()

@salabim
Copy link
Owner

salabim commented May 13, 2022

As far as I can see from the code above, I think this is intended behaviour.
In order to show that I made a small model that does nothing else than put once 20 and once 80 in a resource with a capacity 100 from which nothing has been claimed. The put request fails properly as the resource is at its capacity.
Then, a bit later, I claim 100 from the resource and you can see that immediately, the putting resource gets honoured.

import salabim as sim


class Putter(sim.Component):
    def process(self):
        yield self.put((r, 20))
        yield self.hold(1)
        yield self.put((r, 80))


class Getter(sim.Component):
    def process(self):
        yield self.hold(2)
        yield self.get((r, 100))


env = sim.Environment(trace=True)
r = sim.Resource("r", capacity=100, anonymous=True)
Putter()
Getter()
env.run()

with output:

line#        time current component    action                               information
------ ---------- -------------------- -----------------------------------  ------------------------------------------------
                                       line numbers refers to               demo.py
   16                                  default environment initialize
   16                                  main create
   16       0.000 main                 current
   17                                  r create                             capacity=100 anonymous
   18                                  putter.0 create
   18                                  putter.0 activate                    scheduled for 0.000 @    5  process=process
   19                                  getter.0 create
   19                                  getter.0 activate                    scheduled for 0.000 @   11  process=process
   20                                  main run                             ends on no events left   @   20+
    5       0.000 putter.0             current
    6                                  putter.0                             put (request) 20 to r priority=inf
    6                                  putter.0 request                     scheduled for inf @    6+
   11       0.000 getter.0             current
   12                                  getter.0 hold +2.000                 scheduled for 2.000 @   12+
   12+      2.000 getter.0             current
   13                                  getter.0                             get (request) 100 from r priority=inf
   13                                  getter.0                             claim 100 from r
   13                                  getter.0 request honor r             scheduled for 2.000 @   13+
   13                                  putter.0                             claim 80 from r
   13                                  putter.0 request honor r             scheduled for 2.000 @    6+
   13+      2.000 getter.0             current
   13+                                 getter.0 ended
    6+      2.000 putter.0             current
    7                                  putter.0 hold +1.000                 scheduled for 3.000 @    7+
    7+      3.000 putter.0             current
    8                                  putter.0                             put (request) 80 to r priority=inf
    8                                  putter.0                             claim 0 from r
    8                                  putter.0 request honor r             scheduled for 3.000 @    8+
    8+      3.000 putter.0             current
    8+                                 putter.0 ended
   20+                                 run ended                            no events left
   20+      3.000 main                 current

I don't know why you think this is incorrect.

If you want to chat about this, please contact me via e-mail on info@salabim.org .

@salabim salabim closed this as completed May 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants