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. It builds on Custom Resource Types, which shows how to subclass ResourceBase, and on Priority and Preemption, which serves the lowest priority value first, so a class of low priority waits as long as a higher one has work. A weight gives each class its share.
Choose the Weights
Each class has a weight. Here tier 1 weighs 3 and tier 2 weighs 1:
# Tier 1 is served three times for every once of tier 2WEIGHTS={1:3,2:1}
Order the Waiting Line
A resource keeps its waiting requests in its PutQueue, which ResourceBase creates with no arguments and which a request joins with append. Write a queue that keeps them sorted by finish tag. 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, so a class of weight 3 gets tags a third as far apart as a class of weight 1, and is served three times as often. 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:
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 equal tags are equal. The tags assume requests of the same size, as one packing time is here.
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 the line itself and ignores the order that setting names.
resources:# queue: priority lets a task set a priority; the class orders the waiting line 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 line 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 Order
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. All three forms grant the order worked out here.
The queue leaves out three things. A request that leaves the line 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.
Example
The Python file with the queue and the resource, the YAML blueprint, and the script that runs the YAML, builder and low-level forms and prints the tier of each grant in order. The orders file is the CSV in the YAML tab of Use It. Put the four files in one folder and run python share_by_weight.py from it.
"""Python for share_by_weight.yaml: a packer that shares its time between classes byweight, as the Share by Weight guide builds it."""importbisectimportitertoolsfromfractionsimportFractionfromdesflowimportResourceBase# Tier 1 is served three times for every once of tier 2WEIGHTS={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 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 line 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 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, in YAML, in thebuilder and in the low-level API. Each function returns the tier of each order inthe order the packer granted them."""importloggingimportsysfrompathlibimportPathfromdesflowimport(CapacityConfig,DistributionConfig,DynamicRealtimeEnvironment,SimParameter,SimulationContext,)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]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 line 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").granteddeflow_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__":forformin(yaml_form,builder_form,low_level_form):print(f"{form.__name__:15}{form()}")