Add Constraint
Add a business rule that Network Optimization must respect: caps on lane
flows, how many sources serve a customer, how many warehouses are open, budget
limits, and similar. A rule says what it covers with a filter, such as every
lane from a site tagged Export, and can be held once for everything it covers
or once for each source, target, location or product.
When to use this¶
- You want the optimizer to honour a real-world rule: a lane can ship at most 500 units, each customer gets everything from one warehouse, at most three warehouses are open, total transport cost must not exceed €1M.
- You want one rule over a set that follows from your data: "at most this much leaves our export sites", "each plant ships at most this much by road".
- You want to test sensitivity by tightening or loosening a rule and re-running.
Place Add Constraint between Create Scenario (or Load Scenario) and Network Optimization. Chain several Add Constraint nodes for several rules.
What you need¶
- Scenario data wired into the first input, from Create Scenario, Load Scenario, Add Costs or another Add Constraint.
- Optionally: a table wired into the second input (Constraint Table), with one constraint per row instead of a single one typed into the panel. The table then defines every rule and the panel stays empty.
- For filters on tags: tags on your locations and products, given in Map Locations and Map Products.
Constraint types¶
Pick a type from the Constraint type dropdown in the side panel. The form below it shows the settings that type needs. Every type except Budget Limit has Applies to and For each, described in the next section.
| Type | What it does | Settings |
|---|---|---|
| Service Level | Requires a share of selected flow to qualify | Applies to, For each, Qualifying flow, Minimum percentage |
| Lane Capacity | Limits the flow on the lanes the rule covers | Applies to, For each, Maximum flow, Unit |
| Lane Minimum | Requires a minimum flow on the lanes the rule covers, always or only when they are used (MIP) | Applies to, For each, Minimum flow, Minimum applies, Unit |
| Fix Flow | Fixes the flow on the lanes the rule covers | Applies to, For each, Exact flow, Unit |
| Throughput Limit | Limits incoming flow plus local supply at the locations the rule covers | Applies to, For each, Minimum throughput, Maximum throughput (at least one), Unit |
| Budget Limit | Limits transport cost, or the network's total cost | Maximum transport cost, Maximum total cost (at least one) |
| Min Origins | Requires a number of origins shipping on the covered lanes (MIP) | Applies to, For each, Minimum number of origins, Smallest flow that counts, Unit |
| Max Origins | Limits how many origins ship on the covered lanes (MIP) | Applies to, For each, Maximum number of origins |
| Min Targets | Requires a number of targets reached by the covered lanes (MIP) | Applies to, For each, Minimum number of targets, Smallest flow that counts, Unit |
| Max Targets | Limits how many targets the covered lanes reach (MIP) | Applies to, For each, Maximum number of targets |
| Min Active Locations | Requires a number of the covered locations to be used (MIP) | Applies to, For each, Minimum number of locations, Smallest flow that counts, Unit |
| Max Active Locations | Limits how many of the covered locations are used (MIP) | Applies to, For each, Maximum number of locations |
Constraints marked MIP introduce binary variables. The solver handles them automatically, but solve times can grow on large networks.
There is no free-form constraint: the solver in this product runs no authored code, so a rule the table above cannot express is a request for a new type.
Service Level¶
To require 90% of deliveries to each customer within two hours, choose Service Level and enter:
- Applies to:
target.type == "Customer" - Qualifying flow:
lane.lead_time <= hours(2) - Minimum percentage:
90 - For each: target
Applies to selects the total flow; Qualifying flow selects the portion of that flow that counts toward the percentage. Both filters use the same condition editor. Blank Applies to selects all flows; blank Qualifying flow makes every selected flow qualify. Quantities use the scenario's quantity unit. Lead time measures the last lane from the serving stock location to the customer, not the whole path from the original source. A missing lane lead time does not qualify for a lead-time comparison.
The rule measures delivered flow, not requested demand. Demand fulfillment is controlled separately by Network Optimization. With no qualifying lane, a positive percentage permits only zero selected flow. Required delivery can then make the model infeasible; debug can loosen the service rule and names the affected customer. The rule adds no binary decisions.
The Rules table reports the requested and achieved percentage for each group. Zero total flow reports not_used with no achieved percentage. For a violated rule, missed_by is the shortfall in percentage points; debug minimizes missing flow quantity before optimizing cost.
Leave For each empty to measure one network-wide share. To compare targets,
branch the same input scenario through Service Level rules at 80%, 90% and
95%, solve each branch, and compare their transport costs and achieved shares.
A Constraint Table uses filter, qualifying_filter, min_percent and
for_each columns for the same rule.
Unit: every quantity rule states its own¶
A rule that bounds a quantity says what unit the bound is in, in its Unit
field. It can be the scenario's unit or any other unit, as long as every
product the rule covers has a conversion to it in Map Units & Conversions.
That is how a lane quoted in pallets also gets a weight limit: add a Lane
Capacity rule with Maximum flow 24 and Unit t.
A rule over several products weighs each product by its own conversion: one pallet of a heavy product takes more of a weight limit than one pallet of a light one.
A quantity rule with no Unit is refused before the run starts. One whose unit has no conversion for a product it covers is refused when the run reaches Network Optimization. That refusal is reported on this Add Constraint node. It names the rule, the Constraint Table row it came from if a table defines it, the product and the two units. On a Min count rule, Unit is the unit of the Smallest flow that counts. The Max count rules count locations, and Budget Limit bounds an amount of money; they have no Unit.
What a rule covers: Applies to and For each¶
Applies to¶
Applies to is the rule's filter: a condition that each of the rule's rows
is tested against. A lane rule's rows are the flows on your lanes, each with
its source, its target, its product and the lane itself. A rule covers the
rows that pass the filter. Leave it blank and the rule covers everything it
can, which is what ALL used to mean.
In the panel you build the filter as condition rows. Click Add condition,
pick an attribute such as source.tags or target.type, an operator and a
value. A row passes when every condition holds. Edit as expression shows
the same filter as text, where you can also write or, not and comparisons
between two attributes; Edit as conditions goes back when the expression
is still a list of conditions joined with and. Only the expression is saved,
so both views always describe the same rule.
The filter is read against the scenario on every run. A site tagged Export
after you wrote the rule is covered on the next run without editing it.
What a filter can name¶
Each rule type offers its own names:
| Rule types | Names in the filter | For each |
|---|---|---|
| Service Level, Lane Capacity, Lane Minimum, Fix Flow | source, target, product, lane |
source, target, product |
| Min Origins, Max Origins | source, target, product, lane |
target, product |
| Min Targets, Max Targets | source, target, product, lane |
source, product |
| Throughput Limit | location, product |
location, product |
| Min Active Locations, Max Active Locations | location, product |
product |
source, target and location are locations, product is a product, and
lane is the lane the flow runs on. Each offers these attributes:
| Name | Attributes |
|---|---|
| a location | name, type, address, latitude, longitude, tags |
product |
name, product_group, description, tags |
lane |
transport_mode, lead_time |
The attributes are the columns you mapped in Map Locations, Map Products
and Map Lanes: type is the location's Type, product_group the product's
Product Group, tags its Tags. A name or attribute the rule does not offer is
refused in the panel, with the names it does offer.
Writing a filter¶
| You want | Filter |
|---|---|
| One location | source.name == "Plant Hamburg" |
| Every location of a type | target.type == "Customer" |
| Locations of several types | source.type in {"Plant", "Warehouse"} |
| Locations with a tag | "Export" in source.tags |
| Locations without a tag | "Export" not in source.tags |
| Products with any of several tags | product.tags & {"Food", "Frozen Food"} |
| Products of a Product Group | product.product_group == "Chilled" |
| One transport mode | lane.transport_mode == "road" |
| Short lanes | lane.lead_time <= hours(36) |
| Northern customers | target.type == "Customer" and target.latitude > 52 |
| Flows between two kinds of site | source.type != target.type |
| Either of two things | "Export" in source.tags or source.type == "Port" |
The rules behind them:
- Text is written in quotes, as in
"Customer", and compared exactly as your data spells it, capitalisation included:"customer"does not matchCustomer. Text is compared with==(is) and!=(is not), or listed within {...}. - Tags are tested one at a time with
"Export" in source.tags, or any of several withsource.tags & {"Export", "Hub"}. - Numbers (latitude, longitude, lead time) take
==,!=,<,<=,>and>=. - Conditions combine with
and,orandnot, with parentheses where needed:"Food" in product.tags and not (lane.transport_mode == "air"). - Two attributes can be compared when they hold the same kind of value:
source.type != target.typecovers flows from one kind of site to another, and leaves out transfers between two warehouses. - A lead time is stated with its unit:
hours(2),days(2), alsominutes(...)andweeks(...). A bare number such aslane.lead_time <= 2is refused, because it does not say whether two hours or two days was meant. - A blank value matches no comparison. A location with no Type passes
neither
source.type == "Plant"norsource.type != "Plant", and a lane with no transport mode passes neither== "road"nor!= "road". A rule never covers a row because a cell was left empty.
To cover the locations of one country or region, tag them, for example
Germany, and write "Germany" in source.tags. The tags come from Map
Locations, so a new site with the tag is covered as soon as it is in your
data.
For each¶
For each says whether the rule is one limit over everything it covers, or one limit per member. Tick nothing for one rule. Tick For each source for one rule per source, on that source's covered flows. Tick two, such as source and product, for one rule per combination: one per source and product that has covered flows.
| Rule | With nothing ticked | With For each |
|---|---|---|
| Lane Capacity / Lane Minimum / Fix Flow | One limit on the total covered flow. "Germany" in source.tags and "Export" in target.tags: "at most 5,000 go from Germany to export markets" |
One limit per member. The same filter, For each source: "each German plant ships at most 1,000 to export markets" |
| Throughput Limit | One limit on the covered locations' combined throughput: what arrives at each of them plus what each sources itself. A transfer between two covered locations counts once, at the location receiving it | One limit per location, or per product |
| Min Origins / Max Origins | One count of the sources shipping on any covered lane: "at most 3 plants supply our customers" | One count per target, or per product: "each customer gets everything from one source" is For each target |
| Min Targets / Max Targets | One count of the targets any covered lane reaches | One count per source, or per product: "each warehouse serves at most 20 customers" is source.type == "Warehouse" and target.type == "Customer", For each source |
| Min Active Locations / Max Active Locations | One count over every covered location, a location counting when it is used for any covered product | For each product: one count per product |
Flow between covered locations counts. A lane rule covers every flow its
filter admits, so "Germany" in source.tags includes what German plants send
to German warehouses. To limit only what leaves Germany, say so:
"Germany" in source.tags and "Germany" not in target.tags.
When For each covers members the rule has nothing to limit for, such as a plant with no covered lane or a product nobody supplies, the rule applies to the members it can. The run lists the ones it left out, only members that take part in the network that way, so a customer is never listed as a source it could have been. A rule that matches nothing at all is refused.
What the run reports¶
Each rule says what it covered, in a Constraint applied message on this
node: how many lanes or locations, and as how many rules when it is held For
each. Because a filter follows the data, this count changes when the scenario
does. Read it after a data change, and when a filter's value might be
misspelt: a misspelt value in one part of an or matches less without
matching nothing, so only the count shows it.
Lane Minimum: always, or only when used¶
Minimum applies says when a Lane Minimum binds. Always requires the minimum whatever it costs. Only when used lets the optimizer leave the lanes empty: their flow is zero, or at least the minimum, as in "if we use the carrier at all, we give it at least 500". Only when used is a yes/no decision per rule, so the rule becomes MIP.
Held For each, every member decides on its own: For each source with Only when used lets each source either ship nothing on its covered lanes or at least the minimum. A lane's Minimum Quantity in Map Lanes is a separate, per-lane rule of the same zero-or-at-least form and is not affected by this choice.
Keeping products to a transport mode¶
To keep products to one transport mode, such as chilled products only by
reefer, give those products lanes of that mode only, in the lanes table; a
lane's product can be a filter such as product.product_group == "Chilled".
A cap of 0 on the other transport modes is not the same thing: it covers
only the modes your lanes have today, and a transport mode added later would
not be capped.
Counting origins, targets and active locations¶
Each of the six count rules counts locations, and says which:
- Origins: the sources that ship anything on the covered lanes. The filter
decides which lanes, and so which origins, count: "at most 2 origins among
German plants for each customer" is Max Origins with
target.type == "Customer" and "Germany" in source.tags, For each target, and a count of 2. Lanes from other sources are not covered, so those sources are free. With nothing ticked under For each, a covered lane between two targets counts too: the location shipping on it is an origin. - Targets: the destinations the covered lanes reach.
target.type == "Customer"counts customers and leaves transfers to warehouses free. - Active Locations: the covered locations that are used at all for the
covered products: anything arrives there, is sourced there or is made there.
"Open at most three warehouses" is Max Active Locations with
location.type == "Warehouse"and a count of 3. Under a Max rule whose filter says nothing about the product and with nothing ticked under For each, a location in Map Location Policy is counted by whether it is open: one set toincludecounts whether or not it ships. A Min rule counts only flow, so an included site is not made to carry anything.
Under a Max rule a location counts once it carries any flow of the covered
products; under a Min rule once its flow reaches the smallest flow that counts.
Only the covered products are looked at: Max Origins with
product.name == "Riesling" lets a customer receive other wines from anywhere.
Single source: each customer from one warehouse¶
Single sourcing is a Max Origins rule with Maximum number of origins set to 1:
- Applies to
target.type == "Customer", so the count is over the lanes into customers. Addand source.type == "Warehouse"to count only warehouses, so a plant delivering straight to a customer is not counted as its one source. - For each target: one count per customer. With nothing ticked the count would be over all customers together, and the whole network could be served from one site.
- For each product as well decides what "everything" means. Without it,
each customer gets all its products from the same warehouse. With it, each
product comes from one warehouse, which may differ by product. A condition
on the product, such as
"Key Wines" in product.tags, single-sources only those products and leaves the rest free.
Single sourcing is a yes/no choice for every customer and possible warehouse, so it can slow a solve on a large network. Applying it only to the products that need it is the usual first step; the guide Simplify a network optimization, in the documentation's Optimization section, has more.
Minimum rules: why Smallest flow that counts matters¶
Min Origins, Min Targets and Min Active Locations ask for the Smallest flow that counts. A location counts towards the minimum only when its flow reaches that quantity: its flow on the covered lanes for Min Origins and Min Targets, and what arrives there plus what it sources and makes for Min Active Locations. A location with a smaller flow is allowed; it is simply not counted. Without the floor, the solver could meet "at least two origins" by shipping a token unit from a second source. The value is in the rule's Unit and must be positive. A rule without it is refused before the run starts, and a value that is not positive is refused at the solve.
If the locations the rule can count are fewer than the minimum, or demand is too small for the minimum count times the smallest flow, the model has no solution; turn on Debug mode to see by how much the rule falls short.
Budget Limit: which costs count¶
Maximum transport cost caps transport cost: each lane's cost per unit times its flow, plus the cost Add Costs puts on lanes. Maximum total cost caps the network's total cost: supply, transport, handling, inventory carrying, production and fixed operating cost together, the same costs Network Optimization's Maximize Profit subtracts from revenue. A budget covers the whole network and has no filter.
Settings¶
| Setting | What it is | Default |
|---|---|---|
| Constraint type | One of the types above. Required. | none. The node is refused until one is picked |
| Constraint parameters | The values the chosen type asks for, in the form under the dropdown: Applies to, For each and the rule's bounds. Left empty when a table is wired into Constraint Table. | empty; Applies to covers everything, For each holds one rule, and Minimum applies starts at Always |
Driving constraints from a table¶
Wire a table to the Constraint Table input (the second port) to generate one constraint per row. The constraint type stays on the node; each row supplies the rule's values, with column names matching the ones below.
The table is the whole definition. Nothing is taken from the panel: with a table wired, the panel shows Defined by the table on Constraint Table in place of its fields, and names any required column the table lacks. A node with both a table and values typed into its panel is refused before the run starts, naming the values. A table with no rows is refused when the run reaches the node. Every row carries every required column: a row that leaves one blank is refused, naming the row and the column.
| Constraint Type | Required Columns | Optional Columns |
|---|---|---|
| Lane Capacity | max_flow, unit | filter, for_each |
| Lane Minimum | min_flow, unit | filter, for_each, min_flow_applies |
| Fix Flow | value, unit | filter, for_each |
| Throughput Limit | unit, and min or max | filter, for_each |
| Min Origins | min_count, min_flow, unit | filter, for_each |
| Max Origins | max_count | filter, for_each |
| Min Targets | min_count, min_flow, unit | filter, for_each |
| Max Targets | max_count | filter, for_each |
| Min Active Locations | min_count, min_flow, unit | filter, for_each |
| Max Active Locations | max_count | filter, for_each |
| Budget Limit | max_transport_cost or max_total_cost | none |
filter holds the rule's filter, written as in Edit as expression.
for_each holds the names to hold the rule for, separated by commas, such as
source or source, product, from the rule's For each column above.
min_flow_applies holds always or when_used. A blank optional cell is no
value: a blank filter covers everything, a blank for_each holds one rule, and a
missing bound is left out. A column the rule does not take, such as a misspelt
maxflow, is refused rather than ignored. So are the columns of earlier
versions (source, target, location, product, transport_mode and the
_scope columns): write the same selection as a filter.
A filter or for_each that cannot be read is refused when the run reaches Network Optimization. The refusal is reported on this node and names the Constraint Table row and what is wrong. In a spreadsheet you type the filter as it is. In a CSV file, a cell that holds commas or quotes is written in double quotes, with each quote inside it doubled, as in the example below.
Example: three lane caps from one file¶
With Constraint type set to Lane Capacity and nothing typed into the panel, this table wired into Constraint Table adds three rules:
filter,for_each,max_flow,unit
"""Germany"" in source.tags and ""Export"" in target.tags",,400,case
"target.name == ""DC Warsaw"" and product.product_group == ""Chilled"" and lane.transport_mode == ""reefer""",,150,case
"source.name == ""Plant Hamburg"" and target.name == ""Customer Berlin"" and product.product_group == ""Ambient""",product,50,case
In a spreadsheet the same filters are typed without the doubled quotes:
- Row 1,
"Germany" in source.tags and "Export" in target.tags: at most 400 cases go from locations taggedGermanyto locations taggedExportin total, every product and every transport mode. - Row 2,
target.name == "DC Warsaw" and product.product_group == "Chilled" and lane.transport_mode == "reefer": at most 150 chilled cases arrive at DC Warsaw by reefer, from anywhere. - Row 3,
source.name == "Plant Hamburg" and target.name == "Customer Berlin" and product.product_group == "Ambient": on the direct lane from Plant Hamburg to Customer Berlin, each ambient product on its own may ship at most - for_each is
product, so with two ambient products this is two rules of 50, not one of 50 over both.
The for_each column may be left out when every row is one rule; here it is written out so a reader sees which row is per member.
Workflow wiring¶
flowchart LR
n1["Import Data (locations.csv)"] --> n2["Map Locations"]
n3["Import Data (lanes.csv)"] --> n4["Map Lanes"]
n5["Import Data (supply.csv)"] --> n6["Map Supply Policy"]
n7["Import Data (demand.csv)"] --> n8["Map Demand Policy"]
n9["Import Data (products.csv)"] --> n10["Map Products"]
n2 --> n11["Create Scenario"]
n4 --> n11
n6 --> n11
n8 --> n11
n10 --> n11
n11 --> n12["Add Constraint (into Scenario Data)"]
n12 --> n13["Network Optimization"]
n13 --> n14["Create Scenario"]
Chain two nodes for two rules, such as a lane cap and a supplier floor:
flowchart LR
n1["Load Scenario"] --> n2["Add Constraint (into Scenario Data)"]
n2 --> n3["Add Constraint (into Scenario Data)"]
n3 --> n4["Network Optimization"]
n4 --> n5["Create Scenario"]
To drive the rules from a file, wire the Import Data node carrying the table into the second port, Constraint Table, and the scenario into the first.
Common mistakes¶
- Conflicting rules (a Minimum flow greater than a Maximum flow on the same lane): the model has no solution and the run fails. Turn on Debug mode on Network Optimization to see which rule gives way, and by how much; a rule held For each is named with the member that gave way and its filter.
- Leaving Unit blank on a rule that bounds a quantity: the rule is refused before the run starts, in the panel or, for a table, when the run reaches the node. The unit is never taken from the scenario.
- Forgetting the Smallest flow that counts on a Min count rule: refused the same way, because the count means nothing without it.
- A count that is not a whole number: the solve is refused.
- A Throughput Limit with neither a minimum nor a maximum, or a Budget Limit with neither ceiling, blank cells included: the solve is refused, because the rule limits nothing.
- Writing text without quotes, as in
target.type == Customer: refused, becauseCustomeris read as a name the filter does not know. - Spelling a value differently from the data, as in
"customer"forCustomeror a tag with a typo: the condition matches nothing. A filter that matches nothing at all is refused; one that only matches less shows it in the Constraint applied count. - Comparing a lead time with a bare number, as in
lane.lead_time <= 2: refused; writehours(2)ordays(2). - Expecting a filter on a location to leave out flow between covered
locations:
"Germany" in source.tagsincludes German-to-German flow. Add a condition on the target to leave it out. - A rule whose filter is valid but has nothing to limit, such as no lane from the covered sources to the covered targets, a target nothing ships to, or a Throughput location with no flow: the solve is refused as a rule that matches nothing, rather than run without it.
- Leaving For each empty when you meant a limit per member, or the other way round: check the ticks under For each.
- Expecting a rule to change the baseline data: rules shape what the optimizer is allowed to do, not what the scenario already holds.
- Heavy use of MIP rules (the Origins, Targets and Active Locations counts) on very large networks: solve time can grow significantly.
- Table column names that do not match the ones above: the run is refused and names the column; check the table above.
- Typing values into the panel beside a wired table, expecting them to fill the cells a row leaves blank: the run is refused. Put the value in every row of the table, and clear the panel.
Messages this node can show¶
A run gives this node 5 minutes. A node that takes longer stops with Time limit reached.
Like every node, it can also stop on the checks before a run and the run's limits.