-
Notifications
You must be signed in to change notification settings - Fork 1
Coupled
Coupled models describes a system as a network of components coupled together. These components are in the modular form, which means their interactions are restricted to happen through identified ports with a preestablished connection.
Coupled models are themselves modular models, which means they also have input and output ports. Thus, it is possible to build a hierarchy of models shaped as a tree, which helps modellers managing complexity.
Coupled models are defined under the Quartz::CoupledModel class.
They can either be instantiated directly or inherited.
In both cases, the API to add sub-models and coupling them is identical.
We will re-use our running example from the atomic model page to connect models.
Adding sub-models is done using the #add_child method or the #<< alias method.
In the example below we build a model of a store, with 2 cashiers, 2 client queues and a counter collector:
class SmallStoreModel < Quartz::CoupledModel
def after_initialize
cashier1 = Cashier.new("cashier1")
cashier2 = Cashier.new("cashier2")
queue1 = Clients.new("queue1")
queue2 = Clients.new("queue2")
counter = Counter.new("counter")
self << cashier1 << cashier2 << queue1 << queue2 << counter
end
endNow we need to couple output ports with input ports between sub-models, using the #attach method, which expects an output port and a target input port:
attach(sender.output_port("out"), receiver.input_port("in"))or using port names instead of a reference to the ports:
attach "output_port_name", to: "input_port_name", between: sender, and: receiver
# or also
attach "output_port_name", to: "input_port_name", between: "sender_name", and: "receiver_name"Below is an example that connects ports of our store model:
class SmallStoreModel < Quartz::CoupledModel
def after_initialize
cashier1 = Cashier.new("cashier1")
cashier2 = Cashier.new("cashier2")
queue1 = Clients.new("queue1")
queue2 = Clients.new("queue2")
counter = Counter.new("counter")
self << cashier1 << cashier2 << queue1 << queue2 << counter
attach queue1.output_port(:articles), cashier1.input_port(:articles)
attach cashier1.output_port(:available), queue1.input_port(:free)
attach queue2.output_port(:articles), queue2.input_port(:articles)
attach cashier2.output_port(:available), queue2.input_port(:free)
attach cashier1.output_port(:articles), counter.input_port(:in)
attach cashier2.output_port(:articles), counter.input_port(:in)
end
endWhen two models are coupled together, we need to make sure they can understand each other in terms of transmitted values.
If the receiver expects a different type of values for example, it is possible to use transducers,
a function that maps the output value to another input value.
This can be done by passing a block to the #attach function.
Below is an example that use a transducer to map floats to integers:
m1 = SenderModel.new("sender")
m2 = ReceiverModel.new("receiver")
root = Quartz::CoupledModel.new("root")
root << m1 << m2
root.attach(m1.output_port("out"), m2.input_port("in")) do |outputs|
outputs.map { |v| Quartz::Any.new(v.as_f.round) }
endNote: the block is passed an argument of type Array(Quartz::Any) and expects the same return type.
This is because the underlying formalism upon which Quartz is based allows multiple values to be posted
on the same port.
Up to now, we defined a model hierarchy of depth one, with one root coupled model having atomic models as children. If we want to have a deeper hierarchy, we will have to define other type of couplings, used to map ports of different levels.
External input couplings maps two input ports from a coupled model to one of its child model.
They can be constructed using the #attach method:
coupled_model.attach(
coupled_model.input_port("inport"),
submodel.input_port("inport")
)or using the #attach_input by using names instead of references:
coupled_model.attach_input("inport", to: "inport", of: "child")
# or
coupled_model.attach_input("inport", to: "inport", of: child)Note: transducers blocks can also be used.
Here is a more complete example mapping the output of a model 'A' to the input of a group of collectors:
root = Quartz::CoupledModel.new("root")
a = ModelA.new("generator")
cm = Quartz::CoupledModel.new("collectors").tap do |coupled|
col1 = Collector1.new("col1")
col2 = Collector1.new("col2")
coupled << col1 << col2
coupled.attach(coupled.input_port("in"), col1.input_port("in"))
coupled.attach(coupled.input_port("in"), col2.input_port("in"))
end
root << a << cm
root.attach(a.output_port("out"), cm.input_port("in"))External output couplings maps two output ports from a model to its parent coupled model.
They can be constructed using the #attach method:
coupled_model.attach(
submodel.output_port("out")
coupled_model.output_port("out"),
)or using the #attach_output by using names instead of references:
coupled_model.attach_output("out", of: "child", to: "out")
# or
coupled_model.attach_output("out", of: child, to: "out")Note: transducers blocks can also be used.
Congratulations! You mastered atomic and coupled models with Quartz.
From here, you can jump to Running simulations or keep reading on more specific type of models supported in this tool, the next being discrete-time models.