mlopter 0.1.4
Machine-learning enhanced optimization library with pluggable solvers
Requires Python: >=3.9

# mlopt

Machine-learning enhanced optimization with a clean modeling API, pluggable solvers (CBC/HiGHS/GA/Bayesian), and learning-augmented features. Model variables, constraints, and objectives in Python, then solve with mathematical programming or black-box global optimizers. Add surrogate models, pass learning hints, and scale from notebooks to CI/CD and PyPI.

- Friendly modeling API inspired by LP/MIP tools
- Multiple backends: CBC via PuLP, HiGHS via PuLP, Genetic Algorithm, Bayesian Optimization
- ML-first utilities: Gaussian-process surrogates, learning-augmented solver hints
- Robust validation and logging for safe, observable runs
- Extensible plugin registry for custom solvers

## Install

Prerequisites:
- Python 3.9+
- For LP/MIP: install PuLP. For Bayesian Opt: scikit-optimize. For surrogates: scikit-learn.

Examples:
- Core + CBC via PuLP:
  - pip install pulp
- Bayesian optimization:
  - pip install scikit-optimize
- Surrogates:
  - pip install scikit-learn

Local editable install:
- pip install -e .

Optional developer tools:
- pip install pytest ruff mypy

## Quick start

Linear program with CBC:

```python
from mlopt.core.model import Model
from mlopt.core.objective import Objective
from mlopt.core.constraints import Constraint
from mlopt.plugins.registry import SolverRegistry

m = Model("diet_like")

x1 = m.add_var("x1", low=0)
x2 = m.add_var("x2", low=0)

obj = m.linexpr({x1: 3.0, x2: 2.0})
m.set_objective(Objective(obj, "min"))

m.add_constraint(Constraint(m.linexpr({x1: 2.0, x2: 1.0}), "<=", 100, "c1"))
m.add_constraint(Constraint(m.linexpr({x1: 1.0, x2: 1.0}), ">=", 50, "c2"))

solver = SolverRegistry.create("pulp_cbc")
m.set_solver(solver)
res = m.solve()

print(res.status, res.obj, res.x, res.solver_name)
```

Bayesian optimization for a black-box:

```python
from typing import Dict
from mlopt.core.model import Model
from mlopt.core.objective import Objective
from mlopt.core.constraints import Constraint
from mlopt.plugins.registry import SolverRegistry

def black_box(a: Dict[str, float]) -> float:
    # (x1 - 2)^2 + (x2 - 3)^2
    return (a["x1"] - 2.0) ** 2 + (a["x2"] - 3.0) ** 2

m = Model("black_box")
m.add_var("x1", low=0.0, up=5.0)
m.add_var("x2", low=0.0, up=5.0)
m.set_objective(Objective(m.linexpr({}, 0.0), "min"))
m.add_constraint(Constraint(m.linexpr({}, 0.0), ">=", 0.0, "noop"))

solver = SolverRegistry.create("bayesopt", n_calls=25, random_state=42)
# If callable API available:
try:
    res = solver.solve_with_callable(m, black_box)  # type: ignore[attr-defined]
except Exception:
    setattr(m, "_black_box", black_box)
    res = solver.solve(m)

print(res.status, res.obj, res.x)
```

Genetic algorithm (heuristic):

```python
from mlopt.core.model import Model
from mlopt.core.objective import Objective
from mlopt.core.constraints import Constraint
from mlopt.plugins.registry import SolverRegistry

m = Model("ga_example")

x = m.add_var("x", low=0, up=100)
y = m.add_var("y", low=0, up=100)

m.set_objective(Objective(m.linexpr({x: 1.0, y: 2.0}), "min"))
m.add_constraint(Constraint(m.linexpr({x: 1.0, y: 1.0}), ">=", 10, "c1"))

solver = SolverRegistry.create("genetic", pop_size=60, generations=120, seed=123)
m.set_solver(solver)
res = m.solve()
print(res.status, res.obj, res.x)
```

## Concepts

- Variables: name, bounds, category (Continuous, Integer, Binary)
- Linear expressions: sum of coefficients times variables plus constant
- Constraints: LinExpr with sense in {<=, >=, ==} and RHS
- Objective: LinExpr with direction in {min, max}
- Model: container for vars, constraints, objective, and a solver
- SolveResult: status, objective value, solution map, solver name

## Public API

Convenience imports:

```python
from mlopt import (
  Model, Var, LinExpr, Constraint, Objective,
  SolveResult, SolverRegistry,
  PuLPCBC, GeneticSolver, BayesianOpt,
  SurrogateModel, LearningHints,
)
```

Subpackages:
- core: modeling primitives
- solvers: backends (CBC, HiGHS, GA, Bayesian)
- ml: surrogates and hints
- plugins: registry
- utils: logging and validation

## Solvers

- CBC via PuLP (pulp_cbc)
  - Requires pulp
  - Deterministic LP/MIP solver, good defaults
- HiGHS via PuLP (highs)
  - Requires PuLP with HiGHS support
  - Modern LP/MIP solver; adapter selects HiGHS_CMD/Highs_CMD automatically
- GeneticSolver (meta_ga)
  - Population-based heuristic for nonconvex/black-box; supports seed and early stopping
- BayesianOpt (bayesopt)
  - Global optimization with Gaussian Process surrogate
  - Accepts black-box via model._black_box or direct callable API (if available)
  - Requires scikit-optimize; respects variable bounds and adds constraint penalties

To create solvers:

```python
solver = SolverRegistry.create("pulp_cbc")
# or
solver = SolverRegistry.create("highs", msg=False)
# or
solver = SolverRegistry.create("genetic", pop_size=50, generations=100, seed=42)
# or
solver = SolverRegistry.create("bayesopt", n_calls=30, random_state=42)
```

## ML features

- SurrogateModel (Gaussian Process)
  - fit(X, y), predict(X, return_std=False), reproducible with random_state
  - Useful for custom Bayesian/global loops outside the built-in solver
- LearningHints
  - var_priorities: branching priorities by name
  - var_fixings: proposed fixings (e.g., binary 0/1)
  - cut_hints: numeric cut thresholds
  - to_solver_kwargs() returns normalized dict usable by adapters

Example usage:

```python
from mlopt.ml.surrogate import SurrogateModel
from mlopt.ml.learning_hints import LearningHints

sur = SurrogateModel(random_state=42)
# sur.fit(X, y), sur.predict(X, return_std=True)

hints = LearningHints(var_priorities={"x": 10}, var_fixings={"y": 0})
kwargs = hints.to_solver_kwargs()
# Pass kwargs to adapters that support hint ingestion
```

## Validation and logging

Validation:
- validate_readiness_for_solver(model) checks:
  - Objective set, variables present, senses valid
  - Bounds consistency, duplicate names
  - Expressions reference declared variables

Logging:
- get_logger(__name__) creates a namespaced logger
- Environment variable MLOPT_LOG_LEVEL controls default level (e.g., DEBUG, INFO)

```python
from mlopt.utils.validation import validate_readiness_for_solver, summary
from mlopt.utils.logging import get_logger, enable_debug

logger = get_logger(__name__)
enable_debug()

validate_readiness_for_solver(m)
logger.info(summary(m))
res = m.solve()
logger.info("Solved: %s obj=%.6f", res.status, res.obj)
```

## Examples

- examples/lp_example.py: basic LP with CBC
- examples/bayes_example.py: black-box optimization with BayesianOpt

Run:
- python examples/lp_example.py
- python examples/bayes_example.py

## Extending

Add a new solver:
- Implement a class conforming to SolverBase with a solve(model) method returning SolveResult
- Register it in plugins/registry.py:
  - SolverRegistry.register("my_solver", MySolver)

Support nonlinear or convex modeling:
- Add new expression/objective types and adapters that translate to the target solver’s API
- Keep the core API backward compatible

Learning-augmented strategies:
- Log solve traces, train models to predict branching priorities or variable fixings
- Use LearningHints to pass them into adapters that support hints

## Project structure

- mlopt/
  - core/: model, variables, expressions, constraints, objective
  - solvers/: base, pulp_cbc, highs, meta_ga, bayesopt
  - ml/: surrogate, learning_hints
  - plugins/: registry
  - utils/: logging, validation
- examples/: lp_example.py, bayes_example.py
- tests/: add unit tests for APIs and solvers
- README.md, pyproject.toml, LICENSE

## Development

Setup:
- python -m venv .venv && source .venv/bin/activate  # on Windows: .venv\Scripts\activate
- pip install -e .
- pip install pytest ruff mypy

Quality:
- ruff check .
- mypy mlopt
- pytest

Conventional commits and PR guidance:
- feat:, fix:, docs:, refactor:, test:, chore:
- Include tests and update README/docs for user-facing changes

## Release

- Update version in pyproject.toml
- Build:
  - python -m pip install --upgrade build twine
  - python -m build
  - twine check dist/*
- Publish:
  - twine upload dist/*  # or use your CI workflow on tag

## Troubleshooting

- Type checker error “Variable not allowed in type expression”:
  - Avoid annotating with runtime classes from external libs; prefer typing.Any or checker-only aliases.
- CBC/HiGHS not available:
  - Ensure PuLP is installed and supports the chosen command (PULP_CBC_CMD, HiGHS_CMD/Highs_CMD).
- Bayesian optimization import error:
  - Install scikit-optimize. If using surrogates, also install scikit-learn.
- Black-box objective not used:
  - Use solver.solve_with_callable(model, fn) if available, or set model._black_box = fn before solve.

## License

Apache-2.0