Share one resource between classes of work by weight, so that a class of weight 3 is served three times for each time a class of weight 1 is served while both wait. A priority queue serves the lowest priority value first, so a class with a higher value waits for as long as a class with a lower value has work. A weight gives each class its share instead.
# Tier 1 is served three times for each time tier 2 is servedWEIGHTS={1:3,2:1}
Order the Waiting Line
A resource keeps its waiting requests in its PutQueue. ResourceBase creates the queue with no arguments, and a request joins it with append. Write a queue that gives each request a finish tag, a number that sets its turn, and keeps the requests sorted by it. The queue follows three rules:
A request's finish tag is the later of the queue's clock and its class's last tag, plus 1 divided by the class's weight.
The queue serves the smallest tag first. Requests with equal tags leave in the order they arrived, which the counter arrived keeps.
When a request is granted, the queue takes its tag as the clock.
A class of weight 3 therefore gets tags a third as far apart as a class of weight 1, so it is served three times as often:
classFinishTagQueue(list):"""Waiting requests in the order of their finish tags, smallest first. A request's tag is where its class would finish it if every class had its share: the later of the queue's clock and the class's last tag, plus 1 divided by the class's weight. Equal tags leave in the order the requests arrived. """def__init__(self):super().__init__()self.clock=Fraction(0)# the tag of the request granted lastself.last={}# the last tag given to each classself.arrived=itertools.count()defappend(self,request):tier=request.priority# set by `priority: attr.tier`, not by _rankstart=max(self.clock,self.last.get(tier,Fraction(0)))tag=start+Fraction(1,WEIGHTS[tier])self.last[tier]=tagrequest.tag=(tag,next(self.arrived))bisect.insort(self,request,key=lambdawaiting:waiting.tag)defpop(self,index=-1):request=super().pop(index)self.clock=request.tag[0]# a grant moves the clock to its tagreturnrequest
The class of a request is its priority, which the task sets. The queue reads it there, not from the rank ResourceBase computes for the request, which this queue does not use. Fractions keep the tags exact, so two tags that should be equal compare as equal. The tags assume that every request holds the packer for the same time, as here, where every order takes 1 second to pack.
Make the Resource
Subclass ResourceBase and set PutQueue. The resource also keeps the tier of each grant, so a test can read the order:
classWeightedPackers(ResourceBase):"""A resource that grants its units by weight and keeps the tier of each grant."""PutQueue=FinishTagQueuedef__init__(self,*args,**kwargs):super().__init__(*args,**kwargs)self.granted=[]def_do_put(self,request):super()._do_put(request)ifrequest.triggered:# the request was granted just nowself.granted.append(request.priority)
Use It
A task sets the class with its priority, and the loader refuses a task priority on a resource that does not set queue: priority. So set queue: priority beside type. This queue orders its waiting requests itself and ignores the order that setting names.
resources:# queue: priority lets a task set a priority; the class orders the waiting requests itselfpackers:current_cap:1max_cap:4queue:prioritytype:!pythonshare_by_weight_logic.WeightedPackers
The class is in share_by_weight_logic.py, beside the blueprint. The task takes the tier from the order's attributes:
app=(SimulationContext(sim_id="Shop",factor=0,random_seed=42).add_service("packing",dist="constant",value=1.0).add_arrival_replay("orders",[{"time":0,"tier":tier}fortierinTIERS],fields=["tier"]))# queue="priority" lets a task set a priority; the class orders the waiting requests itselfapp.add_resource("packers",current_cap=1,max_cap=4,queue="priority",type=WeightedPackers)# The tier of the order is the class of its request@app.task(service_id="packing",resource_id="packers",priority="attr.tier")defpack_order(task_id:int):return{"event_type":"order_packed"}@app.arrival_loop("orders")deforder_generator(context:SimulationContext):task_id=0whileTrue:order=yieldcontext.wait_for_arrival("orders")context.spawn(pack_order(task_id,entity=order))task_id+=1
env=DynamicRealtimeEnvironment(factor=0.0)env.registry.register_sim_parameter(SimParameter(sim_id="Shop",service={"packing":DistributionConfig(dist="constant",value=1.0)},resources={"packers":CapacityConfig(current_cap=1,max_cap=4)},))packers=WeightedPackers(env,"Shop","packers",queue="priority")deforder(env,tier):# The tier is the priority of the requestwithpackers.request(priority=tier)asrequest:yieldrequestyieldenv.timeout(1.0)fortierinTIERS:env.process(order(env,tier))
The class is the one the YAML tab shows.
Check the Result
Look at the tags the queue gives the orders, on a line from 1 to 3:
The first order is tier 2. It takes the packer at once, with tag 1, and the clock becomes 1. The six orders of tier 1 then get tags 4/3, 5/3, 2, 7/3, 8/3 and 3, and the two of tier 2 get 2 and 3. The smallest tag goes first, and where two tags are equal, tier 1 arrived first. So after the first order the packer grants 1, 1, 1, 2, 1, 1, 1, 2: three of tier 1 for each of tier 2. First in, first out would grant 1, 1, 1, 1, 1, 1, 2, 2. The resource keeps the tier of each grant in granted. python share_by_weight.py prints it for the YAML and builder forms, and python share_by_weight_low_level.py for the low-level form. All three grant the order worked out here.
The queue leaves out three things. A request that leaves the queue before it is granted keeps the tag it took, so its class does not get that share back. A class not in WEIGHTS raises KeyError when its first request joins. Do not set preempt: preemption ranks users by priority, here the tier, so a tier 1 order would take the packer from tier 2 whatever the weights.
Full Files
Every form uses the class in share_by_weight_logic.py unchanged. Put it, the blueprint and the orders file in one folder with the scripts, and run them from there.
"""Python for share_by_weight.yaml: a packer that shares its time between classes byweight, as the Share Work by Weight guide builds it."""importbisectimportitertoolsfromfractionsimportFractionfromdesflowimportResourceBase# Tier 1 is served three times for each time tier 2 is servedWEIGHTS={1:3,2:1}classFinishTagQueue(list):"""Waiting requests in the order of their finish tags, smallest first. A request's tag is where its class would finish it if every class had its share: the later of the queue's clock and the class's last tag, plus 1 divided by the class's weight. Equal tags leave in the order the requests arrived. """def__init__(self):super().__init__()self.clock=Fraction(0)# the tag of the request granted lastself.last={}# the last tag given to each classself.arrived=itertools.count()defappend(self,request):tier=request.priority# set by `priority: attr.tier`, not by _rankstart=max(self.clock,self.last.get(tier,Fraction(0)))tag=start+Fraction(1,WEIGHTS[tier])self.last[tier]=tagrequest.tag=(tag,next(self.arrived))bisect.insort(self,request,key=lambdawaiting:waiting.tag)defpop(self,index=-1):request=super().pop(index)self.clock=request.tag[0]# a grant moves the clock to its tagreturnrequestclassWeightedPackers(ResourceBase):"""A resource that grants its units by weight and keeps the tier of each grant."""PutQueue=FinishTagQueuedef__init__(self,*args,**kwargs):super().__init__(*args,**kwargs)self.granted=[]def_do_put(self,request):super()._do_put(request)ifrequest.triggered:# the request was granted just nowself.granted.append(request.priority)
# The Share Work by Weight guide: one packer shared between two tiers of orders by weight.# Run it with: desflow run share_by_weight.yamlsimulation:sim_id:Shopfactor:0random_seed:42resources:# queue: priority lets a task set a priority; the class orders the waiting requests itselfpackers:current_cap:1max_cap:4queue:prioritytype:!pythonshare_by_weight_logic.WeightedPackersservices:packing:{dist:constant, value:1 s}# Nine orders at time 0, each with a tierarrivals:orders:replay:{path:share_by_weight_orders.csv, time:time, unit:s}spawn:pack_ordertasks:pack_order:service:packingresource:packerspriority:attr.tier# the tier of the order is the class of its requestpayload:{event_type:order_packed}egress:-type:Consolerun:until:20 s
"""Share Work by Weight: one packer shared between two tiers of orders, 3 to 1.Nine orders arrive at time 0: the first is tier 2, then six of tier 1, then two moreof tier 2. The first takes the packer and the rest wait. The packer grants thewaiting orders by weight, three of tier 1 for each of tier 2. `builder_form` runsthe shop with the builder and `yaml_form` runs share_by_weight.yaml, which loads asa context; share_by_weight_low_level.py is the low-level API. Each returns the tierof each order in the order the packer granted them."""importloggingimportsysfrompathlibimportPathfromdesflowimportSimulationContextHERE=Path(__file__).resolve().parentsys.path.insert(0,str(HERE))# share_by_weight_logic.py is beside the blueprintfromshare_by_weight_logicimportWeightedPackers# noqa: E402logging.basicConfig(level=logging.WARNING,format="%(levelname)s [%(asctime)s] %(message)s")# The tier of each order, in the order the orders arriveTIERS=[2,1,1,1,1,1,1,2,2]defyaml_form():"""The blueprint beside the class."""app=SimulationContext.from_yaml(HERE/"share_by_weight.yaml")app.run()returnapp.get_resource("packers").granteddefbuilder_form():"""The same shop with the builder."""app=(SimulationContext(sim_id="Shop",factor=0,random_seed=42).add_service("packing",dist="constant",value=1.0).add_arrival_replay("orders",[{"time":0,"tier":tier}fortierinTIERS],fields=["tier"]))# queue="priority" lets a task set a priority; the class orders the waiting requests itselfapp.add_resource("packers",current_cap=1,max_cap=4,queue="priority",type=WeightedPackers)# The tier of the order is the class of its request@app.task(service_id="packing",resource_id="packers",priority="attr.tier")defpack_order(task_id:int):return{"event_type":"order_packed"}@app.arrival_loop("orders")deforder_generator(context:SimulationContext):task_id=0whileTrue:order=yieldcontext.wait_for_arrival("orders")context.spawn(pack_order(task_id,entity=order))task_id+=1app.run(until=20)returnapp.get_resource("packers").grantedif__name__=="__main__":forformin(yaml_form,builder_form):print(f"{form.__name__:15}{form()}")
"""Share Work by Weight, low-level API: one packer shared between two tiers oforders, 3 to 1.The low-level version of share_by_weight.py: nine orders at time 0, the first oftier 2, then six of tier 1, then two more of tier 2. `low_level_form` returns thetier of each order in the order the packer granted them."""importloggingimportsysfrompathlibimportPathfromdesflowimport(CapacityConfig,DistributionConfig,DynamicRealtimeEnvironment,SimParameter,)HERE=Path(__file__).resolve().parentsys.path.insert(0,str(HERE))# share_by_weight_logic.py is beside the blueprintfromshare_by_weight_logicimportWeightedPackers# noqa: E402logging.basicConfig(level=logging.WARNING,format="%(levelname)s [%(asctime)s] %(message)s")# The tier of each order, in the order the orders arriveTIERS=[2,1,1,1,1,1,1,2,2]deflow_level_form():"""The same shop with the environment and the resource."""env=DynamicRealtimeEnvironment(factor=0.0)env.registry.register_sim_parameter(SimParameter(sim_id="Shop",service={"packing":DistributionConfig(dist="constant",value=1.0)},resources={"packers":CapacityConfig(current_cap=1,max_cap=4)},))packers=WeightedPackers(env,"Shop","packers",queue="priority")deforder(env,tier):# The tier is the priority of the requestwithpackers.request(priority=tier)asrequest:yieldrequestyieldenv.timeout(1.0)fortierinTIERS:env.process(order(env,tier))env.run(until=20)returnpackers.grantedif__name__=="__main__":print(f"{'low_level_form':15}{low_level_form()}")