clara-opt 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clara_opt-0.1.0/.gitignore +68 -0
- clara_opt-0.1.0/CONTRIBUTING.md +41 -0
- clara_opt-0.1.0/LICENSE +21 -0
- clara_opt-0.1.0/PKG-INFO +123 -0
- clara_opt-0.1.0/README.md +89 -0
- clara_opt-0.1.0/benchmarks/results/final/.gitkeep +0 -0
- clara_opt-0.1.0/benchmarks/scripts/.gitkeep +0 -0
- clara_opt-0.1.0/clara/__init__.py +3 -0
- clara_opt-0.1.0/clara/cli.py +198 -0
- clara_opt-0.1.0/clara/engine/__init__.py +1 -0
- clara_opt-0.1.0/clara/engine/highs_backend.py +198 -0
- clara_opt-0.1.0/clara/engine/simplex.py +399 -0
- clara_opt-0.1.0/clara/explain/__init__.py +4 -0
- clara_opt-0.1.0/clara/explain/binding.py +77 -0
- clara_opt-0.1.0/clara/explain/explainer.py +127 -0
- clara_opt-0.1.0/clara/explain/sensitivity.py +129 -0
- clara_opt-0.1.0/clara/explain/types.py +315 -0
- clara_opt-0.1.0/clara/explain/variable.py +92 -0
- clara_opt-0.1.0/clara/io/__init__.py +3 -0
- clara_opt-0.1.0/clara/io/lp_parser.py +702 -0
- clara_opt-0.1.0/clara/model/__init__.py +13 -0
- clara_opt-0.1.0/clara/model/problem.py +46 -0
- clara_opt-0.1.0/clara/model/solve_state.py +332 -0
- clara_opt-0.1.0/clara/reopt/__init__.py +8 -0
- clara_opt-0.1.0/clara/reopt/types.py +129 -0
- clara_opt-0.1.0/docs/.gitkeep +0 -0
- clara_opt-0.1.0/docs/cli_spec.py +452 -0
- clara_opt-0.1.0/docs/explainer_spec.py +688 -0
- clara_opt-0.1.0/docs/lp_parser_spec.py +398 -0
- clara_opt-0.1.0/docs/solve_state_design.py +541 -0
- clara_opt-0.1.0/docs/v010_release_spec.py +571 -0
- clara_opt-0.1.0/examples/01_basic_lp.py +21 -0
- clara_opt-0.1.0/examples/02_sensitivity.py +33 -0
- clara_opt-0.1.0/examples/03_engine_comparison.py +37 -0
- clara_opt-0.1.0/examples/04_json_output.py +28 -0
- clara_opt-0.1.0/paper/Makefile +14 -0
- clara_opt-0.1.0/paper/figures/.gitkeep +0 -0
- clara_opt-0.1.0/paper/latexmkrc +3 -0
- clara_opt-0.1.0/paper/sections/.gitkeep +0 -0
- clara_opt-0.1.0/paper/tables/.gitkeep +0 -0
- clara_opt-0.1.0/pyproject.toml +119 -0
- clara_opt-0.1.0/tests/__init__.py +0 -0
- clara_opt-0.1.0/tests/fixtures/.gitkeep +0 -0
- clara_opt-0.1.0/tests/fixtures/albici_base.lp +20 -0
- clara_opt-0.1.0/tests/fixtures/albici_expected.py +216 -0
- clara_opt-0.1.0/tests/fixtures/parser_comprehensive.lp +33 -0
- clara_opt-0.1.0/tests/fixtures/parser_expected.py +96 -0
- clara_opt-0.1.0/tests/fixtures/parser_minimal.lp +7 -0
- clara_opt-0.1.0/tests/fixtures/test_albici.py +418 -0
- clara_opt-0.1.0/tests/test_cli.py +215 -0
- clara_opt-0.1.0/tests/test_cross_validation.py +116 -0
- clara_opt-0.1.0/tests/test_explainer.py +298 -0
- clara_opt-0.1.0/tests/test_lp_parser.py +332 -0
- clara_opt-0.1.0/tests/test_simplex.py +271 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Claude
|
|
2
|
+
CLAUDE.md
|
|
3
|
+
.claude/
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*$py.class
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
dist/
|
|
12
|
+
build/
|
|
13
|
+
.eggs/
|
|
14
|
+
*.so
|
|
15
|
+
*.whl
|
|
16
|
+
pip-log.txt
|
|
17
|
+
pip-delete-this-directory.txt
|
|
18
|
+
|
|
19
|
+
# Virtual environments
|
|
20
|
+
.venv/
|
|
21
|
+
venv/
|
|
22
|
+
env/
|
|
23
|
+
|
|
24
|
+
# IDE
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
*.swp
|
|
28
|
+
*.swo
|
|
29
|
+
*~
|
|
30
|
+
|
|
31
|
+
# Testing
|
|
32
|
+
.pytest_cache/
|
|
33
|
+
.coverage
|
|
34
|
+
htmlcov/
|
|
35
|
+
.hypothesis/
|
|
36
|
+
|
|
37
|
+
# Type checking
|
|
38
|
+
.mypy_cache/
|
|
39
|
+
|
|
40
|
+
# Ruff
|
|
41
|
+
.ruff_cache/
|
|
42
|
+
|
|
43
|
+
# LaTeX (xelatex)
|
|
44
|
+
*.aux
|
|
45
|
+
*.bbl
|
|
46
|
+
*.bcf
|
|
47
|
+
*.blg
|
|
48
|
+
*.fdb_latexmk
|
|
49
|
+
*.fls
|
|
50
|
+
*.log
|
|
51
|
+
*.out
|
|
52
|
+
*.run.xml
|
|
53
|
+
*.synctex.gz
|
|
54
|
+
*.toc
|
|
55
|
+
*.lof
|
|
56
|
+
*.lot
|
|
57
|
+
*.nav
|
|
58
|
+
*.snm
|
|
59
|
+
*.vrb
|
|
60
|
+
*.xdv
|
|
61
|
+
|
|
62
|
+
# Benchmark results (keep only final/)
|
|
63
|
+
benchmarks/results/*
|
|
64
|
+
!benchmarks/results/final/
|
|
65
|
+
|
|
66
|
+
# OS
|
|
67
|
+
.DS_Store
|
|
68
|
+
Thumbs.db
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Contributing to CLARA
|
|
2
|
+
|
|
3
|
+
## Development Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git clone https://github.com/yoonsik-jung-opt/clara-opt.git
|
|
7
|
+
cd clara-opt
|
|
8
|
+
python3.10 -m venv .venv
|
|
9
|
+
source .venv/bin/activate
|
|
10
|
+
pip install -e ".[dev]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Code Style
|
|
14
|
+
|
|
15
|
+
- Python 3.10+, type hints required on all public functions
|
|
16
|
+
- Format: `ruff format clara/ tests/`
|
|
17
|
+
- Lint: `ruff check clara/ tests/`
|
|
18
|
+
- Tests: `pytest tests/`
|
|
19
|
+
|
|
20
|
+
## Branch Convention
|
|
21
|
+
|
|
22
|
+
- `feat/*` — new features
|
|
23
|
+
- `fix/*` — bug fixes
|
|
24
|
+
- `bench/*` — benchmarks
|
|
25
|
+
- `paper/*` — paper writing
|
|
26
|
+
- All PRs target `dev` branch
|
|
27
|
+
|
|
28
|
+
## Commit Convention
|
|
29
|
+
|
|
30
|
+
`feat:`, `fix:`, `test:`, `bench:`, `paper:`, `docs:`, `refactor:`, `ci:`, `chore:`
|
|
31
|
+
|
|
32
|
+
## Testing
|
|
33
|
+
|
|
34
|
+
Every new feature must include tests. Run the full suite before submitting:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pytest tests/ -v
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Cross-validation tests compare Internal Simplex vs HiGHS — both engines
|
|
41
|
+
must agree on optimal values, duals, and sensitivity ranges.
|
clara_opt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yoonsik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
clara_opt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clara-opt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A white-box optimization solver that explains why solutions are optimal, when reoptimization is needed, and what changed.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yoonsikyang/clara-opt
|
|
6
|
+
Project-URL: Repository, https://github.com/yoonsikyang/clara-opt
|
|
7
|
+
Project-URL: Issues, https://github.com/yoonsikyang/clara-opt/issues
|
|
8
|
+
Author: Yoonsik Yang
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: explainability,linear-programming,operations-research,optimization,reoptimization,sensitivity-analysis
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: click>=8.0
|
|
26
|
+
Requires-Dist: highspy>=1.7
|
|
27
|
+
Requires-Dist: numpy>=1.24
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# CLARA — Classical LP Analysis for Reoptimization and Attribution
|
|
36
|
+
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
[](https://python.org)
|
|
39
|
+
|
|
40
|
+
A white-box optimization solver that explains why solutions are optimal,
|
|
41
|
+
when reoptimization is needed, and what changed.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Solver-intrinsic explanations** — not post-hoc ML surrogate, but direct
|
|
46
|
+
interpretation of basis, dual values, and sensitivity ranges
|
|
47
|
+
- **Dual backend** — Internal Revised Simplex (full transparency, step-by-step trace)
|
|
48
|
+
or HiGHS (performance for large problems)
|
|
49
|
+
- **Three explanation levels** mapped to the XAIOR framework (De Bock et al., 2024):
|
|
50
|
+
- *Understandability*: Which constraints are binding? What resources are fully used?
|
|
51
|
+
- *Justifiability*: Why is each variable at this value? What's the reduced cost?
|
|
52
|
+
- *Actionability*: What can change without breaking the solution? Where to invest?
|
|
53
|
+
- **CLI and Python API** — `clara explain problem.lp` or import as library
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install clara-opt
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For development:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/yoonsik-jung-opt/clara-opt.git
|
|
65
|
+
cd clara-opt
|
|
66
|
+
pip install -e ".[dev]"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Explain a production planning problem
|
|
73
|
+
clara explain examples/production.lp
|
|
74
|
+
|
|
75
|
+
# Brief summary
|
|
76
|
+
clara explain problem.lp --level brief
|
|
77
|
+
|
|
78
|
+
# JSON output
|
|
79
|
+
clara explain problem.lp --format json
|
|
80
|
+
|
|
81
|
+
# Use HiGHS engine for larger problems
|
|
82
|
+
clara explain problem.lp --engine highs
|
|
83
|
+
|
|
84
|
+
# Solve only (no explanation)
|
|
85
|
+
clara solve problem.lp
|
|
86
|
+
|
|
87
|
+
# Problem info
|
|
88
|
+
clara info problem.lp
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Python API
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from clara.io import read_lp
|
|
95
|
+
from clara.engine.simplex import RevisedSimplex
|
|
96
|
+
from clara.explain import Explainer
|
|
97
|
+
|
|
98
|
+
problem = read_lp("problem.lp")
|
|
99
|
+
state = RevisedSimplex(problem).solve()
|
|
100
|
+
report = Explainer().explain(state, problem=problem)
|
|
101
|
+
print(report.to_text())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Roadmap
|
|
105
|
+
|
|
106
|
+
- [x] v0.1.0 — LP solver + explainer + CLI (you are here)
|
|
107
|
+
- [ ] v0.5.0 — MIP (Branch-and-Bound), counterfactual analysis, TUI
|
|
108
|
+
- [ ] v1.0.0 — Incremental reoptimization, parametric LP, streaming
|
|
109
|
+
- [ ] v2.0.0 — LLM hybrid explanations, web UI
|
|
110
|
+
|
|
111
|
+
## Academic Use
|
|
112
|
+
|
|
113
|
+
CLARA fills a gap identified in the XAIOR framework (De Bock et al., 2024, EJOR):
|
|
114
|
+
explainable AI for mathematical optimization solvers. If you use CLARA in research,
|
|
115
|
+
please cite:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
(citation TBD — paper in preparation for Mathematical Programming Computation)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT © 2026 Yoonsik
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# CLARA — Classical LP Analysis for Reoptimization and Attribution
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://python.org)
|
|
5
|
+
|
|
6
|
+
A white-box optimization solver that explains why solutions are optimal,
|
|
7
|
+
when reoptimization is needed, and what changed.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Solver-intrinsic explanations** — not post-hoc ML surrogate, but direct
|
|
12
|
+
interpretation of basis, dual values, and sensitivity ranges
|
|
13
|
+
- **Dual backend** — Internal Revised Simplex (full transparency, step-by-step trace)
|
|
14
|
+
or HiGHS (performance for large problems)
|
|
15
|
+
- **Three explanation levels** mapped to the XAIOR framework (De Bock et al., 2024):
|
|
16
|
+
- *Understandability*: Which constraints are binding? What resources are fully used?
|
|
17
|
+
- *Justifiability*: Why is each variable at this value? What's the reduced cost?
|
|
18
|
+
- *Actionability*: What can change without breaking the solution? Where to invest?
|
|
19
|
+
- **CLI and Python API** — `clara explain problem.lp` or import as library
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install clara-opt
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For development:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/yoonsik-jung-opt/clara-opt.git
|
|
31
|
+
cd clara-opt
|
|
32
|
+
pip install -e ".[dev]"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Explain a production planning problem
|
|
39
|
+
clara explain examples/production.lp
|
|
40
|
+
|
|
41
|
+
# Brief summary
|
|
42
|
+
clara explain problem.lp --level brief
|
|
43
|
+
|
|
44
|
+
# JSON output
|
|
45
|
+
clara explain problem.lp --format json
|
|
46
|
+
|
|
47
|
+
# Use HiGHS engine for larger problems
|
|
48
|
+
clara explain problem.lp --engine highs
|
|
49
|
+
|
|
50
|
+
# Solve only (no explanation)
|
|
51
|
+
clara solve problem.lp
|
|
52
|
+
|
|
53
|
+
# Problem info
|
|
54
|
+
clara info problem.lp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Python API
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from clara.io import read_lp
|
|
61
|
+
from clara.engine.simplex import RevisedSimplex
|
|
62
|
+
from clara.explain import Explainer
|
|
63
|
+
|
|
64
|
+
problem = read_lp("problem.lp")
|
|
65
|
+
state = RevisedSimplex(problem).solve()
|
|
66
|
+
report = Explainer().explain(state, problem=problem)
|
|
67
|
+
print(report.to_text())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Roadmap
|
|
71
|
+
|
|
72
|
+
- [x] v0.1.0 — LP solver + explainer + CLI (you are here)
|
|
73
|
+
- [ ] v0.5.0 — MIP (Branch-and-Bound), counterfactual analysis, TUI
|
|
74
|
+
- [ ] v1.0.0 — Incremental reoptimization, parametric LP, streaming
|
|
75
|
+
- [ ] v2.0.0 — LLM hybrid explanations, web UI
|
|
76
|
+
|
|
77
|
+
## Academic Use
|
|
78
|
+
|
|
79
|
+
CLARA fills a gap identified in the XAIOR framework (De Bock et al., 2024, EJOR):
|
|
80
|
+
explainable AI for mathematical optimization solvers. If you use CLARA in research,
|
|
81
|
+
please cite:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
(citation TBD — paper in preparation for Mathematical Programming Computation)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT © 2026 Yoonsik
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""CLARA command-line interface.
|
|
2
|
+
|
|
3
|
+
Pipeline: LP Parser → Solve Engine → Explainer → Output
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from clara.io.lp_parser import LPParseError, read_lp
|
|
15
|
+
from clara.engine.simplex import RevisedSimplex
|
|
16
|
+
from clara.explain.explainer import Explainer
|
|
17
|
+
from clara.explain.types import DetailLevel
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.group()
|
|
21
|
+
@click.version_option(version="0.1.0", prog_name="clara")
|
|
22
|
+
def main():
|
|
23
|
+
"""CLARA — Classical LP Analysis for Reoptimization and Attribution.
|
|
24
|
+
|
|
25
|
+
A white-box optimization solver that explains why solutions are optimal,
|
|
26
|
+
when reoptimization is needed, and what changed.
|
|
27
|
+
"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@main.command()
|
|
32
|
+
@click.argument("file", type=click.Path(exists=True, dir_okay=False))
|
|
33
|
+
@click.option("--engine", type=click.Choice(["internal", "highs"]),
|
|
34
|
+
default="internal", help="Solve engine.")
|
|
35
|
+
@click.option("--level", type=click.Choice(["brief", "detailed"]),
|
|
36
|
+
default="detailed", help="Explanation detail level.")
|
|
37
|
+
@click.option("--format", "fmt", type=click.Choice(["text", "json"]),
|
|
38
|
+
default="text", help="Output format.")
|
|
39
|
+
@click.option("--output", "-o", type=click.Path(), default=None,
|
|
40
|
+
help="Write output to file.")
|
|
41
|
+
@click.option("--quiet", "-q", is_flag=True, help="Suppress header.")
|
|
42
|
+
def explain(file, engine, level, fmt, output, quiet):
|
|
43
|
+
"""Solve an LP and explain the solution."""
|
|
44
|
+
problem = _parse_file(file)
|
|
45
|
+
state = _solve(problem, engine)
|
|
46
|
+
|
|
47
|
+
if not state.is_optimal:
|
|
48
|
+
click.secho(f"Problem is {state.status.name}.", fg="yellow", err=True)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
detail = DetailLevel.BRIEF if level == "brief" else DetailLevel.DETAILED
|
|
52
|
+
explainer = Explainer()
|
|
53
|
+
report = explainer.explain(state, level=detail, problem=problem)
|
|
54
|
+
|
|
55
|
+
if fmt == "json":
|
|
56
|
+
text = report.to_json()
|
|
57
|
+
else:
|
|
58
|
+
text = report.to_text(level=detail)
|
|
59
|
+
if quiet:
|
|
60
|
+
text = _strip_header(text)
|
|
61
|
+
|
|
62
|
+
_write_output(text, output)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@main.command()
|
|
66
|
+
@click.argument("file", type=click.Path(exists=True, dir_okay=False))
|
|
67
|
+
@click.option("--engine", type=click.Choice(["internal", "highs"]),
|
|
68
|
+
default="internal")
|
|
69
|
+
@click.option("--format", "fmt", type=click.Choice(["text", "json"]),
|
|
70
|
+
default="text")
|
|
71
|
+
@click.option("--output", "-o", type=click.Path(), default=None)
|
|
72
|
+
def solve(file, engine, fmt, output):
|
|
73
|
+
"""Solve an LP without explanation (quick mode)."""
|
|
74
|
+
problem = _parse_file(file)
|
|
75
|
+
state = _solve(problem, engine)
|
|
76
|
+
|
|
77
|
+
if fmt == "json":
|
|
78
|
+
text = state.to_json()
|
|
79
|
+
else:
|
|
80
|
+
text = _format_solve_text(state)
|
|
81
|
+
|
|
82
|
+
_write_output(text, output)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@main.command()
|
|
86
|
+
@click.argument("file", type=click.Path(exists=True, dir_okay=False))
|
|
87
|
+
def info(file):
|
|
88
|
+
"""Show problem statistics without solving."""
|
|
89
|
+
problem = _parse_file(file)
|
|
90
|
+
nonzeros = int((problem.A != 0).sum())
|
|
91
|
+
lines = [
|
|
92
|
+
f"Problem: {problem.name}",
|
|
93
|
+
f"Variables: {problem.num_variables}",
|
|
94
|
+
f"Constraints: {problem.num_constraints}",
|
|
95
|
+
f"Nonzeros: {nonzeros}",
|
|
96
|
+
]
|
|
97
|
+
click.echo("\n".join(lines))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@main.command(name="what-if", hidden=True)
|
|
101
|
+
@click.argument("file", type=click.Path(exists=True, dir_okay=False))
|
|
102
|
+
def what_if(file):
|
|
103
|
+
"""[Phase 1.5] Interactive what-if analysis."""
|
|
104
|
+
click.secho("what-if is not yet implemented (Phase 1.5).", fg="yellow")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@main.command(hidden=True)
|
|
108
|
+
@click.argument("file", type=click.Path(exists=True, dir_okay=False))
|
|
109
|
+
@click.option("--params", type=click.Path(exists=True))
|
|
110
|
+
def watch(file, params):
|
|
111
|
+
"""[Phase 2] Stream parameter changes and reoptimize."""
|
|
112
|
+
click.secho("watch is not yet implemented (Phase 2).", fg="yellow")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ============================================================
|
|
116
|
+
# Helpers
|
|
117
|
+
# ============================================================
|
|
118
|
+
|
|
119
|
+
def _parse_file(filepath: str):
|
|
120
|
+
"""Parse LP file with user-friendly error handling."""
|
|
121
|
+
try:
|
|
122
|
+
return read_lp(filepath)
|
|
123
|
+
except LPParseError as e:
|
|
124
|
+
click.secho(f"Parse error: {e}", fg="red", err=True)
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _solve(problem, engine_name: str):
|
|
129
|
+
"""Solve with the selected engine."""
|
|
130
|
+
solver = _make_engine(engine_name)
|
|
131
|
+
state = solver.solve()
|
|
132
|
+
return state
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _make_engine(engine_name: str):
|
|
136
|
+
"""Factory for solve engines. Returns a solver instance (not yet solved)."""
|
|
137
|
+
# For now, we need the problem to create the solver.
|
|
138
|
+
# This is handled differently — _solve creates the engine with the problem.
|
|
139
|
+
raise NotImplementedError # not used directly
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _solve(problem, engine_name: str):
|
|
143
|
+
"""Solve with the selected engine."""
|
|
144
|
+
if engine_name == "highs":
|
|
145
|
+
try:
|
|
146
|
+
from clara.engine.highs_backend import HiGHSBackend
|
|
147
|
+
return HiGHSBackend().solve(problem)
|
|
148
|
+
except (ImportError, ModuleNotFoundError):
|
|
149
|
+
click.secho(
|
|
150
|
+
"HiGHS backend not yet available. Using internal engine.",
|
|
151
|
+
fg="yellow", err=True,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
solver = RevisedSimplex(problem)
|
|
155
|
+
return solver.solve()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _format_solve_text(state) -> str:
|
|
159
|
+
"""Minimal text output for `clara solve`."""
|
|
160
|
+
lines = [f"Optimal value: {state.optimal_value:.4f}"]
|
|
161
|
+
for v in state.variables:
|
|
162
|
+
lines.append(f" {v.name} = {v.value:.4f}")
|
|
163
|
+
lines.append(
|
|
164
|
+
f"Solved in {state.iteration_count} iterations, "
|
|
165
|
+
f"{state.solve_time_seconds:.3f}s ({state.engine.name})"
|
|
166
|
+
)
|
|
167
|
+
return "\n".join(lines)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _strip_header(text: str) -> str:
|
|
171
|
+
"""Remove header section (everything before double newline after header)."""
|
|
172
|
+
# Header is between two ═ lines, skip past the second one
|
|
173
|
+
lines = text.split("\n")
|
|
174
|
+
sep_count = 0
|
|
175
|
+
start = 0
|
|
176
|
+
for i, line in enumerate(lines):
|
|
177
|
+
if line.startswith("\u2550"):
|
|
178
|
+
sep_count += 1
|
|
179
|
+
if sep_count == 2:
|
|
180
|
+
start = i + 1
|
|
181
|
+
break
|
|
182
|
+
# Skip blank line after header
|
|
183
|
+
while start < len(lines) and not lines[start].strip():
|
|
184
|
+
start += 1
|
|
185
|
+
return "\n".join(lines[start:])
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _write_output(text: str, output_path: Optional[str]) -> None:
|
|
189
|
+
"""Write to file or stdout."""
|
|
190
|
+
if output_path:
|
|
191
|
+
try:
|
|
192
|
+
Path(output_path).write_text(text)
|
|
193
|
+
click.secho(f"Written to {output_path}", fg="green", err=True)
|
|
194
|
+
except IOError as e:
|
|
195
|
+
click.secho(f"Write error: {e}", fg="red", err=True)
|
|
196
|
+
sys.exit(1)
|
|
197
|
+
else:
|
|
198
|
+
click.echo(text)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Solver engine."""
|