Handling AMPL Errors and Warnings: ErrorHandler in amplpy#
Description: Shows how to use amplpy.ErrorHandler and amplpy.AMPLException to intercept, filter, and route AMPL errors and warnings — covering default raise behaviour, custom recovery strategies, and logging integration.
Tags: amplpy, api, error-handler, exceptions, logging, highlights
Notebook author: Jürgen Lentz <lentz@ampl.com>
By default amplpy prints every error and warning that the AMPL interpreter reports to stdout, and then raises it in Python as an amplpy.AMPLException. That is convenient while developing interactively, but in a larger application you usually want more control:
keep a bad
eval()call from crashing the whole process,tell errors and warnings apart and only fail on real errors,
collect every problem found during a batch validation run instead of stopping at the first one, or
route errors and warnings to a logger instead of stdout.
amplpy exposes two building blocks for all of these use cases:
Symbol |
Role |
|---|---|
|
Base class (or duck-typed interface) — implement |
|
Exception AMPL reports; carries the message, source, line and offset |
|
Register |
|
Retrieve the currently active handler |
This notebook walks through the interface and then shows four practical handler patterns.
# Install dependencies
%pip install -q amplpy
# Google Colab & Kaggle integration
from amplpy import AMPL, ampl_notebook
ampl = ampl_notebook(
modules=["highs"], # modules to install
license_uuid="default", # license to use
) # instantiate AMPL object and register magics
Running example: the diet problem#
The solving examples below reuse the classic diet problem. The helper builds a fresh AMPL instance with model and data already loaded. The error and warning examples use small standalone eval() snippets instead, so each one is self-contained and the mistake is obvious.
import sys
import logging
import pandas as pd
import numpy as np
import amplpy
from amplpy import AMPL, AMPLException
foods = ["BEEF", "CHK", "FISH", "HAM", "MCH", "MTL", "SPG", "TUR"]
nutrs = ["A", "B1", "B2", "C"]
food_df = pd.DataFrame(
{
"cost": [3.19, 2.59, 2.29, 2.89, 1.89, 1.99, 1.99, 2.49],
"f_min": [0] * 8,
"f_max": [100] * 8,
},
index=pd.Index(foods, name="FOOD"),
)
nutr_df = pd.DataFrame(
{"n_min": [700] * 4, "n_max": [10000] * 4},
index=pd.Index(nutrs, name="NUTR"),
)
amt_df = pd.DataFrame(
np.array(
[
[60, 8, 8, 40, 15, 70, 25, 60],
[10, 20, 15, 35, 15, 15, 25, 15],
[15, 20, 10, 10, 15, 15, 15, 10],
[20, 0, 10, 40, 35, 30, 50, 20],
]
),
index=pd.Index(nutrs, name="NUTR"),
columns=pd.Index(foods, name="FOOD"),
)
_MODEL = """
set FOOD;
set NUTR;
param cost {FOOD} > 0;
param f_min {FOOD} >= 0;
param f_max {j in FOOD} >= f_min[j];
param n_min {NUTR} >= 0;
param n_max {i in NUTR} >= n_min[i];
param amt {NUTR, FOOD} >= 0;
var Buy {j in FOOD} >= f_min[j], <= f_max[j];
minimize Total_Cost: sum {j in FOOD} cost[j] * Buy[j];
subject to Diet {i in NUTR}:
n_min[i] <= sum {j in FOOD} amt[i,j] * Buy[j] <= n_max[i];
"""
def make_ampl():
"""Return a fresh AMPL instance with the diet model and data loaded."""
a = AMPL()
a.eval(_MODEL)
a.set["FOOD"] = foods
a.set["NUTR"] = nutrs
a.set_data(food_df, "FOOD")
a.set_data(nutr_df, "NUTR")
a.param["amt"] = amt_df
return a
Default behaviour#
Without a custom handler, amplpy uses a built-in ErrorHandler that prints every error and warning to stdout (prefixed [ERROR] or [WARNING]) and also raises the corresponding amplpy.AMPLException in Python — for warnings just as much as for genuine errors.
ampl = AMPL()
try:
ampl.eval("var x >= undeclared_param;") # AMPL only considers this a warning
except AMPLException as e:
print("Raised even though AMPL only reported a warning:")
print(e)
[WARNING]
line 1 offset 9
undeclared_param is not defined
context: var x >= >>> undeclared_param; <<<
Raised even though AMPL only reported a warning:
line 1 offset 9
undeclared_param is not defined
context: var x >= >>> undeclared_param; <<<
Anatomy of an AMPLException#
Every error and warning arrives as an amplpy.AMPLException with four pieces of information:
Accessor |
Meaning |
|---|---|
|
The human-readable message (same text AMPL prints) |
|
Name of the file being read, empty for |
|
Line where the problem was detected |
|
Character offset on that line |
ampl = AMPL()
try:
ampl.eval("var x >= undeclared_param;")
except AMPLException as e:
print("message: ", e.get_message().splitlines()[0])
print("source name: ", repr(e.get_source_name()))
print("line number: ", e.get_line_number())
print("offset: ", e.get_offset())
[WARNING]
line 1 offset 9
undeclared_param is not defined
context: var x >= >>> undeclared_param; <<<
message: undeclared_param is not defined
source name: '-'
line number: 1
offset: 9
The ErrorHandler interface#
Register a custom handler by passing any object with error(exception) and warning(exception) methods to ampl.set_error_handler(). The simplest approach is to subclass amplpy.ErrorHandler:
class MyHandler(amplpy.ErrorHandler):
def error(self, exception: amplpy.AMPLException) -> None:
...
def warning(self, exception: amplpy.AMPLException) -> None:
...
Important: once a custom handler is registered, amplpy stops raising automatically. error() and warning() are called instead, and it is entirely up to the handler whether to re-raise, log, collect, or silently ignore the problem.
class PrintingHandler(amplpy.ErrorHandler):
def error(self, exception):
print("ERROR: ", exception.get_message().splitlines()[0])
def warning(self, exception):
print("WARNING:", exception.get_message().splitlines()[0])
ampl = AMPL()
ampl.set_error_handler(PrintingHandler())
ampl.eval("var x >= undeclared_param;") # warning goes to the handler, nothing is raised
print("Execution continued after the warning.")
WARNING: undeclared_param is not defined
Execution continued after the warning.
Pattern 1 — Collecting errors and warnings#
The most general pattern: store every AMPLException in a list instead of acting on it immediately. Useful when validating a batch of models or data files and you want a full report at the end rather than stopping at the first problem.
class CollectingHandler(amplpy.ErrorHandler):
"""Accumulates every error and warning instead of raising or printing."""
def __init__(self):
self.errors = []
self.warnings = []
def error(self, exception):
self.errors.append(exception)
def warning(self, exception):
self.warnings.append(exception)
ampl = AMPL()
handler = CollectingHandler()
ampl.set_error_handler(handler)
ampl.eval("var x >= undeclared_param;") # warning
ampl.eval("param p := 1; param p := 2;") # error
ampl.eval("var y >= 0;") # clean statement, nothing reported
print(f"{len(handler.errors)} error(s), {len(handler.warnings)} warning(s) collected")
for e in handler.errors:
print("ERROR: ", e.get_message().splitlines()[0])
for w in handler.warnings:
print("WARNING:", w.get_message().splitlines()[0])
1 error(s), 1 warning(s) collected
ERROR: p is already defined
WARNING: undeclared_param is not defined
Pattern 2 — Fail on errors, log warnings#
A common middle ground: treat warnings as non-fatal (log and continue) but immediately abort on real errors by re-raising the exception from inside error().
class StrictHandler(amplpy.ErrorHandler):
"""Re-raises errors; only logs warnings."""
def error(self, exception):
raise exception
def warning(self, exception):
print("Ignoring warning:", exception.get_message().splitlines()[0])
ampl = AMPL()
ampl.set_error_handler(StrictHandler())
ampl.eval("var x >= undeclared_param;") # warning: logged, execution continues
try:
ampl.eval("param p := 1; param p := 2;") # error: re-raised
except AMPLException as e:
print("Stopped on error:", e.get_message().splitlines()[0])
Ignoring warning: undeclared_param is not defined
Stopped on error: p is already defined
Pattern 3 — Errors during solve()#
Not every problem is reported through eval(). Errors raised while the solver is running — e.g. a numeric error while evaluating a constraint — go through the same ErrorHandler, but solve() does not raise, even with the default handler. You still need to check ampl.solve_result afterwards.
class SolveErrorHandler(amplpy.ErrorHandler):
def error(self, exception):
print("Solve error:")
print(exception.get_message())
def warning(self, exception):
print("Solve warning:", exception.get_message().splitlines()[0])
ampl = AMPL()
ampl.set_error_handler(SolveErrorHandler())
ampl.eval("param p := 0; var x >= 0; minimize z: x; subject to c1: x >= 1 / p;")
ampl.solve(solver="highs") # division by zero while evaluating c1 -- no exception here
print("solve_result:", repr(ampl.solve_result))
assert ampl.solve_result != "solved"
Solve error:
Error executing "solve" command:
error processing constraint c1:
can't compute 1/0
For support/feedback go to https://discuss.ampl.com or e-mail <support@ampl.com>
solve_result: '?'
Pattern 4 — Routing to the Python logging module#
When AMPL is embedded in a larger application that already uses Python’s logging module, route errors and warnings through the same logger — errors at ERROR level, warnings at WARNING level.
logging.basicConfig(
level=logging.DEBUG,
format="%(levelname)-8s [%(name)s] %(message)s",
stream=sys.stdout,
force=True,
)
logger = logging.getLogger("ampl")
class LoggingHandler(amplpy.ErrorHandler):
def error(self, exception):
logger.error(exception.get_message().splitlines()[0])
def warning(self, exception):
logger.warning(exception.get_message().splitlines()[0])
ampl = AMPL()
ampl.set_error_handler(LoggingHandler())
ampl.eval("var x >= undeclared_param;")
ampl.eval("param p := 1; param p := 2;")
WARNING [ampl] undeclared_param is not defined
ERROR [ampl] p is already defined
Saving and restoring the active handler#
ampl.get_error_handler() returns the currently active handler, so you can save it and restore it later. This is useful when you only want custom error handling for a specific block of code, such as validating user-supplied data before running the real model.
ampl = make_ampl()
default_handler = ampl.get_error_handler()
print("Default handler type:", type(default_handler).__name__)
# Temporarily collect problems instead of raising, e.g. while validating input
ampl.set_error_handler(CollectingHandler())
ampl.eval("var stray >= undeclared_param;") # collected, not raised
print("Collected without raising.")
# Restore the default handler
ampl.set_error_handler(default_handler)
try:
ampl.eval("var stray2 >= another_undeclared_param;") # raised again
except AMPLException as e:
print("Back to raising by default:", e.get_message().splitlines()[0])
ampl.solve(solver="highs")
print(f"Optimal cost: ${ampl.obj['Total_Cost'].value():.2f}")
[WARNING]
line 1 offset 14
another_undeclared_param is not defined
context: var stray2 >= >>> another_undeclared_param; <<<
Default handler type: ErrorHandler
Collected without raising.
Back to raising by default: another_undeclared_param is not defined
HiGHS 1.7.1:
HiGHS 1.7.1: optimal solution; objective 88.2
1 simplex iterations
0 barrier iterations
Optimal cost: $88.20
Summary#
Pattern |
Handler skeleton |
Typical use |
|---|---|---|
Collect all |
store every |
batch validation, testing |
Fail on errors, log warnings |
|
strict mode with graceful degradation |
Solve-time errors |
handler receives the exception, but |
robust solve loops |
Route by severity |
dispatch |
production application integration |
Key points:
Register with
ampl.set_error_handler(handler)and retrieve the current one withampl.get_error_handler().Inherit from
amplpy.ErrorHandleror simply provide any object witherror(exception)andwarning(exception)methods.Without a custom handler, amplpy prints and raises
amplpy.AMPLExceptionfor both errors and warnings.With a custom handler, amplpy no longer raises automatically — call
raise exceptioninsideerror()/warning()yourself if you want that behaviour back.AMPLExceptioncarriesget_message(),get_source_name(),get_line_number(), andget_offset().Errors reported while
solve()is running do not raise, even with the default handler — always checkampl.solve_result.ErrorHandlerdoes not receive AMPL’s ordinary output (display, the solver log, etc.); useamplpy.OutputHandlerfor that — see the companion notebookoutput_handler.ipynb.