Equipment selection and scheduling with binary and integer variables
Published
May 28, 2026
1 Overview
This example demonstrates Mixed-Integer Linear Programming (MILP) in Optyx — optimization problems where some variables must take discrete (integer or binary) values. MILP is essential for engineering decisions like equipment selection, shift scheduling, and facility location.
Optyx automatically detects integer/binary variables and routes to the MILP solver (SciPy’s HiGHS backend).
1.1 MILP Variable Types
Type
Creation
Values
Use Case
BinaryVariable
BinaryVariable("x")
0 or 1
Yes/no decisions
IntegerVariable
IntegerVariable("x", lb, ub)
Integers in range
Discrete quantities
VectorVariable (binary)
VectorVariable("x", n, domain="binary")
Vector of 0/1
Selection problems
VectorVariable (integer)
VectorVariable("x", n, domain="integer")
Vector of integers
Batch scheduling
2 Problem Setup
A mining operation needs to select equipment, schedule shifts, and decide which depots to open.
import numpy as npfrom optyx import ( BinaryVariable, IntegerVariable, Variable, VectorVariable, Problem,)equipment = ["Excavator A", "Excavator B", "Loader C", "Drill Rig D"]n_equip =len(equipment)# Fixed cost to acquire/lease ($k/month)fixed_cost = [120, 180, 85, 95]# Capacity per shift (tonnes/shift)capacity_per_shift = [500, 800, 350, 200]# Operating cost per shift ($k/shift)op_cost_per_shift = [8, 12, 5, 6]# Maximum shifts per weekmax_shifts = [14, 14, 21, 21]min_production =8000# tonnes/week minimumtarget_production =12000# tonnes/week idealprint(f"{'Machine':<16}{'Fixed ($k)':>10}{'Cap/Shift':>10}{'Op ($k)':>8}{'Max Shifts':>11}")print("-"*58)for i inrange(n_equip):print(f"{equipment[i]:<16}{fixed_cost[i]:>10}{capacity_per_shift[i]:>8} t {op_cost_per_shift[i]:>7}{max_shifts[i]:>9}/wk")
Machine Fixed ($k) Cap/Shift Op ($k) Max Shifts
----------------------------------------------------------
Excavator A 120 500 t 8 14/wk
Excavator B 180 800 t 12 14/wk
Loader C 85 350 t 5 21/wk
Drill Rig D 95 200 t 6 21/wk
3 Part 1: Binary Equipment Selection
The simplest MILP involves binary decisions — should we acquire each piece of equipment?
3.1 BinaryVariable
BinaryVariable("name") creates a variable constrained to \(\{0, 1\}\):
# Binary: acquire this equipment?acquire = [BinaryVariable(f"acquire_{equipment[i]}") for i inrange(n_equip)]print("Created binary variables:")for a in acquire:print(f" {a}")
prob1 = Problem(name="equipment_selection")# Objective: minimize fixed coststotal_fixed =sum(fixed_cost[i] * acquire[i] for i inrange(n_equip))prob1.minimize(total_fixed)# Capacity constraint (at max shifts)total_capacity =sum( capacity_per_shift[i] * max_shifts[i] * acquire[i] for i inrange(n_equip))prob1.subject_to(total_capacity >= min_production)# Redundancy: need at least 2 machinesprob1.subject_to(sum(acquire) >=2)sol1 = prob1.solve()print(f"Status: {sol1.status.name}")print(f"Optimal fixed cost: ${sol1.objective_value:.0f}k/month")print(f"MIP gap: {sol1.mip_gap}")print(f"Best bound: {sol1.best_bound}")print("\nEquipment acquired:")for i inrange(n_equip): selected = sol1[acquire[i].name] marker ="YES"if selected >0.5else"no"print(f" {equipment[i]:<16} → {marker}")
Status: OPTIMAL
Optimal fixed cost: $180k/month
MIP gap: 0.0
Best bound: 180.0
Equipment acquired:
Excavator A → no
Excavator B → no
Loader C → YES
Drill Rig D → YES
Note
sol.mip_gap and sol.best_bound are MIP-specific solution fields. The gap indicates how close the solution is to proven optimality — a gap of 0 means the solution is provably optimal.
4 Part 2: Integer Shift Scheduling
IntegerVariable handles discrete quantities — here, the number of shifts per week for each machine.
shifts = [ IntegerVariable(f"shifts_{equipment[i]}", lb=0, ub=max_shifts[i])for i inrange(n_equip)]print("Created integer variables:")for s in shifts:print(f" {s}")
Minimize operating cost while meeting the production target:
prob2 = Problem(name="shift_scheduling")total_op_cost =sum(op_cost_per_shift[i] * shifts[i] for i inrange(n_equip))prob2.minimize(total_op_cost)# Meet production targetproduction =sum(capacity_per_shift[i] * shifts[i] for i inrange(n_equip))prob2.subject_to(production >= target_production)# Minimum 2 shifts each (maintenance window)for i inrange(n_equip): prob2.subject_to(shifts[i] >=2)sol2 = prob2.solve()print(f"Status: {sol2.status.name}")print(f"Optimal operating cost: ${sol2.objective_value:.0f}k/week")print(f"\n{'Machine':<16}{'Shifts/wk':>10}{'Production':>12}{'Cost':>10}")print("-"*52)total_prod =0for i inrange(n_equip): s = sol2[shifts[i].name] prod = s * capacity_per_shift[i] cost = s * op_cost_per_shift[i] total_prod += prodprint(f"{equipment[i]:<16}{s:>10.0f}{prod:>10.0f} t ${cost:>7.0f}k")print("-"*52)print(f"{'TOTAL':<16}{'':>10}{total_prod:>10.0f} t ${sol2.objective_value:>7.0f}k")
Status: OPTIMAL
Optimal operating cost: $183k/week
Machine Shifts/wk Production Cost
----------------------------------------------------
Excavator A 2 1000 t $ 16k
Excavator B 5 4000 t $ 60k
Loader C 19 6650 t $ 95k
Drill Rig D 2 400 t $ 12k
----------------------------------------------------
TOTAL 12050 t $ 183k
5 Part 3: Binary Vectors
VectorVariable with domain="binary" creates a vector of binary variables — ideal for selection/knapsack problems with many items.
5.1 Spare Parts Selection
Select which spare parts to stock, maximizing equipment coverage within a budget:
The most powerful MILP pattern combines binary (open/close) and continuous (allocation) variables linked by Big-M constraints.
6.1 Problem
Decide which supply depots to open and how to allocate material to mining sites:
Binary: whether to open each depot
Continuous: tonnes/day shipped from each depot to each site
Big-M: can only ship from open depots
depots = ["Central", "North", "South"]sites = ["Pit A", "Pit B", "Pit C", "Pit D"]n_depots =len(depots)n_sites =len(sites)depot_fixed_cost = [50, 35, 40] # $k/monthdepot_capacity = [200, 150, 180] # tonnes/day# Transport cost per tonne ($)transport_cost = np.array([ [3, 8, 5, 7], # Central → each site [6, 2, 9, 4], # North [7, 5, 3, 2], # South])site_demand = [60, 45, 55, 40] # tonnes/dayprint(f"{'Depot':<10}{'Fixed ($k)':>10}{'Capacity':>10}")print("-"*33)for j inrange(n_depots):print(f"{depots[j]:<10}{depot_fixed_cost[j]:>10}{depot_capacity[j]:>8} t/d")print(f"\nTransport cost matrix ($/t):")print(f"{'':>10}", end="")for k inrange(n_sites):print(f"{sites[k]:>10}", end="")print()for j inrange(n_depots):print(f"{depots[j]:>10}", end="")for k inrange(n_sites):print(f"{transport_cost[j,k]:>10}", end="")print()
Depot Fixed ($k) Capacity
---------------------------------
Central 50 200 t/d
North 35 150 t/d
South 40 180 t/d
Transport cost matrix ($/t):
Pit A Pit B Pit C Pit D
Central 3 8 5 7
North 6 2 9 4
South 7 5 3 2
prob4 = Problem(name="depot_location")# Binary: open depot j?open_depot = [BinaryVariable(f"open_{depots[j]}") for j inrange(n_depots)]# Continuous: tonnes from depot j to site kalloc = {}for j inrange(n_depots):for k inrange(n_sites): alloc[j, k] = Variable(f"alloc_{depots[j]}_{sites[k]}", lb=0)# Objective: fixed + transport costsobj =sum(depot_fixed_cost[j] * open_depot[j] for j inrange(n_depots))for j inrange(n_depots):for k inrange(n_sites): obj = obj + transport_cost[j, k] * alloc[j, k]prob4.minimize(obj)# Demand satisfactionfor k inrange(n_sites): prob4.subject_to(sum(alloc[j, k] for j inrange(n_depots)) >= site_demand[k] )# Big-M capacity linking: can only allocate from open depotsfor j inrange(n_depots): prob4.subject_to(sum(alloc[j, k] for k inrange(n_sites)) <= depot_capacity[j] * open_depot[j] )
6.3 Solution
sol4 = prob4.solve()print(f"Status: {sol4.status.name}")print(f"Total cost: ${sol4.objective_value:.1f}k")print(f"MIP gap: {sol4.mip_gap}")print("\nDepot decisions:")for j inrange(n_depots): opened = sol4[open_depot[j].name] status ="OPEN"if opened >0.5else"closed" fc =f"${depot_fixed_cost[j]}k"if opened >0.5else"-"print(f" {depots[j]:<10} → {status:<8} (fixed: {fc})")print(f"\n{'From → To':<25}{'Tonnes/day':>12}")print("-"*40)for j inrange(n_depots):if sol4[open_depot[j].name] >0.5:for k inrange(n_sites): val = sol4[alloc[j, k].name]if val >0.1:print(f" {depots[j]} → {sites[k]:<12}{val:>10.1f}")
Status: OPTIMAL
Total cost: $640.0k
MIP gap: 0.0
Depot decisions:
Central → OPEN (fixed: $50k)
North → OPEN (fixed: $35k)
South → OPEN (fixed: $40k)
From → To Tonnes/day
----------------------------------------
Central → Pit A 60.0
North → Pit B 45.0
South → Pit C 55.0
South → Pit D 40.0
7 Automatic Solver Routing
Optyx detects variable domains and routes automatically:
Variables
Solver
All continuous
LP (linprog)
Any integer/binary
MILP (HiGHS via milp)
Nonlinear objective/constraints
NLP (minimize)
No configuration needed — just declare your variable types and call prob.solve().