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 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,8 @@
1
+ """
2
+ Configuration parser for optimization configs.
3
+ """
4
+
5
+ from pyoptima.config_parser.parser import parse_config, parse_config_file
6
+
7
+ __all__ = ["parse_config", "parse_config_file"]
8
+
@@ -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
@@ -0,0 +1,9 @@
1
+ """
2
+ Solver abstraction layer for different optimization solvers.
3
+ """
4
+
5
+ from pyoptima.solvers.base import Solver, SolverType
6
+ from pyoptima.solvers.factory import get_solver
7
+
8
+ __all__ = ["Solver", "SolverType", "get_solver"]
9
+
@@ -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
+