scipplan 0.2.1a0__py2.py3-none-any.whl → 0.2.2a0__py2.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.
@@ -0,0 +1,5 @@
1
+ __version__ = "0.2.2alpha0"
2
+ print(f"SCIPPlan Version: {__version__}")
3
+ __release__ = "v0.2.2a0"
4
+ __author__ = "Ari Gestetner, Buser Say"
5
+ __email__ = "ari.gestetner@monash.edu, buser.say@monash.edu"
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from textwrap import dedent
5
+ import argparse
6
+
7
+ @dataclass
8
+ class Config:
9
+ """
10
+ The Config class provides an interface to set the configuration for SCIPPlan.
11
+ Config can be set by creating an instance of the class adding the variables desired.
12
+ Alternatively, the `get_config` class method will return a config instance using variables set by the programs args (e.g. -D 'domain').
13
+ """
14
+ domain: str
15
+ instance: str
16
+ horizon: int = field(default=None)
17
+ epsilon: float = field(default=None)
18
+ gap: float = field(default=None)
19
+ provide_sols: bool = field(default=False)
20
+ show_output: bool = False
21
+ save_sols: bool = False
22
+ bigM: float = 1000.0
23
+ dt_var: str = "Dt"
24
+ _defaults: dict[str, bool] = field(default_factory=dict, repr=False)
25
+
26
+ def __post_init__(self) -> None:
27
+ # Set all defaults to False and if the value is None then it will be updated to true
28
+ self._defaults = {
29
+ "domain": False,
30
+ "instance": False,
31
+ "horizon": False,
32
+ "epsilon": False,
33
+ "gap": False
34
+ }
35
+ if self.horizon is None:
36
+ print("Horizon is not provided, and is set to 1. ")
37
+ self.horizon = 1
38
+ self._defaults["horizon"] = True
39
+
40
+ if self.epsilon is None:
41
+ print("Epsilon is not provided, and is set to 0.1. ")
42
+ self.epsilon = 0.1
43
+ self._defaults["epsilon"] = True
44
+
45
+ if self.gap is None:
46
+ print("Gap is not provided, and is set to 10.0%. ")
47
+ self.gap = 0.1
48
+ self._defaults["gap"] = True
49
+
50
+ def __str__(self) -> str:
51
+ text = f"""
52
+ Configuration:
53
+
54
+ Use System of ODE's: {not self.provide_sols}
55
+ Display SCIP Output: {self.show_output}
56
+ Save Solutions: {self.show_output}
57
+ Dt Variable Name: {self.dt_var}
58
+
59
+ Domain (str): {self.domain}
60
+ Instance (str): {self.instance}
61
+ Horizon (int): {self.horizon} {'(default)' if self._defaults['horizon'] is True else ''}
62
+ Epsilon (float): {self.epsilon} {'(default)' if self._defaults['epsilon'] is True else ''}
63
+ Gap (float): {self.gap * 100}% {'(default)' if self._defaults['gap'] is True else ''}
64
+ BigM (float): {self.bigM}
65
+ """
66
+ return dedent(text)
67
+
68
+ def increment_horizon(self, value: int = 1):
69
+ # self._defaults["horizon"] = False
70
+ self.horizon += value
71
+
72
+ def get_defaults(self) -> dict[str, bool]:
73
+ return self._defaults
74
+
75
+ @classmethod
76
+ def get_config(cls) -> Config:
77
+ parser = argparse.ArgumentParser(
78
+ prog="SCIPPlan"
79
+ )
80
+ parser.add_argument(
81
+ "-D",
82
+ "--domain",
83
+ required=True,
84
+ type=str,
85
+ help="This variable is the name of the domain (e.g. pandemic or navigation)."
86
+ )
87
+ parser.add_argument(
88
+ "-I",
89
+ "--instance",
90
+ required=True,
91
+ type=str,
92
+ help="This is the instance number of the domain (e.g. navigation has instances 1, 2 and 3)."
93
+ )
94
+ parser.add_argument(
95
+ "-H",
96
+ "--horizon",
97
+ required=False,
98
+ # default=1,
99
+ type=int,
100
+ help="The initial horizon. The solve method will initially begin with this horizon until it finds a feasible solution."
101
+ )
102
+ parser.add_argument(
103
+ "-E",
104
+ "--epsilon",
105
+ required=False,
106
+ # default=0.1,
107
+ type=float,
108
+ help="SCIPPlan iteratively checks solution for violations at each epsilon value."
109
+ )
110
+ parser.add_argument(
111
+ "-G",
112
+ "--gap",
113
+ required=False,
114
+ # default=0.1,
115
+ type=float,
116
+ help="SCIP will search for solution with an optimality gap by at least this value."
117
+ )
118
+
119
+ parser.add_argument(
120
+ "--bigM",
121
+ required=False,
122
+ default=1000.0,
123
+ type=float,
124
+ help="A large value which is used for some constraint encoding formulations, defaults to 1000.0 and can be changed as needed."
125
+ )
126
+
127
+ parser.add_argument(
128
+ "--dt-var",
129
+ required=False,
130
+ default="Dt",
131
+ type=str,
132
+ help="When writing the constraints, dt_var is the variable name for Dt, defaults to 'Dt' and can be changed based on users preference (e.g. 'dt')."
133
+ )
134
+
135
+ parser.add_argument(
136
+ "--provide-sols",
137
+ action="store_true",
138
+ default=False,
139
+ help="This flag determines whether the user would like to provide a system of odes or solution equations, odes must be provided by default."
140
+ )
141
+
142
+ parser.add_argument(
143
+ "--show-output",
144
+ action="store_true",
145
+ default=False,
146
+ help="Include this flag to show output from SCIP."
147
+ )
148
+
149
+ parser.add_argument(
150
+ "--save-sols",
151
+ action="store_true",
152
+ default=False,
153
+ help="Include this flag to save the solutions from each of the scipplan iterations as well as constraints generated (note, only saves for horizon which has been solved)."
154
+ )
155
+
156
+ args = parser.parse_args()
157
+
158
+ return Config(**vars(args))
159
+
160
+
161
+ if __name__ == "__main__":
162
+
163
+ print(Config.get_config())
@@ -0,0 +1,31 @@
1
+ import os
2
+ import csv
3
+
4
+ from typing import Generator
5
+
6
+ from .config import Config
7
+
8
+ class InfeasibilityError(Exception):
9
+ """Raise this error when there are no valid solutions for the given horizon"""
10
+
11
+ def iterate(start: float, stop: float, step: float = 1) -> Generator[float, None, None]:
12
+ n = start
13
+ while n <= stop:
14
+ yield n
15
+ n += step
16
+
17
+
18
+ def list_accessible_files(directory):
19
+ try:
20
+ files = os.listdir(directory)
21
+ return files
22
+ except FileNotFoundError:
23
+ return [] # Return an empty list if the directory doesn't exist
24
+
25
+ def write_to_csv(file_name: str, data: list[dict], config: Config) -> None:
26
+ with open(f"{file_name}_{config.domain}_{config.instance}.csv", 'w', encoding='utf8', newline='') as output_file:
27
+ fc = csv.DictWriter(output_file,
28
+ fieldnames=data[0].keys(),
29
+ )
30
+ fc.writeheader()
31
+ fc.writerows(data)
@@ -0,0 +1,284 @@
1
+ import ast
2
+ import operator as op
3
+ from typing import Union
4
+
5
+ from .variables import Variable
6
+
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+ from math import exp, log, sqrt, sin, cos, isclose
10
+
11
+ from pyscipopt.scip import Model, SumExpr
12
+
13
+
14
+ def linearise(expr: ast.Compare, aux_var: Variable) -> tuple[ast.Compare, ast.Compare]:
15
+ """linearise
16
+ This function linearises an inequality using an auxilary variable
17
+
18
+ The linearisation process is as follows.
19
+ If the expression is of the form of E1 <= E2, then we linearise by using the following expressions.
20
+ z=0 ==> E1 <= E2 and z=1 ==> E1 > E2 (equivalent to E1 >= E2 + feastol). This is then equivalent to,
21
+ E2 + feastol - M + z*M <= E1 <= E2 + zM.
22
+
23
+ Similarly, for E1 < E2 we have z=0 ==> E1 < E2 which is equivalent to E1 <= E2 - feastol + z*M
24
+ and z=1 ==> E1 >= E2.
25
+ Thus for E1 < E2 we have,
26
+ E2 + z*M - M <= E1 <= E2 + z*M - feastol.
27
+
28
+ If, however, the inequality is of the form of E1 >= E2 then we evaluate the expression, E2 <= E1.
29
+ Similarly, if the expression is E1 > E2 then we evaluate the expression E2 < E1.
30
+
31
+ :param expr: An inequality expression which is linearised.
32
+ :type expr: ast.Compare
33
+ :param aux_var: An auxiliary variable used when linearising the inequality to determine if the expression is true or false.
34
+ :type aux_var: Variable
35
+ :raises ValueError: If expr is not a valid inequality (i.e. doesn't use <, <=, > and >=)
36
+ :return: both the linearised inequalities
37
+ :rtype: tuple[ast.Compare, ast.Compare]
38
+ """
39
+ if not isinstance(expr.ops[0], (ast.Lt, ast.LtE, ast.Gt, ast.GtE)):
40
+ raise ValueError("Only <, <=, > or >= are allowed")
41
+ if isinstance(expr.ops[0], ast.GtE):
42
+ expr.left, expr.comparators[0] = expr.comparators[0], expr.left
43
+ expr.ops[0] = ast.LtE()
44
+ if isinstance(expr.ops[0], ast.Gt):
45
+ expr.left, expr.comparators[0] = expr.comparators[0], expr.left
46
+ expr.ops[0] = ast.Lt()
47
+
48
+ if isinstance(expr.ops[0], ast.LtE):
49
+ lhs = ast.BinOp(
50
+ left=expr.comparators[0],
51
+ op=ast.Add(),
52
+ right=ast.parse(f"feastol - bigM + {aux_var.name} * bigM").body[0].value
53
+ )
54
+ rhs = ast.BinOp(
55
+ left=expr.comparators[0],
56
+ op=ast.Add(),
57
+ right=ast.parse(f"{aux_var.name} * bigM").body[0].value
58
+ )
59
+ if isinstance(expr.ops[0], ast.Lt):
60
+ lhs = ast.BinOp(
61
+ left=expr.comparators[0],
62
+ op=ast.Add(),
63
+ right=ast.parse(f"{aux_var.name} * bigM - bigM").body[0].value
64
+ )
65
+ rhs = ast.BinOp(
66
+ left=expr.comparators[0],
67
+ op=ast.Add(),
68
+ right=ast.parse(f"{aux_var.name} * bigM - feastol").body[0].value
69
+ )
70
+ expr1 = ast.Compare(lhs, [ast.LtE()], [expr.left])
71
+ expr2 = ast.Compare(expr.left, [ast.LtE()], [rhs])
72
+ return expr1, expr2
73
+
74
+
75
+
76
+ @dataclass
77
+ class Expressions:
78
+ expr_str: str
79
+ horizon: int
80
+ expressions: list
81
+ aux_vars: list
82
+
83
+ def add_expressions(self, *expressions):
84
+ for expression in expressions:
85
+ self.expressions.append(expression)
86
+ def __len__(self) -> int:
87
+ return len(self.expressions)
88
+ def __iter__(self):
89
+ for expr in self.expressions:
90
+ yield expr
91
+
92
+
93
+ class ParserType(Enum):
94
+ """ParserType
95
+ enum type CALCULATOR: Used to calculate using feastol
96
+ enum type PARSER: Used to parse an expression and create the correct minlp constraints
97
+ """
98
+ CALCULATOR = "calculator"
99
+ PARSER = "parser"
100
+
101
+
102
+ @dataclass
103
+ class EvalParams:
104
+ variables: dict = field(default_factory={})
105
+ functions: dict = field(default_factory={})
106
+ operators: dict = field(default_factory={})
107
+ parser_type: ParserType = ParserType.PARSER
108
+ rounds_vars: bool = False
109
+ model: Union[Model, None] = None
110
+ add_aux_vars: bool = False
111
+
112
+ @classmethod
113
+ def as_calculator(cls, variables: dict, functions: dict, operators: dict, model: Model, add_aux_vars: bool = False):
114
+ return EvalParams(variables, functions, operators, ParserType.CALCULATOR, False, model, add_aux_vars)
115
+ @classmethod
116
+ def as_parser(cls, variables: dict, functions: dict, operators: dict, model: Model, add_aux_vars: bool = False):
117
+ return EvalParams(variables, functions, operators, ParserType.PARSER, True, model, add_aux_vars)
118
+
119
+
120
+ class ParseModel:
121
+
122
+ def __init__(self, eval_params: EvalParams):
123
+ self.params = eval_params
124
+ self.variables = eval_params.variables
125
+ self.functions = eval_params.functions
126
+
127
+ self.model_feastol = eval_params.model.feastol()
128
+ self.variables["feastol"] = self.model_feastol
129
+
130
+ # Calculate operators (use evaluate operators for non calculations)
131
+ feastol = self.model_feastol if eval_params.parser_type is ParserType.CALCULATOR else 0
132
+ self.operators = {
133
+ ast.Add: op.add,
134
+ ast.Sub: op.sub,
135
+ ast.Mult: op.mul,
136
+ ast.Div: op.truediv,
137
+ ast.Pow: op.pow,
138
+
139
+ ast.UAdd: op.pos,
140
+ ast.USub: op.neg,
141
+
142
+ ast.Not: op.not_,
143
+ ast.Eq: lambda x, y: op.eq(x, y) if self.params.parser_type is ParserType.PARSER else isclose(x, y, rel_tol=feastol),
144
+ # ast.Eq: op.eq,
145
+ ast.NotEq: lambda x, y: op.ne(x, y) if self.params.parser_type is ParserType.PARSER else not isclose(x, y, rel_tol=feastol),
146
+ # ast.NotEq: op.ne,
147
+ ast.Lt: lambda x, y: op.lt(x, y + feastol),
148
+ ast.LtE: lambda x, y: op.le(x, y + feastol),
149
+ ast.Gt: lambda x, y: op.gt(x + feastol, y),
150
+ ast.GtE: lambda x, y: op.ge(x + feastol, y),
151
+ } | eval_params.operators
152
+
153
+ self.expressions = Expressions("", -1, [], [])
154
+
155
+
156
+ def evaluate(self, eqtn, aux_vars: Union[list, None] = None, expr_name: Union[str, None] = None, horizon: int = -1):
157
+
158
+ if isinstance(eqtn, str):
159
+ # Reset self.expressions for every new equation evaluated
160
+ self.expressions = Expressions(
161
+ "" if expr_name is None else expr_name,
162
+ horizon,
163
+ [],
164
+ [] if aux_vars is None else aux_vars
165
+ )
166
+
167
+ return self.evaluate(ast.parse(eqtn))
168
+
169
+ if isinstance(eqtn, ast.Module):
170
+ self.expressions.add_expressions(*(self.evaluate(expr) for expr in eqtn.body))
171
+ return self.expressions
172
+
173
+ if isinstance(eqtn, ast.Expr):
174
+ return self.evaluate(eqtn.value)
175
+
176
+ if isinstance(eqtn, ast.BoolOp):
177
+ # And expressions are decomposed into their individual segments and evaluated separately
178
+ if isinstance(eqtn.op, ast.And):
179
+ # Evaluate and add all expressions except the first to the expressions object
180
+ self.expressions.add_expressions(*(self.evaluate(expr) for expr in eqtn.values[1:]))
181
+ # Evaluate and return the first expression
182
+ return self.evaluate(eqtn.values[0])
183
+
184
+ if isinstance(eqtn.op, ast.Or):
185
+ if self.params.parser_type is ParserType.CALCULATOR:
186
+ results = [self.evaluate(expr) for expr in eqtn.values]
187
+ return any(results)
188
+ else:
189
+ aux_vars = self.expressions.aux_vars
190
+ if self.params.add_aux_vars is True and len(aux_vars) > 0:
191
+ raise Exception("aux_vars should be empty to add new aux vars")
192
+
193
+ for idx, expr in enumerate(eqtn.values):
194
+ if isinstance(expr, ast.Compare):
195
+
196
+ if self.params.add_aux_vars is True:
197
+ aux_var = Variable.create_var(
198
+ model=self.params.model,
199
+ name=f"Aux_{idx}_{self.expressions.expr_str}",
200
+ vtype="auxiliary_boolean",
201
+ time=self.expressions.horizon,
202
+ const_vals={}
203
+ )
204
+ else:
205
+ aux_var: Variable = self.expressions.aux_vars[idx]
206
+
207
+ if self.params.add_aux_vars is True:
208
+ aux_vars.append(aux_var)
209
+ self.variables[aux_var.name] = aux_var.model_var
210
+
211
+ expr1, expr2 = linearise(expr, aux_var)
212
+
213
+ self.expressions.add_expressions(self.evaluate(expr1))
214
+ self.expressions.add_expressions(self.evaluate(expr2))
215
+
216
+ else:
217
+ raise Exception("or expressions may only be made up of inequalities")
218
+ lhs = SumExpr()
219
+ for var in aux_vars:
220
+ lhs += var.model_var
221
+ return lhs <= len(aux_vars) - 1
222
+
223
+ if isinstance(eqtn, ast.IfExp):
224
+ raise Exception("Can't use if else")
225
+
226
+ if isinstance(eqtn, ast.Compare):
227
+ if len(eqtn.comparators) != 1:
228
+ raise Exception("Too many comparator operators, please don't use more than 1 per equation")
229
+
230
+ left = self.evaluate(eqtn.left)
231
+ right = self.evaluate(eqtn.comparators[0])
232
+ comp_type = type(eqtn.ops[0])
233
+ comparator = self.operators[comp_type]
234
+
235
+ return comparator(left, right)
236
+
237
+ if isinstance(eqtn, ast.Name):
238
+ return self.variables[eqtn.id]
239
+
240
+ if isinstance(eqtn, ast.BinOp):
241
+ left = self.evaluate(eqtn.left)
242
+ right = self.evaluate(eqtn.right)
243
+ operator = type(eqtn.op)
244
+ return self.operators[operator](left, right)
245
+
246
+ if isinstance(eqtn, ast.Call):
247
+ if not isinstance(eqtn.func, ast.Name):
248
+ raise Exception("Attributes such as main.secondary() functions are not allowed")
249
+
250
+ func_name = eqtn.func.id
251
+ args = (self.evaluate(arg) for arg in eqtn.args)
252
+ func = self.functions[func_name]
253
+ return func(*args)
254
+
255
+ if isinstance(eqtn, ast.Constant):
256
+ return eqtn.value
257
+
258
+ if isinstance(eqtn, ast.UnaryOp):
259
+ op_type = type(eqtn.op)
260
+ operator = self.operators[op_type]
261
+ return operator(self.evaluate(eqtn.operand))
262
+
263
+ raise Exception("Unknown ast type")
264
+
265
+
266
+
267
+ if __name__ == "__main__":
268
+ # params = EvalParams.as_calculator(variables, functions, {})
269
+ # calc = ParseModel(params).evaluate
270
+
271
+ # print(calc(constraint))
272
+ eqtns = """
273
+ 1 + 2
274
+ 1 + 5 < 5
275
+ """
276
+ print((eqtns))
277
+ # print(calc(
278
+ # """
279
+ # a >= b if a + b + c >= 5 else a - b - c == 10
280
+ # """))
281
+ # print(calc(
282
+ # """
283
+ # a + b + c >= 5 or a - b - c == 10
284
+ # """))