An AUGMECON based multi-objective optimization solver for Pyomo.
Project description
Multi-objective optimization for Pyomo using the AUGMECON method
PyAUGMECON solves multi-objective optimization problems defined in Pyomo using the augmented epsilon-constraint method (AUGMECON) and its variants.
- Generates the Pareto front by solving a sequence of single-objective subproblems with epsilon constraints.
- Supports exact mode (full grid coverage) and sampled mode (user-defined discretization).
- Implements AUGMECON early exit, AUGMECON2 bypass, and AUGMECON-R pruning to skip redundant solves.
- Runs subproblems in parallel across multiple processes with spawn-safe serialization.
- Resolves solver backends automatically across HiGHS, Gurobi, CPLEX, XPRESS, CBC, and SCIP families.
GAMS implementations of the method were provided by the original authors. To the best of our knowledge, this is the first publicly available Python implementation.
Contents
Installation
PyAUGMECON is published on PyPI and conda-forge. The base package ships without a solver, so pick a solver extra when installing:
# Recommended: uv (https://docs.astral.sh/uv/)
uv add "pyaugmecon[highs]"
# Or with pip
pip install "pyaugmecon[highs]"
# Or with conda / mamba
conda install -c conda-forge pyaugmecon
Available solver extras:
| Extra | Solver |
|---|---|
pyaugmecon[highs] |
HiGHS (open source, no license required). |
pyaugmecon[gurobi] |
Gurobi (requires license). |
pyaugmecon[xpress] |
XPRESS (uses the license available to the installed xpress package). |
pyaugmecon[cbc] |
CBC executable via cbcbox (matches Pyomo cbc backends). |
Solvers not bundled as extras but supported when their binary is on PATH:
| Solver | Link | Solver name |
|---|---|---|
| SCIP | scipopt.org | scip |
| CPLEX | ibm.com/products/ilog-cplex-optimization-studio | cplex |
Once installed, pass solver_name="scip" or solver_name="cplex". You can also install CBC manually instead of pyaugmecon[cbc]; cbcbox is typically faster than a system-installed CBC binary (e.g. 85 s vs 471 s on 3kp40), as it ships AVX2-optimized builds with bundled OpenBLAS.
conda solver names: PyPI
[solver]extras don't apply to conda. Install solvers as separate packages:
Solver conda package HiGHS conda install -c conda-forge highspyGurobi conda install -c gurobi gurobiXPRESS conda install -c fico-xpress xpressCPLEX conda install -c ibmdecisionoptimization cplexCBC conda install -c conda-forge coincbcSCIP conda install -c conda-forge scipGurobi, XPRESS, and CPLEX are distributed via vendor-specific channels (not conda-forge) and require a license to use.
Quick start
Define a Pyomo model with objectives in an ObjectiveList named obj_list, then pass it to PyAUGMECON. Defaults are sensible, so a minimal run takes no config:
from pyomo.environ import ConcreteModel, Constraint, ObjectiveList, Var, NonNegativeReals, maximize
from pyaugmecon import PyAugmecon, PyAugmeconConfig
def two_objective_model():
model = ConcreteModel()
model.x1 = Var(within=NonNegativeReals)
model.x2 = Var(within=NonNegativeReals)
model.con1 = Constraint(expr=model.x1 <= 20)
model.con2 = Constraint(expr=model.x2 <= 40)
model.con3 = Constraint(expr=5 * model.x1 + 4 * model.x2 <= 200)
model.obj_list = ObjectiveList()
model.obj_list.add(expr=model.x1, sense=maximize)
model.obj_list.add(expr=3 * model.x1 + 4 * model.x2, sense=maximize)
return model
if __name__ == "__main__":
solver = PyAugmecon(two_objective_model(), PyAugmeconConfig())
result = solver.solve()
print(result.points)
print(result.payoff_table)
The if __name__ == "__main__" guard is intentional. PyAUGMECON starts worker processes with Python's spawn mode, which imports your script in each child process. The guard keeps child processes from running your top-level solve code again.
[!IMPORTANT] Always wrap your
PyAugmecon(...)/.solve()calls inif __name__ == "__main__"when usingworkers > 1, otherwise child processes will re-execute your script on import.
A more configured example using a bundled knapsack model, showing options that are available but optional in the minimal example above:
from pyaugmecon import PyAugmecon, PyAugmeconConfig
from pyaugmecon.example_models import kp_model
if __name__ == "__main__":
config = PyAugmeconConfig(
name="3kp40",
mode="exact",
solver_name="highs",
nadir_points=[1031, 1069],
store_decision_variables=True,
workers=4,
)
solver = PyAugmecon(kp_model("3kp40", 3), config)
result = solver.solve()
decision_vars = result.variables_for(result.points[0])
API
Constructor
PyAugmecon(model, config, *, log_sink=None) where:
model: unsolved PyomoConcreteModelwith anObjectiveListnamedobj_list.config: aPyAugmeconConfiginstance or a plain dict with the same fields.log_sink: optional object with.info(message)method for forwarding PyAUGMECON log messages to your own logging system.
Methods
| Method | Returns |
|---|---|
solve() |
Runs the algorithm and returns PyAugmeconResult. |
Result object
| Attribute | Description |
|---|---|
solutions |
Final Pareto solutions as Solution records. |
points |
Objective vectors from solutions. |
count |
Number of Pareto points. |
total_points |
Number of distinct objective vectors (after rounding) before Pareto filtering. |
runtime_seconds |
Wall-clock solve time. |
payoff_table |
Lexicographic payoff table. |
models_solved |
Number of subproblems solved. |
models_infeasible |
Number of infeasible subproblems. |
visited_points |
Number of grid points visited by workers. |
grid_point_count |
Planned grid point count. |
hypervolume() |
Hypervolume of the Pareto front. Computed lazily on first call. |
Each Solution record has:
point: the objective vector.variables: variable values for that point, orNonewhen decision storage is off.
Example result.solutions layout with store_decision_variables=True:
[
Solution(point=(3, 5), variables={"x": {0: 1, 1: 2}}),
Solution(point=(4, 4), variables={"x": {0: 3, 1: 4}}),
]
Config
PyAugmeconConfig is a Pydantic model. Pass it directly or as a plain dict.
| Field | Default | Description |
|---|---|---|
name |
"{solver_name}-{mode}" (auto) |
Run name for logs and artifacts. |
mode |
"exact" |
"exact" (full grid) or "sampled" (user-defined discretization). |
sample_points |
None |
Grid density per constrained objective. Required in sampled mode. |
nadir_points |
None |
Explicit nadir values for constrained objectives. Auto-computed if omitted. |
nadir_strategy |
"safe" |
How nadirs are auto-computed when nadir_points is omitted: "safe" or "payoff". |
nadir_undercut |
0.8 |
Widening factor used by nadir_strategy="payoff". Smaller values widen the grid more. |
objective_order |
"auto_range" |
"auto_range" (sort by range, descending) or "given". |
workers |
cpu_count() |
Number of worker processes. |
work_distribution |
"auto" |
How grid points are assigned to workers: "auto", "dynamic", "fixed", or "outer_grid". |
flag_policy |
"auto" |
Whether AUGMECON-R flag information is private to each worker ("local") or shared between workers ("shared"). |
process_timeout |
None |
Timeout in seconds for the entire run. |
solve_warmstart |
True |
Pass previous solution to solver when supported. |
store_decision_variables |
False |
Keep variable values with each Pareto point. |
early_exit |
True |
AUGMECON early exit: stop iterating when infeasible. |
bypass |
True |
AUGMECON2 bypass: skip non-binding constraint levels. |
flag |
True |
AUGMECON-R pruning: mark visited grid points to avoid re-solving. |
penalty_weight |
1e-3 |
Slack penalty multiplier in the augmented objective. |
objective_tolerance |
1e-6 |
Tolerance for deduplicating objective values. |
round_decimals |
9 |
Decimal precision for solution keys. |
solver_name |
"highs" |
Solver family or explicit Pyomo backend name. |
solver_io |
None |
Optional Pyomo backend hint. |
solver_options |
{} |
Options passed to the solver backend. |
write_csv |
True |
Write payoff, grid, and final solution tables as CSV files. |
artifact_folder |
"logs" |
Output directory for logs and CSV artifacts. |
artifact_name |
auto | Explicit artifact/log basename. Defaults to <name>_<timestamp>. |
progress_bar |
True |
Show progress output. |
log_to_console |
True |
Emit logs to stdout/stderr. |
process_logging |
False |
Enable per-worker logging. Reduces performance. |
Advanced work and pruning settings
Most users only need workers. Leave work_distribution="auto" and flag_policy="auto" unless you are benchmarking, comparing algorithm variants, or debugging parallel performance.
work_distribution controls how the flat epsilon grid is split:
"auto"selects"outer_grid"for exact multi-worker runs and"dynamic"otherwise."dynamic"uses one shared queue. A worker takes the next small range as soon as it finishes its current range. This is simple and handles uneven solve times well."fixed"gives each worker one continuous part of the grid. There is no work stealing, so it is useful for controlled benchmarks but can be slower when some points take longer to solve."outer_grid"groups points by the slower-changing constrained objectives: a worker gets blocks where the outer objective levels stay together while the innermost objective level changes fastest. This helps shared pruning because one infeasible solve can tell other workers to skip later inner levels for the same outer objective levels.
flag_policy controls who can see AUGMECON-R flag information:
"auto"selects"shared"for exact multi-worker runs and"local"otherwise."local"keeps skip information inside each worker. It has the least coordination overhead."shared"stores flag information in shared memory. Workers can avoid repeating solves that another worker already proved redundant. This is most useful withwork_distribution="outer_grid".
The skip controls are still separate because they are useful for benchmarks and research:
early_exit=Truestops scanning the innermost objective levels after an infeasible subproblem because later inner levels are at least as hard.bypass=Trueuses positive slack to skip nearby epsilon levels that lead to the same objective vector.flag=Truerecords skip counts so later grid points can be skipped before calling the solver.
Nadir computation
The lower bound of the epsilon grid for each constrained objective (its "nadir") sets how wide the search has to be. You can supply explicit values via nadir_points; otherwise PyAUGMECON computes them with one of two strategies:
nadir_strategy="safe"(default) solves one extra single-objective minimization per constrained objective over the entire feasible region. The result is a provably safe lower bound on the true nadir, so no Pareto-optimal point can fall below the grid. It costsn - 1extra setup solves and can produce a wider grid than strictly needed; AUGMECON-R bypass usually skips the slack cells without solver calls.nadir_strategy="payoff"reuses the lex payoff table column minima and widens them bynadir_undercut. This is the classic AUGMECON / AUGMECON-R convention. It adds no setup solves and yields a tighter grid, but the column minimum is the anti-ideal point, which can be larger than the true nadir for three or more objectives. Thenadir_undercutfactor (default0.8) is a safety margin that pushes the bound down; lower values widen the grid,1.0disables widening.
Use "safe" when correctness must be guaranteed without tuning. Use "payoff" to reproduce literature results or when the extra setup solves are expensive (e.g. large MIPs where the relaxation is hard).
Solver fallback order
When you specify a solver family, PyAUGMECON tries backends in order until one is available:
| Family | Tried in order |
|---|---|
highs |
appsi_highs |
gurobi |
gurobi_direct, appsi_gurobi, gurobi_persistent, gurobi |
cplex |
appsi_cplex, cplex_persistent, cplex |
xpress |
xpress_direct, xpress_persistent, xpress |
cbc |
cbc, appsi_cbc |
scip |
scip |
You can also pass an explicit backend name (e.g. solver_name="appsi_highs").
The order favors in-process Pyomo backends first because PyAUGMECON solves many closely related models and avoids command-line startup cost when possible. HiGHS tries appsi_highs first because the highspy Python backend (installed via pyaugmecon[highs]) is in-process. Commercial solver families prefer direct/APPSI-style Python interfaces, then persistent interfaces, then the broad command-line backend. If you need one exact backend for a benchmark or deployment, pass that backend name directly.
Solver notes
gurobiandxpressextras add their Python bindings, but you still need a working license/setup.- System-installed CBC may be slower than
cbcboxdue to compiler and BLAS optimizations. - SCIP and CPLEX need a solver binary installed separately (see installation section above).
Free/community solver options change over time, so check vendor docs before relying on them:
| Solver | Free/community option |
|---|---|
| HiGHS | Open-source solver, no commercial license needed. |
| CBC | Open-source solver from COIN-OR, no commercial license needed. |
| SCIP | Free for academic and non-commercial use. See SCIP licensing. |
| Gurobi | Restricted non-production use and academic licenses are available. See restricted license notes and academic licenses. |
| CPLEX | A limited free edition and academic access are available. See IBM CPLEX pricing FAQ. |
| XPRESS | Community licensing is available through the Python package. See xpress on PyPI. |
Solver parameter references: HiGHS | Gurobi | CPLEX | XPRESS | CBC | SCIP
Benchmarks
See benchmarks/README.md for details.
uv run python -m benchmarks --profile quick
Tests
Most tests require HiGHS (uv sync --extra highs).
uv run pytest # default suite (skips slow cases)
uv run pytest -m "" # full suite
uv run pytest -m knapsack # knapsack regression only
Knapsack datasets are packaged inside pyaugmecon.data and used directly by tests and benchmarks.
Notes
[!NOTE] In exact mode, the grid is derived from the payoff table and covers the full objective range. In sampled mode,
sample_pointscontrols discretization density.
[!TIP] With multi-process runs, lowering solver-internal thread counts (e.g.
solver_options={"threads": 1}) can improve total throughput.
- The choice of constrained objectives affects grid traversal order but not the Pareto front.
- Small instances can be slower with
workers > 1because process startup overhead dominates. flag_policy="shared"uses inter-process shared memory for pruning state. This adds coordination overhead but can avoid redundant solves across workers.
Development
git clone https://github.com/wouterbles/pyaugmecon.git
cd pyaugmecon
uv sync
uv sync installs the package in editable mode with all dev dependencies. Add --extra highs for the solver (required by integration tests and benchmarks):
uv sync --extra highs
Then run any of:
uv run ruff check . # lint
uv run ruff format --check . # format check
uv run ty check src/pyaugmecon # type check
uv run pytest # tests
Pre-commit hooks
Hooks are defined in prek.toml. Install prek once, then enable hooks for this repo:
prek install
prek run --all-files
Why uv?
uv handles Python version management, virtual environments, and dependency resolution in a single tool. It is significantly faster than pip and avoids common pitfalls with environment isolation. The project's pyproject.toml and lockfile are designed for uv, but pip install -e . works too.
References
AUGMECON
- G. Mavrotas, "Effective implementation of the epsilon-constraint method in Multi-Objective Mathematical Programming problems," Applied Mathematics and Computation, 213(2), 2009.
- GAMS implementation
AUGMECON2
- K. Florios and G. Mavrotas, "An improved version of the augmented epsilon-constraint method (AUGMECON2)," Applied Mathematics and Computation, 219(18), 2013.
- GAMS implementation
AUGMECON-R
- A. Nikas, A. Fountoulakis, A. Forouli, and H. Doukas, A robust augmented epsilon-constraint method (AUGMECON-R) for finding exact solutions of multi-objective linear programming problems, 2020.
- Reference implementation
Other
Changelog
See CHANGELOG.md.
Citing
If you use PyAUGMECON in academic work, please cite the Zenodo DOI.
Credit
Developed at the Electricity Markets & Power System Optimization Laboratory (EMPSOLab), Electrical Energy Systems Group, Department of Electrical Engineering, Eindhoven University of Technology.
Contributors: Wouter Bles, Nikolaos Paterakis
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyaugmecon-2.0.1.tar.gz.
File metadata
- Download URL: pyaugmecon-2.0.1.tar.gz
- Upload date:
- Size: 198.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a749ef6bc67c4e8fbb86a374528ec261219de529a5f8366d8c7f455a49271d8
|
|
| MD5 |
898d1f5ee547268665c411d97cf25a7e
|
|
| BLAKE2b-256 |
0c34bf88cb853947a11aded869a89a1c2cc05c1afb0e293f40332bbfd2021d14
|
File details
Details for the file pyaugmecon-2.0.1-py3-none-any.whl.
File metadata
- Download URL: pyaugmecon-2.0.1-py3-none-any.whl
- Upload date:
- Size: 120.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7efd9290c859449c2efbbc7d939bcd61a0d36334d405990c9aad3d9e488cb871
|
|
| MD5 |
7df902a343b55b2c5f9e61dd2ea0e090
|
|
| BLAKE2b-256 |
4a1402df9a8b4b13ba0f4d48d758761d83d3eb60acef8d8e7f01179d64e4e5a8
|