HLPL language reference
MIP/LP Modelling Language Syntax (HLPL Files)
IMPORTANT: Read this entire syntax guide BEFORE creating any optimization models.
This is a domain-specific modelling language for Linear Programming (LP) and Mixed Integer Programming (MIP) that combines:
- Mathematical optimization model declarations
- Python/Lua scripting for data processing and results analysis
- LaTeX for documentation
- Template-based reusable constraint blocks
File Format
HLPL files use the .hlpl extension and are plain text files containing:
- Optional template definitions
- Model declaration (between "model" and "end")
- Python or Lua implementation code
- REQUIRED: main() function that returns integer exit code
Basic Structure
# Optional: Define reusable constraint templates
template templateName()
<constraint declarations>
end
# Main model definition — the model name MAY contain underscores (`shortest_path`), unlike set,
# constant and variable names. Whatever it is, the `init_model`/`run_model` calls in the Python
# section below must use exactly the same string.
model modelName
<set declarations>
<parameter declarations>
<variable declarations>
<objective function>
<constraints>
<template inclusions>
end
# Python/Lua implementation
import elytica
def main():
elytica.init_model("modelName")
elytica.run_model("modelName")
# ... process results
return 0 # REQUIRED: Return 0 for success
CRITICAL SYNTAX RESTRICTIONS
Before writing your model, understand these critical restrictions:
-
NO TUPLE/PAIR NOTATION IN SETS: Sets can only contain simple integers
- INVALID:
set A = {(1,2), (1,3), (2,4)}
- VALID:
set A = {1, 2, 3, 4} with constants for source/target nodes
-
NO UNDERSCORES IN NAMES: Use only in indexing (e.g., x_{i})
- INVALID:
set TIME_PERIODS, bin my_var, const max_cost
- VALID:
set T, bin x, const c
- The model name is exempt:
model shortest_path is valid. This rule is about sets,
constants and variables — the names that appear in indexed expressions. A model name never
does, so an underscore in it is not ambiguous with subscript notation.
VALID: model knapsack, model shortest_path, model office_allocation
- Why the rule exists:
_ is the subscript operator, and its braces are optional for a
single index — x_i is read as exactly x_{i}. A name containing an underscore is therefore
parsed as a name plus a subscript, and the declaration never registers the name you wrote.
Use the braced form x_{i} in models; the unbraced one is what makes the rule necessary.
- The error you get will not point at the declaration. Three shapes to recognise:
error[reference::undefined-name]: 'xarc' was not defined in the model — an underscored
variable name, reported with the underscore removed. bin x_arc gives this at every use
of x_arc_{i,j}, and rewriting the uses as xarc_{i,j} does not help: the declaration is
what failed, not the reference.
error[type::non-linear-term]: product of two expressions that both contain variables —
an underscored constant name multiplied by a real variable. const d_ist used as
d_ist_{e} * x_{i,j} gives this, and it names neither the constant nor the underscore.
error[reference::set-not-found]: set 'N_' was not declared in the model — an underscored
set name whose suffix is a single character, such as set N_i.
- A set name with a longer suffix (
set N_ODES) does happen to parse. That is one spelling
surviving, not the rule being soft: a parameterized set sig_ma(N) fails with a syntax error
inside the sum that uses it. Keep the rule uniformly.
-
ONLY THREE CONSTRAINT OPERATORS: =, >=, <=
- INVALID:
<, >, !=, == (comparison in constraint filters)
- VALID:
=, >=, <=
-
NO RANGE NOTATION: Cannot use {1..10}
- INVALID:
set I = {1..10}
- VALID:
set I = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} or use function
-
USE [] FOR GROUPING, NOT (): Parentheses only for function calls and set functions
- INVALID:
sum_{i in I}{(x_{i} + y_{i}) * c_{i}}
- VALID:
sum_{i in I}{[x_{i} + y_{i}] * c_{i}}
-
SUMMATION MUST HAVE CURLY BRACES WITH EXPRESSION: Cannot use empty brackets
- INVALID:
sum_{i in I}[]
- INVALID:
sum_{i in I}()
- VALID:
sum_{i in I}{x_{i}}
-
NO PROGRAMMATIC OPERATORS IN CONSTRAINTS: Cannot use ? :, ==, != for filtering
- INVALID:
sum_{a in A}{f_{a} * (s_{a} == n ? 1 : 0)}
- VALID: Use indicator constants or
setminus {} for set operations
Model Components
Sets
IMPORTANT NAMING RULES:
- Set names MUST NOT contain underscores.
set N_i fails as
error[reference::set-not-found]: set 'N_' was not declared — the _i is read as a subscript
- PREFER single letters (I, J, K, T, N, A) for mathematical readability
- Document what each set represents in comments
IMPORTANT DATA LOADING: PREFER loading sets from functions over hardcoding values.
CRITICAL: TUPLE/PAIR NOTATION NOT ALLOWED
- Sets can ONLY contain simple integers: {1, 2, 3, 4, 5}
- Tuple notation like {(1,2), (1,3), (2,4)} is NOT supported
- For arcs/edges/pairs, use separate indices in forall loops
# PREFERRED: Load from functions
set I = getItems() # Items (preferred: function-based)
set T = getTimeperiods() # Time periods (preferred: function-based)
set N = getNodes() # Nodes (preferred: function-based)
set A = getArcs() # Arcs - returns simple integers (preferred: function-based)
# Less preferred: Hardcoded values (only for small examples)
set I = {1, 2, 3, 4, 5} # Hardcoded (use only for examples)
set J = {1..10} # INVALID: Range notation not allowed!
set TIMEPERIODS = {1, 2} # Valid but not preferred: less readable
set TIME_PERIODS = {1, 2} # INVALID: underscore in set name
# INVALID: Tuple/pair notation NOT allowed
set A = {(1,2), (1,3), (2,3), (2,4), (3,4), (3,5), (4,5), (1,4), (2,5)} # INVALID!
set E = {(1,3), (1,4), (2,3)} # INVALID!
Set functions (parameterized sets):
set I = getItems() # Base set loaded from function
set P(I) = getProductIdsSubsets() # Function-based parameterized set
Correct approach for arcs/edges/pairs:
Since tuple notation is not supported, use one of these approaches:
Approach 1: Separate node sets with filtering in Python
set N = {1, 2, 3, 4, 5} # Nodes
const hasArc = getArcMatrix(), forall i in N, forall j in N # Adjacency matrix
# Arc flow variables
var 0<=f<=infty, forall i in N, forall j in N # Flow on arc (i,j)
# Constraint: flow only on existing arcs
constr f_{i,j} <= 1000 * hasArc_{i,j}, forall i in N, forall j in N
# Python: Load adjacency matrix where 1 means arc exists, 0 means no arc
getArcMatrix = lambda: {
1: {1: 0, 2: 1, 3: 1, 4: 1, 5: 0},
2: {1: 0, 2: 0, 3: 1, 4: 1, 5: 1},
3: {1: 0, 2: 0, 3: 0, 4: 1, 5: 1},
4: {1: 0, 2: 0, 3: 0, 4: 0, 5: 1},
5: {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}
}
Approach 2: Use a simple arc ID set with mapping constants
set A = {1, 2, 3, 4, 5, 6, 7, 8, 9} # Arc IDs
const source = getSources(), forall a in A # Source node of each arc
const target = getTargets(), forall a in A # Target node of each arc
const cost = getArcCosts(), forall a in A # Cost of each arc
# Arc flow variables
var 0<=f<=infty, forall a in A # Flow on arc a
# Flow conservation: sum outgoing - sum incoming = supply
# Define subsets for outgoing and incoming arcs per node in Python
const outarc = getOutgoingArcs(), forall n in N, forall a in A # 1 if arc a starts at node n, 0 otherwise
const inarc = getIncomingArcs(), forall n in N, forall a in A # 1 if arc a ends at node n, 0 otherwise
constr sum_{a in A}[f_{a} * outarc_{n,a}] - sum_{a in A}[f_{a} * inarc_{n,a}] = supply_{n}, forall n in N
# Python: Define arc mappings and constants
getSources = lambda: {1: 1, 2: 1, 3: 2, 4: 2, 5: 3, 6: 3, 7: 4, 8: 1, 9: 2}
getTargets = lambda: {1: 2, 2: 3, 3: 3, 4: 4, 5: 4, 6: 5, 7: 5, 8: 4, 9: 5}
getArcCosts = lambda: {1: 5, 2: 3, 3: 8, 4: 2, 5: 6, 6: 4, 7: 1, 8: 7, 9: 3}
# Define binary indicators for arc-node relationships
def getOutgoingArcs():
sources = getSources()
return {n: {a: (1 if sources[a] == n else 0) for a in sources} for n in range(1, 6)}
def getIncomingArcs():
targets = getTargets()
return {n: {a: (1 if targets[a] == n else 0) for a in targets} for n in range(1, 6)}
Key point: Always use simple integer sets and represent relationships using constants or adjacency matrices.
Set Operators:
constr x_{i} <= 5, forall i in I setminus {1} # Remove 1 from the set
constr x_{i} <= 5, forall i in I setminus {PythonFunction()} # PythonFunction() should return a list of unsigned integers
Constants
IMPORTANT NAMING RULES:
- Constant names MUST NOT contain underscores (underscores ONLY for indexing:
v_{i}).
const d_ist used as d_ist_{e} * x_{i,j} fails as
error[type::non-linear-term]: product of two expressions that both contain variables, which
names neither the constant nor the underscore
- PREFER Greek letters (alpha, beta, gamma, lambda, mu, sigma) and single letters (c, v, w, p, d)
- Document what each constant represents in comments
IMPORTANT DATA LOADING: PREFER loading constants from functions over hardcoding values.
# PREFERRED: Load from functions
const c = getCosts(), forall i in I # Cost (preferred: function-based)
const v = getValues(), forall i in I # Value (preferred: function-based)
const w = getWeights(), forall i in I # Weight (preferred: function-based)
const alpha = getDiscounts(), forall i in I # Discount factor (preferred: function-based)
const mu = getDemands(), forall i in I # Mean demand (preferred: function-based)
const d = getDistances(), forall i in I, forall j in J # Distance matrix (function-based)
# Less preferred: Hardcoded values (only for small examples)
const c = {1.0, 2.0, 3.0}, forall i in I # Hardcoded (use only for examples)
const v = {0.5, 1, 2.5}, forall i in I # Hardcoded (use only for examples)
# Valid but less readable
const capacity = {10, 20}, forall i in I # Valid but less readable
const maxcost = {1.0, 2.0}, forall i in I # Valid but less readable
# INVALID
const max_cost = {1.0, 2.0}, forall i in I # INVALID: underscore in constant name
Function-based loading (up to 7 indices):
- Single index: getCosts() should return a dictionary, e.g. {1: 0.5, 2: 1.0, 3: 2.5}
- Multiple indices: getDistances() should return nested dictionaries, e.g. {1: {1: 0.5, 2: 2.0}, 2: {1: 1.0, 2: 3.0}}
- Can be up to 7 levels deep (7 indices for the constant)
Best Practice:
Load a data.json file in Python, then use functions to extract data for the model:
import json
data = json.load(open('data/input.json'))
getCosts = lambda: data['costs']
getValues = lambda: data['values']
Decision Variables
IMPORTANT NAMING RULES:
- Variable names MUST NOT contain underscores (underscores ONLY for indexing:
x_{i}).
bin x_arc fails as error[reference::undefined-name]: 'xarc' was not defined in the model at
every use — the underscore is stripped from the reported name, and the declaration is what failed
- PREFER Greek letters (lambda, epsilon, theta, pi, rho) and single letters (x, y, z, f, q)
- Document what each variable represents in comments
Binary variables (0 or 1):
bin x, forall i in I # Item selection (preferred: single letter)
bin y, forall i in I, forall j in J # Assignment (preferred: single letter)
bin lambda, forall i in I # Binary indicator (preferred: Greek letter)
bin theta, forall k in K # Configuration choice (preferred: Greek letter)
bin selected, forall i in I # Valid but less readable
bin myvar, forall i in I # Valid but less readable
bin my_var, forall i in I # INVALID: underscore in variable name
Continuous variables with bounds:
var z, forall i in I # Objective contribution (preferred: single letter)
var f, forall a in A # Flow (preferred: single letter)
var q, forall i in I # Quantity (preferred: single letter)
var 0<=epsilon<=infty, forall t in T # Slack variable (preferred: Greek letter)
var 0<=rho<=infty, forall i in I # Utilization (preferred: Greek letter)
var 0<=pi<=1, forall i in I # Proportion (preferred: Greek letter)
var flow, forall a in A # Valid but less readable
var maxflow, forall a in A # Valid but less readable
var max_flow, forall a in A # INVALID: underscore in variable name
Objective Function
IMPORTANT: Summations use format: sum_{index in SET}{expression}
Use [] for grouping complex expressions, NOT ()
Maximization:
max sum_{i in I}{v_{i} * x_{i}}
max sum_{i in I}{[v_{i} + bonus_{i}] * x_{i}} # Use [] for grouping
Minimization:
min sum_{i in I}{c_{i} * x_{i}}
min sum_{i in I}{[c_{i} + penalty_{i}] * x_{i}} # Use [] for grouping
Constraints
IMPORTANT: Only three constraint operators are allowed: =, >=, <=
IMPORTANT: Use [] for grouping expressions, NOT ()
IMPORTANT: No programmatic operators (? :, ==, !=) in mathematical expressions
Simple constraints:
constr sum_{i in I}{w_{i} * x_{i}} <= 15 # Less than or equal
constr x_{i} + y_{j} >= 1, forall i in I, forall j in J # Greater than or equal
constr x_{i} = 1, forall i in I # Equality
constr x_{i} < 5, forall i in I # INVALID: < not allowed
constr x_{i} > 0, forall i in I # INVALID: > not allowed
constr x_{i} != 1, forall i in I # INVALID: != not allowed
constr sum_{a in A}{f_{a} * [s_{a} == n ? 1 : 0]} = b_{n}, forall n in N # INVALID: cannot use () outside functions, cannot use ternary ? :
constr sum_{a in A : s_{a} == n}{f_{a}} = b_{n}, forall n in N # INVALID: cannot use : filtering with ==, use setminus {} or define indicator constants
Python Integration
After the model declaration, use Python with the elytica module.
IMPORTANT: Scripts MUST have a main() function that returns an integer (the program exit code).
- Return 0 for success
- Return non-zero for failure
Basic elytica API
import elytica
def main():
# Solver configuration
elytica.set_gap_limit("model_name", 0.01) # 1% MIP gap
elytica.set_time_limit("model_name", 3600) # 1 hour time limit
# Initialize and solve
elytica.init_model("model_name")
elytica.run_model("model_name")
# Get variable values
value = elytica.get_variable_value("model_name", "x1")
multi_index = elytica.get_variable_value("model_name", "alpha1,2,3")
# Get objective value
obj = elytica.get_best_primal_bound("model_name")
# Write results (JSON, plain text, or any format)
elytica.write_results(str(obj))
return 0 # REQUIRED: Return 0 for success
Generating Charts with ECharts (Optional)
You can optionally generate interactive visualizations by writing chart configurations to the results file.
Charts are rendered in the tutorial interface using ECharts.
Note: Charts are OPTIONAL. Only create charts when specifically requested by the user.
CRITICAL CHART REQUIREMENTS (when creating charts):
- Charts MUST be fully JSON serializable
- Use integers 0 and 1 instead of boolean True/False
- NO None values - calculate all values beforehand
- NO function references - all data must be literal values
- NO null values - use 0 or empty string instead
import elytica
import json
def main():
elytica.init_model("model_name")
elytica.run_model("model_name")
# Create a chart configuration (ECharts format)
# All values must be JSON serializable - no True/False/None
chart = {
"title": {"text": "Optimization Results"},
"tooltip": {},
"xAxis": {
"type": "category",
"data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
},
"yAxis": {
"type": "value"
},
"series": [
{
"name": "Variable Values",
"data": [3, 4, 5, 6, 4, 3, 1], # Good: literal integers
"type": "line"
}
]
}
# Write results with chart
elytica.write_results(json.dumps({"chart": chart}))
return 0
Multiple charts can be included using the "charts" key (array):
import elytica
import json
def main():
elytica.init_model("model_name")
elytica.run_model("model_name")
# Create multiple charts
# Remember: Use 0/1 for booleans, no None/null values
chart1 = {
"title": {"text": "Decision Variables"},
"xAxis": {"type": "category", "data": ["x1", "x2", "x3", "x4", "x5"]},
"yAxis": {"type": "value"},
"series": [{"data": [1, 0, 1, 1, 0], "type": "bar"}] # Good: 1/0 not True/False
}
chart2 = {
"title": {"text": "Objective Value Over Time"},
"xAxis": {"type": "category", "data": ["Iter 1", "Iter 2", "Iter 3"]},
"yAxis": {"type": "value"},
"series": [{"data": [100, 150, 180], "type": "line"}]
}
# Write multiple charts
elytica.write_results(json.dumps({"charts": [chart1, chart2]}))
return 0
Common chart types supported:
"type": "line" - Line charts
"type": "bar" - Bar charts
"type": "pie" - Pie charts
"type": "scatter" - Scatter plots
Chart Data Best Practices:
- Calculate all values before creating chart structure
- Convert boolean results to integers:
int(value) gives 0 or 1
- Replace None with 0 or appropriate default:
value if value is not None else 0
- Use list comprehensions to ensure all data is literal values
- Test with
json.dumps() before writing - it will fail on non-serializable data
For full ECharts documentation, visit: https://echarts.apache.org/en/option.html
Example: Simple Knapsack Problem
This example demonstrates preferred naming: single letters with clear documentation.
model knapsack
set I = {1, 2, 3, 4, 5} # Items
const v = {0.5, 1, 2.5, 0.1, 6.0}, forall i in I # Value of item i
const w = {3, 4, 5, 4.5, 8.0}, forall i in I # Weight of item i
bin x, forall i in I # 1 if item i is selected, 0 otherwise
max sum_{i in I}{v_{i} * x_{i}} # Maximize total value
constr sum_{i in I}{w_{i} * x_{i}} <= 15 # Weight capacity constraint
end
import elytica
def main():
elytica.init_model("knapsack")
elytica.run_model("knapsack")
for i in range(1,6):
vname = "x" + str(i)
val = elytica.get_variable_value("knapsack", vname)
print(vname, val)
elytica.write_results(str(elytica.get_best_primal_bound("knapsack")))
return 0
Example: Knapsack with Chart Visualization (Optional)
This example demonstrates preferred naming with clear documentation and OPTIONAL chart generation.
Only create charts when specifically requested by the user.
Important: Chart data uses int(val) to convert to 0/1 instead of True/False.
model knapsack
set I = {1, 2, 3, 4, 5} # Items
const v = {0.5, 1, 2.5, 0.1, 6.0}, forall i in I # Value of item i
const w = {3, 4, 5, 4.5, 8.0}, forall i in I # Weight of item i
bin x, forall i in I # 1 if item i is selected, 0 otherwise
max sum_{i in I}{v_{i} * x_{i}} # Maximize total value
constr sum_{i in I}{w_{i} * x_{i}} <= 15 # Weight capacity constraint
end
import elytica
import json
def main():
elytica.init_model("knapsack")
elytica.run_model("knapsack")
# Collect results - calculate all values beforehand
items = []
selected = []
values = []
weights = []
# Define data arrays
value_data = [0.5, 1, 2.5, 0.1, 6.0]
weight_data = [3, 4, 5, 4.5, 8.0]
for i in range(1, 6):
vname = "x" + str(i)
val = elytica.get_variable_value("knapsack", vname)
items.append("Item " + str(i))
# CRITICAL: Use int(val) to get 0 or 1, NOT boolean True/False
selected.append(int(val))
values.append(value_data[i-1])
weights.append(weight_data[i-1])
# Create chart showing selected items
# All data is pre-calculated - no None, True/False, or function references
chart = {
"title": {"text": "Knapsack Solution"},
"tooltip": {},
"legend": {"data": ["Selected", "Value", "Weight"]},
"xAxis": {
"type": "category",
"data": items # List of strings
},
"yAxis": {
"type": "value"
},
"series": [
{
"name": "Selected",
"data": selected, # List of 0/1 integers (not True/False)
"type": "bar",
"itemStyle": {"color": "#5470c6"}
},
{
"name": "Value",
"data": values, # List of floats
"type": "line",
"itemStyle": {"color": "#91cc75"}
},
{
"name": "Weight",
"data": weights, # List of floats
"type": "line",
"itemStyle": {"color": "#fac858"}
}
]
}
# Write results with chart
obj_value = elytica.get_best_primal_bound("knapsack")
elytica.write_results(json.dumps({
"objective": obj_value,
"chart": chart
}))
return 0
Available Solvers
- CPLEX (commercial, high-performance)
- CoinOR/CBC (open-source)
- Gurobi (commercial, high-performance)
- HiGHS (open-source)
- SCIP (open-source)
MIP/LP Modelling Language Syntax Rules Summary
-
Constraint Operators: ONLY =, >=, <= are allowed (no <, >, !=)
-
Summation Format: Always use sum_{index in SET}{expression} with curly braces and an expression
- INVALID:
sum_{i in I}[] or sum_{i in I}()
- VALID:
sum_{i in I}{x_{i}}
-
Grouping: Use [] for grouping expressions, NOT () (parentheses only for function calls)
-
No Programmatic Operators: Cannot use ? :, ==, != in mathematical expressions
- Use indicator constants defined in Python instead
- Use
setminus {} for set operations
-
Data Loading (CRITICAL):
- PREFER loading sets and constants from functions over hardcoding
- Function-based:
set I = getItems(), const c = getCosts(), forall i in I
- Hardcoding is acceptable only for small examples/demonstrations
- Best practice: Load
data.json in Python and use functions to extract data
-
Naming Conventions (CRITICAL):
- MUST NOT contain underscores in names (underscores ONLY for indexing:
x_{i}, v_{i,j}).
_ is the subscript operator and its braces are optional for one index, so x_i is read as
x_{i} and a name carrying one never registers. Interpreter-verified for sets, constants and
variables; the three diagnostics it produces are listed under CRITICAL SYNTAX RESTRICTIONS
- The model name itself is exempt:
model shortest_path is valid, and several of the
provided examples use one. The rule exists so a name cannot be confused with subscript
notation, and a model name never appears in an indexed expression.
- PREFER single letters: I, J, K, T, x, y, z, c, v, w, f, q
- PREFER Greek letters: alpha, beta, gamma, lambda, mu, sigma, epsilon, theta, pi, rho
- ALWAYS document what each variable/constant/set represents in comments
- Mathematical readability:
x_{i} is more readable than itemselection_{i}
- Valid names:
x, lambda, epsilon, c, I, T
- Less preferred (but valid):
cost, flow, NODES, selected
- Invalid:
x_var, max_cost, flow_rate, TIME_PERIODS
-
Return Code: Always return 0 for success, non-zero for failure
-
Semicolons: Not required at the end of statements (see examples)