pyoptima 0.0.1__py3-none-any.whl
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.
- pyoptima/__init__.py +34 -0
- pyoptima/cli.py +124 -0
- pyoptima/config_parser/__init__.py +8 -0
- pyoptima/config_parser/parser.py +67 -0
- pyoptima/models/__init__.py +22 -0
- pyoptima/models/config.py +108 -0
- pyoptima/optimization_engine.py +111 -0
- pyoptima/py.typed +0 -0
- pyoptima/solvers/__init__.py +9 -0
- pyoptima/solvers/base.py +60 -0
- pyoptima/solvers/cbc_solver.py +117 -0
- pyoptima/solvers/factory.py +50 -0
- pyoptima/solvers/glpk_solver.py +111 -0
- pyoptima/solvers/gurobi_solver.py +119 -0
- pyoptima-0.0.1.dist-info/METADATA +404 -0
- pyoptima-0.0.1.dist-info/RECORD +20 -0
- pyoptima-0.0.1.dist-info/WHEEL +5 -0
- pyoptima-0.0.1.dist-info/entry_points.txt +2 -0
- pyoptima-0.0.1.dist-info/licenses/LICENSE +22 -0
- pyoptima-0.0.1.dist-info/top_level.txt +1 -0
pyoptima/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyOptima - Declarative Optimization Service
|
|
3
|
+
|
|
4
|
+
A Python package for declarative optimization that accepts configuration files
|
|
5
|
+
and performs optimizations using various solvers (CBC, GUROBI, GLPK).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.0.1"
|
|
9
|
+
|
|
10
|
+
# Main optimization engine
|
|
11
|
+
from pyoptima.optimization_engine import OptimizationEngine, OptimizationResult
|
|
12
|
+
|
|
13
|
+
# Config parser
|
|
14
|
+
from pyoptima.config_parser import parse_config, parse_config_file
|
|
15
|
+
|
|
16
|
+
# Models
|
|
17
|
+
from pyoptima.models.config import OptimizationConfig
|
|
18
|
+
|
|
19
|
+
# Solvers
|
|
20
|
+
from pyoptima.solvers import SolverType, get_solver
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
# Optimization Engine
|
|
24
|
+
"OptimizationEngine",
|
|
25
|
+
"OptimizationResult",
|
|
26
|
+
# Config Parser
|
|
27
|
+
"OptimizationConfig",
|
|
28
|
+
"parse_config",
|
|
29
|
+
"parse_config_file",
|
|
30
|
+
# Solvers
|
|
31
|
+
"SolverType",
|
|
32
|
+
"get_solver",
|
|
33
|
+
]
|
|
34
|
+
|
pyoptima/cli.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main CLI entry point for pyoptima commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pyoptima.optimization_engine import OptimizationEngine
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
"""Main CLI entry point."""
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
description="PyOptima - Declarative Optimization Service",
|
|
17
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
21
|
+
|
|
22
|
+
# optimize subcommand
|
|
23
|
+
optimize_parser = subparsers.add_parser(
|
|
24
|
+
"optimize", help="Run optimization from configuration file"
|
|
25
|
+
)
|
|
26
|
+
optimize_parser.add_argument(
|
|
27
|
+
"config_file",
|
|
28
|
+
type=str,
|
|
29
|
+
help="Path to optimization configuration file (JSON)",
|
|
30
|
+
)
|
|
31
|
+
optimize_parser.add_argument(
|
|
32
|
+
"--output",
|
|
33
|
+
type=str,
|
|
34
|
+
help="Output file path for results (JSON format)",
|
|
35
|
+
)
|
|
36
|
+
optimize_parser.add_argument(
|
|
37
|
+
"--pretty",
|
|
38
|
+
action="store_true",
|
|
39
|
+
help="Pretty print output",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
|
|
44
|
+
if not args.command:
|
|
45
|
+
parser.print_help()
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
if args.command == "optimize":
|
|
49
|
+
return cmd_optimize(
|
|
50
|
+
config_file=args.config_file,
|
|
51
|
+
output=args.output,
|
|
52
|
+
pretty=args.pretty,
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
parser.print_help()
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_optimize(config_file: str, output: str = None, pretty: bool = False) -> int:
|
|
60
|
+
"""
|
|
61
|
+
Run optimization from configuration file.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
config_file: Path to configuration file
|
|
65
|
+
output: Optional output file path
|
|
66
|
+
pretty: Whether to pretty print output
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Exit code (0 for success, 1 for error)
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
# Create engine
|
|
73
|
+
engine = OptimizationEngine()
|
|
74
|
+
|
|
75
|
+
# Run optimization
|
|
76
|
+
result = engine.optimize_from_file(config_file)
|
|
77
|
+
|
|
78
|
+
# Convert to dictionary
|
|
79
|
+
result_dict = result.to_dict()
|
|
80
|
+
|
|
81
|
+
# Output results
|
|
82
|
+
if output:
|
|
83
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
84
|
+
if pretty:
|
|
85
|
+
json.dump(result_dict, f, indent=2)
|
|
86
|
+
else:
|
|
87
|
+
json.dump(result_dict, f)
|
|
88
|
+
print(f"✓ Results written to {output}")
|
|
89
|
+
else:
|
|
90
|
+
if pretty:
|
|
91
|
+
print(json.dumps(result_dict, indent=2))
|
|
92
|
+
else:
|
|
93
|
+
print(json.dumps(result_dict))
|
|
94
|
+
|
|
95
|
+
# Return appropriate exit code
|
|
96
|
+
if result.is_optimal():
|
|
97
|
+
return 0
|
|
98
|
+
else:
|
|
99
|
+
print(
|
|
100
|
+
f"⚠ Warning: Optimization did not find optimal solution. Status: {result.status}",
|
|
101
|
+
file=sys.stderr,
|
|
102
|
+
)
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
except FileNotFoundError as e:
|
|
106
|
+
print(f"❌ Error: {e}", file=sys.stderr)
|
|
107
|
+
return 1
|
|
108
|
+
except ValueError as e:
|
|
109
|
+
print(f"❌ Error: {e}", file=sys.stderr)
|
|
110
|
+
return 1
|
|
111
|
+
except RuntimeError as e:
|
|
112
|
+
print(f"❌ Error: {e}", file=sys.stderr)
|
|
113
|
+
return 1
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"❌ Unexpected error: {e}", file=sys.stderr)
|
|
116
|
+
import traceback
|
|
117
|
+
|
|
118
|
+
traceback.print_exc()
|
|
119
|
+
return 1
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
sys.exit(main())
|
|
124
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parser for optimization configuration files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
|
|
9
|
+
from pyoptima.models.config import OptimizationConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_config(config_data: Dict[str, Any]) -> OptimizationConfig:
|
|
13
|
+
"""
|
|
14
|
+
Parse optimization configuration from a dictionary.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
config_data: Dictionary containing optimization configuration
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
OptimizationConfig object
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ValueError: If configuration is invalid
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
return OptimizationConfig(**config_data)
|
|
27
|
+
except Exception as e:
|
|
28
|
+
raise ValueError(f"Invalid optimization configuration: {e}") from e
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_config_file(file_path: str) -> OptimizationConfig:
|
|
32
|
+
"""
|
|
33
|
+
Load and parse an optimization configuration file (JSON).
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
file_path: Path to configuration file
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
OptimizationConfig object
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
FileNotFoundError: If file doesn't exist
|
|
43
|
+
ValueError: If file format is not supported or invalid
|
|
44
|
+
"""
|
|
45
|
+
path = Path(file_path)
|
|
46
|
+
|
|
47
|
+
if not path.exists():
|
|
48
|
+
raise FileNotFoundError(f"Configuration file not found: {file_path}")
|
|
49
|
+
|
|
50
|
+
# Determine file format
|
|
51
|
+
suffix = path.suffix.lower()
|
|
52
|
+
|
|
53
|
+
if suffix == ".json":
|
|
54
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
55
|
+
config_data = json.load(f)
|
|
56
|
+
else:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Unsupported file format: {suffix}. Supported formats: .json"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if not isinstance(config_data, dict):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Configuration file must contain a dictionary/object, got {type(config_data)}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return parse_config(config_data)
|
|
67
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for optimization configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pyoptima.models.config import (
|
|
6
|
+
Constraint,
|
|
7
|
+
Meta,
|
|
8
|
+
Objective,
|
|
9
|
+
OptimizationConfig,
|
|
10
|
+
OptimizationVariable,
|
|
11
|
+
Term,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"OptimizationConfig",
|
|
16
|
+
"Meta",
|
|
17
|
+
"OptimizationVariable",
|
|
18
|
+
"Objective",
|
|
19
|
+
"Term",
|
|
20
|
+
"Constraint",
|
|
21
|
+
]
|
|
22
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for optimization configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Meta(BaseModel):
|
|
11
|
+
"""Metadata for the optimization job."""
|
|
12
|
+
|
|
13
|
+
job_id: str = Field(..., description="Unique identifier for the optimization job")
|
|
14
|
+
solver: Literal["CBC", "GUROBI", "GLPK"] = Field(
|
|
15
|
+
..., description="Solver to use for optimization"
|
|
16
|
+
)
|
|
17
|
+
time_limit_seconds: Optional[int] = Field(
|
|
18
|
+
None, description="Time limit for optimization in seconds"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OptimizationVariable(BaseModel):
|
|
23
|
+
"""Definition of an optimization variable."""
|
|
24
|
+
|
|
25
|
+
id: str = Field(..., description="Unique identifier for the variable")
|
|
26
|
+
type: Literal["Continuous", "Binary", "Integer"] = Field(
|
|
27
|
+
..., description="Type of the variable"
|
|
28
|
+
)
|
|
29
|
+
lb: Optional[float] = Field(None, description="Lower bound for the variable")
|
|
30
|
+
ub: Optional[float] = Field(None, description="Upper bound for the variable")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Term(BaseModel):
|
|
34
|
+
"""A term in the objective or constraint expression."""
|
|
35
|
+
|
|
36
|
+
var: str = Field(..., description="Variable ID")
|
|
37
|
+
coef: float = Field(..., description="Coefficient for the variable")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Objective(BaseModel):
|
|
41
|
+
"""Objective function definition."""
|
|
42
|
+
|
|
43
|
+
direction: Literal["Maximize", "Minimize"] = Field(
|
|
44
|
+
..., description="Direction of optimization"
|
|
45
|
+
)
|
|
46
|
+
terms: List[Term] = Field(..., description="Terms in the objective function")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Constraint(BaseModel):
|
|
50
|
+
"""A constraint in the optimization problem."""
|
|
51
|
+
|
|
52
|
+
id: str = Field(..., description="Unique identifier for the constraint")
|
|
53
|
+
lower_bound: Optional[float] = Field(
|
|
54
|
+
None, description="Lower bound for the constraint"
|
|
55
|
+
)
|
|
56
|
+
upper_bound: Optional[float] = Field(
|
|
57
|
+
None, description="Upper bound for the constraint"
|
|
58
|
+
)
|
|
59
|
+
terms: List[Term] = Field(..., description="Terms in the constraint expression")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class OptimizationConfig(BaseModel):
|
|
63
|
+
"""Complete optimization configuration."""
|
|
64
|
+
|
|
65
|
+
meta: Meta = Field(..., description="Metadata for the optimization job")
|
|
66
|
+
variables: List[OptimizationVariable] = Field(
|
|
67
|
+
..., description="List of optimization variables"
|
|
68
|
+
)
|
|
69
|
+
objective: Objective = Field(..., description="Objective function")
|
|
70
|
+
constraints: List[Constraint] = Field(
|
|
71
|
+
default_factory=list, description="List of constraints"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
class Config:
|
|
75
|
+
"""Pydantic config."""
|
|
76
|
+
|
|
77
|
+
extra = "forbid"
|
|
78
|
+
json_schema_extra = {
|
|
79
|
+
"example": {
|
|
80
|
+
"meta": {
|
|
81
|
+
"job_id": "portfolio-rebalance-001",
|
|
82
|
+
"solver": "CBC",
|
|
83
|
+
"time_limit_seconds": 30,
|
|
84
|
+
},
|
|
85
|
+
"variables": [
|
|
86
|
+
{"id": "w_aapl", "type": "Continuous", "lb": 0, "ub": 0.5},
|
|
87
|
+
{"id": "shift_john_1", "type": "Binary"},
|
|
88
|
+
],
|
|
89
|
+
"objective": {
|
|
90
|
+
"direction": "Maximize",
|
|
91
|
+
"terms": [
|
|
92
|
+
{"var": "w_aapl", "coef": 0.12},
|
|
93
|
+
{"var": "shift_john_1", "coef": -10},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
"constraints": [
|
|
97
|
+
{
|
|
98
|
+
"id": "budget_limit",
|
|
99
|
+
"upper_bound": 1.0,
|
|
100
|
+
"terms": [
|
|
101
|
+
{"var": "w_aapl", "coef": 1},
|
|
102
|
+
{"var": "w_msft", "coef": 1},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main optimization engine that processes configurations and runs optimizations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from pyoptima.config_parser import parse_config_file
|
|
8
|
+
from pyoptima.models.config import OptimizationConfig
|
|
9
|
+
from pyoptima.solvers import SolverType, get_solver
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OptimizationResult:
|
|
13
|
+
"""Result of an optimization run."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
status: str,
|
|
18
|
+
objective_value: Optional[float],
|
|
19
|
+
variables: Dict[str, float],
|
|
20
|
+
message: str,
|
|
21
|
+
job_id: str,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initialize optimization result.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
status: Status of the optimization
|
|
28
|
+
objective_value: Objective function value (if optimal)
|
|
29
|
+
variables: Variable values (if optimal)
|
|
30
|
+
message: Status message
|
|
31
|
+
job_id: Job ID
|
|
32
|
+
"""
|
|
33
|
+
self.status = status
|
|
34
|
+
self.objective_value = objective_value
|
|
35
|
+
self.variables = variables
|
|
36
|
+
self.message = message
|
|
37
|
+
self.job_id = job_id
|
|
38
|
+
|
|
39
|
+
def is_optimal(self) -> bool:
|
|
40
|
+
"""Check if the solution is optimal."""
|
|
41
|
+
return self.status == "optimal"
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
44
|
+
"""Convert result to dictionary."""
|
|
45
|
+
return {
|
|
46
|
+
"job_id": self.job_id,
|
|
47
|
+
"status": self.status,
|
|
48
|
+
"objective_value": self.objective_value,
|
|
49
|
+
"variables": self.variables,
|
|
50
|
+
"message": self.message,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
"""String representation."""
|
|
55
|
+
return (
|
|
56
|
+
f"OptimizationResult(job_id={self.job_id}, status={self.status}, "
|
|
57
|
+
f"objective_value={self.objective_value})"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class OptimizationEngine:
|
|
62
|
+
"""Main engine for running optimizations from configuration files."""
|
|
63
|
+
|
|
64
|
+
def __init__(self):
|
|
65
|
+
"""Initialize the optimization engine."""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def optimize_from_file(self, config_file: str) -> OptimizationResult:
|
|
69
|
+
"""
|
|
70
|
+
Run optimization from a configuration file.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
config_file: Path to configuration file
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
OptimizationResult object
|
|
77
|
+
"""
|
|
78
|
+
config = parse_config_file(config_file)
|
|
79
|
+
return self.optimize(config)
|
|
80
|
+
|
|
81
|
+
def optimize(self, config: OptimizationConfig) -> OptimizationResult:
|
|
82
|
+
"""
|
|
83
|
+
Run optimization from a configuration object.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
config: Optimization configuration
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
OptimizationResult object
|
|
90
|
+
"""
|
|
91
|
+
# Get solver type
|
|
92
|
+
solver_type = SolverType(config.meta.solver)
|
|
93
|
+
|
|
94
|
+
# Get solver instance
|
|
95
|
+
solver = get_solver(
|
|
96
|
+
solver_type=solver_type,
|
|
97
|
+
time_limit_seconds=config.meta.time_limit_seconds,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Solve
|
|
101
|
+
result_dict = solver.solve(config)
|
|
102
|
+
|
|
103
|
+
# Create result object
|
|
104
|
+
return OptimizationResult(
|
|
105
|
+
status=result_dict["status"],
|
|
106
|
+
objective_value=result_dict.get("objective_value"),
|
|
107
|
+
variables=result_dict.get("variables", {}),
|
|
108
|
+
message=str(result_dict.get("message", "")),
|
|
109
|
+
job_id=config.meta.job_id,
|
|
110
|
+
)
|
|
111
|
+
|
pyoptima/py.typed
ADDED
|
File without changes
|
pyoptima/solvers/base.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base solver interface and types.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from pyoptima.models.config import OptimizationConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SolverType(str, Enum):
|
|
13
|
+
"""Supported solver types."""
|
|
14
|
+
|
|
15
|
+
CBC = "CBC"
|
|
16
|
+
GUROBI = "GUROBI"
|
|
17
|
+
GLPK = "GLPK"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Solver(ABC):
|
|
21
|
+
"""Abstract base class for optimization solvers."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, time_limit_seconds: Optional[int] = None):
|
|
24
|
+
"""
|
|
25
|
+
Initialize solver.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
time_limit_seconds: Time limit for optimization in seconds
|
|
29
|
+
"""
|
|
30
|
+
self.time_limit_seconds = time_limit_seconds
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def solve(
|
|
34
|
+
self, config: OptimizationConfig
|
|
35
|
+
) -> Dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
Solve the optimization problem.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
config: Optimization configuration
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Dictionary containing:
|
|
44
|
+
- status: str - Status of the optimization (e.g., "optimal", "infeasible", "unbounded")
|
|
45
|
+
- objective_value: float - Objective function value (if optimal)
|
|
46
|
+
- variables: Dict[str, float] - Variable values (if optimal)
|
|
47
|
+
- message: str - Status message
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def is_available(self) -> bool:
|
|
53
|
+
"""
|
|
54
|
+
Check if the solver is available.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if solver is available, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CBC (COIN-OR Branch and Cut) solver implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from pulp import LpMaximize, LpMinimize, LpProblem, LpStatus, lpSum, LpVariable
|
|
9
|
+
PULP_AVAILABLE = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
PULP_AVAILABLE = False
|
|
12
|
+
|
|
13
|
+
from pyoptima.models.config import OptimizationConfig
|
|
14
|
+
from pyoptima.solvers.base import Solver
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CBCSolver(Solver):
|
|
18
|
+
"""CBC solver implementation using PuLP."""
|
|
19
|
+
|
|
20
|
+
def is_available(self) -> bool:
|
|
21
|
+
"""Check if CBC solver is available."""
|
|
22
|
+
return PULP_AVAILABLE
|
|
23
|
+
|
|
24
|
+
def solve(self, config: OptimizationConfig) -> Dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Solve the optimization problem using CBC.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config: Optimization configuration
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Dictionary with optimization results
|
|
33
|
+
"""
|
|
34
|
+
if not self.is_available():
|
|
35
|
+
raise RuntimeError("CBC solver is not available. Install pulp: pip install pulp")
|
|
36
|
+
|
|
37
|
+
# Create PuLP problem
|
|
38
|
+
sense = LpMaximize if config.objective.direction == "Maximize" else LpMinimize
|
|
39
|
+
prob = LpProblem(config.meta.job_id, sense)
|
|
40
|
+
|
|
41
|
+
# Create variables
|
|
42
|
+
variables = {}
|
|
43
|
+
for var_def in config.variables:
|
|
44
|
+
if var_def.type == "Binary":
|
|
45
|
+
var = LpVariable(var_def.id, cat="Binary")
|
|
46
|
+
elif var_def.type == "Integer":
|
|
47
|
+
var = LpVariable(
|
|
48
|
+
var_def.id,
|
|
49
|
+
lowBound=var_def.lb,
|
|
50
|
+
upBound=var_def.ub,
|
|
51
|
+
cat="Integer",
|
|
52
|
+
)
|
|
53
|
+
else: # Continuous
|
|
54
|
+
var = LpVariable(
|
|
55
|
+
var_def.id,
|
|
56
|
+
lowBound=var_def.lb,
|
|
57
|
+
upBound=var_def.ub,
|
|
58
|
+
cat="Continuous",
|
|
59
|
+
)
|
|
60
|
+
variables[var_def.id] = var
|
|
61
|
+
|
|
62
|
+
# Add objective
|
|
63
|
+
objective_terms = [
|
|
64
|
+
term.coef * variables[term.var] for term in config.objective.terms
|
|
65
|
+
]
|
|
66
|
+
prob += lpSum(objective_terms)
|
|
67
|
+
|
|
68
|
+
# Add constraints
|
|
69
|
+
for constraint in config.constraints:
|
|
70
|
+
constraint_terms = [
|
|
71
|
+
term.coef * variables[term.var] for term in constraint.terms
|
|
72
|
+
]
|
|
73
|
+
expr = lpSum(constraint_terms)
|
|
74
|
+
|
|
75
|
+
if constraint.lower_bound is not None and constraint.upper_bound is not None:
|
|
76
|
+
prob += expr >= constraint.lower_bound
|
|
77
|
+
prob += expr <= constraint.upper_bound
|
|
78
|
+
elif constraint.lower_bound is not None:
|
|
79
|
+
prob += expr >= constraint.lower_bound
|
|
80
|
+
elif constraint.upper_bound is not None:
|
|
81
|
+
prob += expr <= constraint.upper_bound
|
|
82
|
+
|
|
83
|
+
# Set time limit if specified
|
|
84
|
+
if self.time_limit_seconds:
|
|
85
|
+
# PuLP doesn't directly support time limits, but we can note it
|
|
86
|
+
# In practice, you might need to use solver-specific options
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
# Solve
|
|
90
|
+
prob.solve(solver="CBC")
|
|
91
|
+
|
|
92
|
+
# Extract results
|
|
93
|
+
status_map = {
|
|
94
|
+
"Optimal": "optimal",
|
|
95
|
+
"Infeasible": "infeasible",
|
|
96
|
+
"Unbounded": "unbounded",
|
|
97
|
+
"Not Solved": "not_solved",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
pulp_status = LpStatus[prob.status]
|
|
101
|
+
status = status_map.get(pulp_status, "unknown")
|
|
102
|
+
|
|
103
|
+
result = {
|
|
104
|
+
"status": status,
|
|
105
|
+
"message": pulp_status,
|
|
106
|
+
"objective_value": None,
|
|
107
|
+
"variables": {},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if status == "optimal":
|
|
111
|
+
result["objective_value"] = prob.objective.value()
|
|
112
|
+
result["variables"] = {
|
|
113
|
+
var_id: var.varValue for var_id, var in variables.items()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
|