The simplest MCDP can be defined as:
mcdp {
}
That is an empty MCDP - it has no functionality or resources.
The interface of an MCDP is defined using
the keywords provides
and requires
:
mcdp {
provides capacity [J]
requires mass [g]
# ...
}
The code above defines an MCDP with one functionality, capacity
, measured in joules,
and one resource, mass
, measured in grams. (See how to describe types.)
Graphically, this is how the interface is represented:
The following is a minimal example of a complete MCDP.
We have given hard bounds to both capacity
and mass
.
|
Functionality and resources can depend on each other using any monotone relations.
For example, we can describe a linear relation between mass and capacity, given by the specific energy.
mcdp {
provides capacity [J]
requires mass [g]
specific_energy = 4 J / g
required mass >= provided capacity specific_energy
}
PyMCDP is picky about units, but generally very helpful. As long as the units have the right dimensionality, it will insert the appropriate conversions.
For example, this is the same example with the specific energy given in kWh/kg.
mcdp {
provides capacity [J]
requires mass [g]
specific_energy = 200 kWh / kg
required mass >= provided capacity specific_energy
}
Suppose we define a simple model called Battery
as follows:
Battery.mcdp
|
Let’s also define the MCDP Actuation1
:
Actuation1.mcdp
|
Then we can combine these two together.
We can re-use previously defined MCDPs using the
keyword new
. This creates two sub-design problems, for now unconnected.
|
To create a complete MCDP, take “endurance” as a high-level functionality. Then the energy required is equal to endurance × power.
mcdp {
actuation = new Actuation1
battery = new Battery
# battery must provide power for actuation
provides endurance [s]
energy = provided endurance * (power required by actuation)
capacity provided by battery >= energy
}
We can create a model with a loop by introducing another constraint.
Take extra_payload
to represent the user payload that we must carry.
Then the lift provided by the actuator must be at least the mass of the battery plus the mass of the payload times gravity:
Composition.mcdp
|
We can also enumerate an arbitrary relation, as follows:
catalogue {
provides capacity [J]
requires mass [g]
model1 | 5 MJ | 100 g
model2 | 6 MJ | 200 g
model3 | 10 MJ | 400 g
}
The coproduct construct allows to describe the idea of “alternatives”. The name comes from the category-theoretical concept of coproduct.
As an example, let us consider how to model the choice between different battery technologies.
Let us consider the model of a battery in which we take the functionality to be the capacity and the resources to be the mass [g] and the cost [$].
Consider two different battery technologies, characterized by their specific energy (Joules per gram) and specific cost (USD per gram).
Specifically, consider Nickel-Hidrogen batteries and Lithium-Polymer batteries. On technology is cheaper but leads to heavier batteries and viceversa. Because of this fact, there might be designs in which we prefer either.
First we model the two battery technologies separately as two MCDP using the same interface (same resources and same functionality).
Battery_LiPo.mcdp
|
Battery1_NiH2.mcdp
|
Then we can define the coproduct of the two using
the keyword choose
.
Graphically, the choice is indicated through dashed lines.
Batteries.mcdp
|
The keyword Uncertain
is used to define uncertain relations.
For example, suppose there is some uncertain in the value of the specific energy, varying between 100 Wh/kg and 120 Wh/kg. This is one way to describe such uncertainty:
mcdp {
provides capacity [Wh]
requires mass [kg]
required mass >=
Uncertain(provided capacity120 Wh/kg ,
provided capacity100 Wh/kg )
}
The resulting MCDP has an uncertainty gate, marked with “?”, which joins two branches, the optimistic and the pessimistic branch.
All values belong to posets.
PyMCDP knows a few built-in posets, and gives you the possibility of creating your own.
The natural numbers with a completion are expressed as Nat
and their values using the syntax Nat:42
.
Floating point with completion are indicated by R
,
and their values as 42 []
.
Floating point with completion and units are indicated using units, such as:
g
,
J
,
m
,
s
,
m/s
,
…
Their values are indicated as follows:
10 g
,
20 J
,
10 m
,
10 s
,
23 m/s
,
…
It is possible to define and use your own arbitrary finite posets.
For example, create a file named my_poset.mcdp_poset
containing the following definition:
my_poset.mcdp_posetfinite_poset {
a <= b <= c
c <= d
c <= e
}
This defines a poset with 5 elements a
, b
, c
, d
, e
and with the given order relations.
Now that this poset has been defined, it can be used in the
definition of an MCDP, by referring to it by name using
the backtick notation, as in “`my_poset
”.
To refer to its elements, use the notation `my_poset: element
.
For example:
|
Use the Unicode symbol “×
” or the simple letter x
to create a poset product.
The syntax is
space × space × … × space
For example:
J × A
This represents a product of Joules and Amperes.
To create a tuple, use angular brackets.
The syntax is:
< element, element, … > ⟨ element, element, … ⟩
An example using regular brackets:
<0 J, 1 A>
An example using fancy unicode brackets:
⟨0 J, 1 A⟩
To access the elements of the tuple, use the syntax
take(value, index)
For example:
mcdp {
provides out [ J x A ]
take(out, 0) <= 10 J
take(out, 1) <= 2 A
}
This is equivalent to
mcdp {
provides out [ J x A ]
out <= <10 J, 2 A>
}
PyMCDP also supports named products, in which each entry in the tuple
is associated to a name. For example, the following declares
a product of the two spaces J
and A
with the two entries
named energy
and current
.
product(energy:J, current:A)
Then it is possible to index those entries using one of these two syntaxes:
take(value, label) (value).label
For example:
mcdp {
provides out [ product(energy:J, current:A) ]
(out).energy <= 10 J
(out).current <= 2 A
}
It is easy to create recursive composition in MCDP.
Composition1.mcdpmcdp {
a = instance mcdp {
c = instance mcdp {
provides f [Nat]
requires r [Nat]
provided f + Nat:1 <= required r
}
provides f using c
requires r for c
}
b = instance mcdp {
d = instance mcdp {
provides f [Nat]
requires r [Nat]
provided f + Nat:1 <= required r
}
provides f using d
requires r for d
}
r required by a <= f provided by b
requires r for b
provides f using a
}
We can completely abstract an MCDP, using the abstract
keyword.
abstract `Composition1
And we can also completely flatten it, by erasing the border between subproblems:
flatten `Composition1
mcdp {
provides a [R]
requires b [R]
requires c [R]
a <= b * c
}
$$n=1$$ | $$n=3$$ | $$n=5$$ | $$n=10$$ | $$n=25$$ |
mcdp {
provides a [R]
requires b [R]
requires c [R]
a <= b + c
}
$$n=1$$ | $$n=3$$ | $$n=5$$ | $$n=10$$ | $$n=25$$ |
A battery is specified as a DP with the functionalities:
and resources
The parameters for the model are the specific energy, the specific cost, and the number of cycles:
specific_energy = 150 Wh/kg
specific_cost = 2.50 Wh/$
cycles = 600 []
The cost for one battery is given by
unit_cost = provided capacity specific_cost
The number of replacements is:
num_replacements = ceil(provided missions cycles)
The total budget for the solution is given by the unit cost times the number of replacements:
required cost >= unit_cost * num_replacements
The code below is the complete model for the battery:
Battery_LiPo.mcdpmcdp {
provides capacity [J]
provides missions [R]
requires mass [g]
requires cost [$]
# Number of replacements
requires maintenance [R]
# Battery properties
specific_energy = 150 Wh/kg
specific_cost = 2.50 Wh/$
cycles = 600 []
# How many times should it be replaced?
num_replacements = ceil(provided missions cycles)
required maintenance >= num_replacements
required mass >= provided capacity specific_energy
# cost for one battery
unit_cost = provided capacity specific_cost
required cost >= unit_cost * num_replacements
}
This is a graphical representation of the network of constraints:
The actuation is defined as a DP where the functionalities are:
and the resources are:
The model first describes some hard constraints for the quantities:
provided lift <= 100N
required actuator_mass >= 100 g
required cost >= 100 $
provided velocity <= 3 m/s
Then it describes a nonlinear polynomial (and monotone) relation between lift and power:
p0 = 2 W
p1 = 1.5 W/N^2
required power >= p0 + (lift^2) * p1
This is the complete MCDP:
Actuation.mcdpmcdp {
provides lift [N]
provides velocity [m/s]
requires power [W]
requires actuator_mass [g]
requires cost [$]
provided lift <= 100N
required actuator_mass >= 100 g
required cost >= 100 $
provided velocity <= 3 m/s
p0 = 2 W
p1 = 1.5 W/N^2
required power >= p0 + (lift^2) * p1
}
This is the graphical representation:
Next, we will define an MCDP that contains both actuation and energetics as sub-MCDPs.
We will take as high-level functionality:
In the model below, first we instantiate the models of battery and actuation:
battery = new Battery_LCO
actuation = new Actuation
The power constraint can be written as follows:
total_power = power required by actuation + extra_power
capacity provided by battery >= endurance * total_power
The lift constraint is the following:
total_mass = (
mass required by battery +
actuator_mass required by actuation
+ extra_payload)
gravity = 9.81 m/s^2
weight = total_mass * gravity
lift provided by actuation >= weight
The cost constraint is the following:
labor_cost = (10 $) * (maintenance required by battery)
total_cost >= (
cost required by actuation +
cost required by battery +
labor_cost)
ActuationEnergetics.mcdpmcdp {
provides endurance [s]
provides extra_payload [kg]
provides extra_power [W]
provides num_missions [R]
provides velocity [m/s]
requires total_cost [$]
battery = new Battery_LCO
actuation = new Actuation
total_power = power required by actuation + extra_power
capacity provided by battery >= endurance * total_power
total_mass = (
mass required by battery +
actuator_mass required by actuation
+ extra_payload)
gravity = 9.81 m/s^2
weight = total_mass * gravity
lift provided by actuation >= weight
velocity provided by actuation >= velocity
labor_cost = (10 $) * (maintenance required by battery)
total_cost >= (
cost required by actuation +
cost required by battery +
labor_cost)
battery.missions >= num_missions
requires total_mass >= total_mass
}
These are other parts that we need.
Computer.mcdp
|
Sensor.mcdp
|
Perception.mcdp
|
Strategy.mcdp
|
Based on the previous models, we can assemble the model for the entire drone, with high-level functionality: travel_distance carry payload * num_missions
DroneComplete.mcdpmcdp {
provides travel_distance [km]
provides num_missions [R]
provides carry_payload [g]
requires total_cost_ownership [$]
strategy = new Strategy
actuation_energetics = new ActuationEnergetics
endurance prov. by actuation_energetics>= endurance req. by strategy
velocity prov. by actuation_energetics>= velocity req. by strategy
num_missions prov. by actuation_energetics >= provided num_missions
extra_payload prov. by actuation_energetics>= provided carry_payload
distance prov. by strategy >= provided travel_distance
computer = new Computer
perception = new Perception
computation prov. by computer>= computation req. by perception
sensor = new Sensor
resolution prov. by sensor >= camera_resolution req. by perception
sensor.framerate >= perception.camera_framerate
sensor.fov >= perception.camera_fov
perception.velocity >= strategy.velocity
actuation_energetics.extra_power >= computer.power + sensor.power
# We can take into account the shipping cost
shipping = new Shipping
total_mass = actuation_energetics.total_mass
shipping.ships >= total_mass
total_cost_ownership >= shipping.postage + actuation_energetics.total_cost
}
Finally, we can define a model of the customer:
and put it in the loop:
CustomerPlusEngineering.mcdpmcdp {
customer = new Customer
robot = new DroneComplete
budget provided by customer >= total_cost_ownership required by robot
travel_distance provided by robot >= travel_distance required by customer
num_missions provided by robot >= num_missions required by customer
carry_payload provided by robot >= carry_payload required by customer
}
We are going to model the domain of AC sockets and plugs.
There are many AC power plugs and sockets.
Type A |
Type B |
Type C |
Type D |
Type E |
Type F |
Type G |
Type H |
Type I |
Type J |
Type K |
Type L |
Type M |
Type N |
Type O |
Some of them are compatible. For example we can fit a plug ot Type A into a socket of Type B. This creates a natural partial order structure.
We can use a finite_poset
to describe the poset as follows:
socket_type.mcdp_posetfinite_poset {
TypeA TypeB TypeC TypeD TypeE TypeF TypeG TypeH
TypeI2 TypeI3 TypeJ TypeK TypeL TypeM TypeN TypeO
# A <= B: if it fits in A, it also fits in B
TypeA <= TypeB
TypeC <= TypeD
TypeE <= TypeD
TypeF <= TypeD
TypeC <= TypeE
TypeF <= TypeE
TypeC <= TypeH
TypeI2 <= TypeI3
TypeC <= TypeJ
TypeK <= TypeC
TypeC <= TypeL
TypeC <= TypeN
TypeC <= TypeO
}
Around the world, the two main voltages are 110V
and 220V
.
In this case, we cannot use the usual Volt poset indicated
by V
, because that would mean
that 220V
is always
preferable to 110V
.
Thus we create a discrete poset as follows:
AC_voltages.mcdp_posetfinite_poset {
v110 v220
}
Similarly, we model different frequencies with the poset
AC_frequencies.mcdp_posetfinite_poset {
f50 f60
}
The function of a socket in the wall is to provide power. This function is parameterized (at least) by:
The socket shape, indicated by `socket_type
The voltage, indicated by `AC_voltages
The frequency, indicated by `AC_frequencies
The maximum power draw, measured in Watts. (Alternatively, this could be parameterized by current.)
Therefore, we can create the poset `AC_power
as follows:
AC_power.mcdp_posetproduct(
task: S(AC_power),
socket: `socket_type,
voltage: `AC_voltages,
frequency: `AC_frequencies,
watts: W
)
Based on these definitions, we can define the function of a socket adapter.
Consider one of these OREI adapters, which you can buy for $7.31:
This is an adapter from Type L to either Type C or Type A:
Type L |
Type A |
Type C |
A plug adapter can be modeled as follows:
orei.mcdp# This device converts from TypeC to either TypeL or TypeA
mcdp {
provides out [`AC_power]
requires in [`AC_power]
requires budget [USD]
budget >= 7.31 USD
(required in).socket >= `socket_type : TypeL
(required in).voltage >= (provided out).voltage
(required in).frequency >= (provided out).frequency
(required in).watts >= (provided out).watts
(provided out).socket <= any-of({`socket_type : TypeA, `socket_type : TypeC})
}
This is another handy 2-in-1 adapter that sells for 6.31:
This one provides 2 outputs:
orei_2in1.mcdpmcdp {
provides out1 [`AC_power]
provides out2 [`AC_power]
requires in [`AC_power]
requires budget [USD]
budget >= 6.31 USD
(required in).socket >= `socket_type : TypeM
(provided out1).socket <= any-of({`socket_type : TypeA, `socket_type : TypeC})
(provided out2).socket <= any-of({`socket_type : TypeA, `socket_type : TypeC})
# this forces the two voltages to be the same
(required in).voltage >= (provided out1).voltage
(required in).voltage >= (provided out2).voltage
(required in).frequency >= (provided out1).frequency
(required in).frequency >= (provided out2).frequency
# this says that the power sums
total_power = (provided out1).watts + (provided out2).watts
(required in).watts >= total_power
}
We can forget all this complexity and consider the block:
We can repeat the same story with DC connectors.
barrel_connectors.mcdp_posetfinite_poset {
tip_sleeve_2_5mm
tip_sleeve_3_5mm
barrel_5mm
# EIAJ-01 (2.35mm barrel, 0.7mm inner diameter)
barrel_2_35mm
barrel_3_35mm_1_35mm
# EIAJ-02 (4.0mm barrel, 1.7mm inner diameter)
barrel_4mm_1_7mm
barrel_4mm_1_5mm
barrel_4mm_2_5mm
}
USB_connectors.mcdp_posetfinite_poset {
USB_Micro_A
USB_Micro_B
USB_Mini_A
USB_Mini_B
USB_Std_A
USB_Std_B
USB_Type_C
}
We can define DC connectors to be the union (co-product) of the two sets:
DC_connectors.mcdp_posetcoproduct(`barrel_connectors, `USB_connectors)
DC_voltages.mcdp_posetfinite_poset {
v1_5 v5 v6_6
}
DC_power.mcdp_posetproduct(
task: S(DC_power),
connector: `DC_connectors,
voltage: `DC_voltages,
amps: A
)
This wall charger can be used to convert from AC power to DC power.
Ravpower.mcdpmcdp {
provides out1 [`DC_power]
provides out2 [`DC_power]
requires in [`AC_power]
requires budget [USD]
budget >= 10.99 USD
(required in).socket >= `socket_type : TypeA
(provided out1).voltage <= `DC_Voltages: v5
(provided out2).voltage <= `DC_Voltages: v5
(provided out1).connector <= `USB_connectors:USB_Std_A
(provided out2).connector <= `USB_connectors:USB_Std_A
# this forces the two voltages to be the same
# this says that the power sums
amps = (provided out1).amps + (provided out2).amps
amps <= 2.4 A
power = 5 V * (amps)
(required in).watts >= power
}
We can query the model as follows. Suppose we need 2 outputs, each of 0.5A.
solve(
⟨ ⟨S(DC_power):*, `USB_connectors:USB_Std_A, `DC_voltages: v5, 0.5 A⟩,
⟨S(DC_power):*, `USB_connectors:USB_Std_A, `DC_voltages: v5, 0.5 A⟩ ⟩,
`Ravpower)
This is the output:
↑{⟨in:⟨'AC_power', 'TypeA', 'v110', 'f50', 5 W⟩, budget:10.99 USD⟩, ⟨in:⟨'AC_power', 'TypeA', 'v110', 'f60', 5 W⟩, budget:10.99 USD⟩, ⟨in:⟨'AC_power', 'TypeA', 'v220', 'f50', 5 W⟩, budget:10.99 USD⟩, ⟨in:⟨'AC_power', 'TypeA', 'v220', 'f60', 5 W⟩, budget:10.99 USD⟩}
The model says we have two options: we need to find an outlet of TypeM
at either 110 V or 220 V which will provide 5 W of power. Moreover, we need
at least 10.99 USD to buy the component.
This is an example of composition of the Ravpower charger and the Orei_2in1 adapter.
orei_plus_ravpower.mcdpmcdp {
provides out [`DC_power]
requires in [`AC_power]
requires budget [USD]
charger = instance `Ravpower
adapter = instance `Orei_2in1
in required by charger <= out1 provided by adapter
required in >= in required by adapter
provided out <= out1 provided by charger
# sum the budget together
budget >= budget required by charger + budget required by adapter
# ignore the functions we don't need
ignore out2 provided by charger
ignore out2 provided by adapter
}
Note the use of the keyword “ignore
” to ignore the
functionality that we do not need.
We can ask now for what resources we would need for a 0.5 A load:
solve(
⟨S(DC_power):*, `USB_connectors:USB_Std_A, `DC_voltages: v5, 0.5 A⟩,
`orei_plus_ravpower)
and obtain
↑{⟨in:⟨'AC_power', 'TypeM', 'v110', 'f50', 2.5 W⟩, budget:17.3 USD⟩, ⟨in:⟨'AC_power', 'TypeM', 'v110', 'f60', 2.5 W⟩, budget:17.3 USD⟩, ⟨in:⟨'AC_power', 'TypeM', 'v220', 'f50', 2.5 W⟩, budget:17.3 USD⟩, ⟨in:⟨'AC_power', 'TypeM', 'v220', 'f60', 2.5 W⟩, budget:17.3 USD⟩}
Next, we are going to model a Goldsource STU-200 Step Up/Down Voltage Transformer Converter.
This is a device with 1 input and 3 outputs:
goldsource_STU_200.mcdpmcdp {
# This provides 3 outputs: 2 AC and 1 DC
provides out1 [`AC_power]
provides out2 [`AC_power]
provides out3 [`DC_power]
requires in [`AC_power]
# the AC output 1 is v110 and takes types A, B (A<=B)
(provided out1).socket <= `socket_type: TypeB
(provided out1).voltage <= `AC_voltages:v110
(provided out1).frequency <= (required in).frequency # same frequency
power1 = (provided out1).watts
# the AC output 2 is v220 and takes types C, D, E, F, G, H (D,G minimals)
(provided out2).socket <= any-of({ `socket_type: TypeD, `socket_type: TypeG })
(provided out2).voltage <= `AC_voltages:v220
(provided out2).frequency <= (required in).frequency # same frequency
power2 = (provided out2).watts
# the DC output is 5v, USB Type A
(provided out3).connector <= `USB_connectors: USB_Std_A
(provided out3).voltage <= `DC_voltages: v5
amp3 = (provided out3).amps
power3 = 5 V * (amp3)
power = power1 + power2 + power3
(required in).watts >= power
# input is either typeC or typeD (C<=D)
(required in).socket >= `socket_type: TypeC
}
A thermocouple is a device that converts heat into electrical power.
Thermocouple.mcdp
|
PlutoniumPellet.mcdp
|
mcdp {
plutonium_pellet = new PlutoniumPellet
thermocouple = new Thermocouple
heat provided by plutonium_pellet >= heat required by thermocouple
requires plutonium for plutonium_pellet
provides power_profile <= thermocouple.power
requires cost >= thermocouple.cost
requires mass >= thermocouple.mass + plutonium_pellet.mass
}
Putting everything together
load <name>
`<name>
℘(V)
set-of(V)
The syntax is
UpperSets(poset)
For example:
UpperSets(V)
The syntax is
Interval(lower bound,upper bound)
For example:
Interval(1g, 10g)
S(tag)
S(tag):*
The syntax is:
⊤ <space>
Top <space>
⊥ <space>
Bottom <space>
For example:
Top V
⊤ V
Bottom V
⊥ V
The syntax is:
{<element>, <element>, ..., <element> }
For example:
{0 g, 1 g}
The syntax is:
upperclosure <set>
For example:
upperclosure {0 g, 1 g}
Suppose f has type F. Then:
ignore f provided by x
is equivalent to
f provided by x >= any-of(Minimals F)
Equivalently,
ignore r required by x
is equivalent to
r required by x <= any-of(Maximals R)
ceil
sqrt
square
pow
max
min
provides f using c
requires r for c
approx()
abstract <ndp>
compact <ndp>
template <ndp>
flatten <ndp>
canonical <ndp>
approx_lower(<n>, <ndp>)
approx_upper(<n>, <ndp>)
solve(value, <mcdp>)