scipplan 0.1.0a0__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.
Files changed (54) hide show
  1. scipplan/__init__.py +5 -0
  2. scipplan/config.py +152 -0
  3. scipplan/helpers.py +30 -0
  4. scipplan/parse_model.py +275 -0
  5. scipplan/plan_model.py +257 -0
  6. scipplan/scipplan.py +196 -0
  7. scipplan/translation/constants_infection_1.txt +17 -0
  8. scipplan/translation/constants_navigation_1.txt +2 -0
  9. scipplan/translation/constants_navigation_2.txt +2 -0
  10. scipplan/translation/constants_navigation_3.txt +1 -0
  11. scipplan/translation/constants_pandemic_1.txt +17 -0
  12. scipplan/translation/goals_infection_1.txt +3 -0
  13. scipplan/translation/goals_navigation_1.txt +2 -0
  14. scipplan/translation/goals_navigation_2.txt +2 -0
  15. scipplan/translation/goals_navigation_3.txt +2 -0
  16. scipplan/translation/goals_pandemic_1.txt +3 -0
  17. scipplan/translation/initials_infection_1.txt +5 -0
  18. scipplan/translation/initials_navigation_1.txt +4 -0
  19. scipplan/translation/initials_navigation_2.txt +4 -0
  20. scipplan/translation/initials_navigation_3.txt +4 -0
  21. scipplan/translation/initials_pandemic_1.txt +5 -0
  22. scipplan/translation/instantaneous_constraints_infection_1.txt +12 -0
  23. scipplan/translation/instantaneous_constraints_navigation_1.txt +9 -0
  24. scipplan/translation/instantaneous_constraints_navigation_2.txt +10 -0
  25. scipplan/translation/instantaneous_constraints_navigation_3.txt +11 -0
  26. scipplan/translation/instantaneous_constraints_pandemic_1.txt +12 -0
  27. scipplan/translation/pvariables_infection_1.txt +12 -0
  28. scipplan/translation/pvariables_navigation_1.txt +8 -0
  29. scipplan/translation/pvariables_navigation_2.txt +8 -0
  30. scipplan/translation/pvariables_navigation_3.txt +8 -0
  31. scipplan/translation/pvariables_pandemic_1.txt +12 -0
  32. scipplan/translation/reward_infection_1.txt +1 -0
  33. scipplan/translation/reward_navigation_1.txt +1 -0
  34. scipplan/translation/reward_navigation_2.txt +1 -0
  35. scipplan/translation/reward_navigation_3.txt +1 -0
  36. scipplan/translation/reward_pandemic_1.txt +1 -0
  37. scipplan/translation/temporal_constraints_infection_1.txt +1 -0
  38. scipplan/translation/temporal_constraints_navigation_1.txt +6 -0
  39. scipplan/translation/temporal_constraints_navigation_2.txt +6 -0
  40. scipplan/translation/temporal_constraints_navigation_3.txt +7 -0
  41. scipplan/translation/temporal_constraints_pandemic_1.txt +1 -0
  42. scipplan/translation/transitions_infection_1.txt +7 -0
  43. scipplan/translation/transitions_navigation_1.txt +4 -0
  44. scipplan/translation/transitions_navigation_2.txt +4 -0
  45. scipplan/translation/transitions_navigation_3.txt +4 -0
  46. scipplan/translation/transitions_pandemic_1.txt +7 -0
  47. scipplan/variables.py +91 -0
  48. scipplan/zero_crossing.py +28 -0
  49. scipplan-0.1.0a0.dist-info/LICENSE +19 -0
  50. scipplan-0.1.0a0.dist-info/METADATA +215 -0
  51. scipplan-0.1.0a0.dist-info/RECORD +54 -0
  52. scipplan-0.1.0a0.dist-info/WHEEL +6 -0
  53. scipplan-0.1.0a0.dist-info/entry_points.txt +2 -0
  54. scipplan-0.1.0a0.dist-info/top_level.txt +1 -0
scipplan/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ __version__ = "0.1.0alpha0"
2
+ print(f"SCIPPlan Version: {__version__}")
3
+ __release__ = "v0.1.0"
4
+ __author__ = "Ari Gestetner, Buser Say"
5
+ __email__ = "ages0001@student.monash.edu, buser.say@monash.edu"
scipplan/config.py ADDED
@@ -0,0 +1,152 @@
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: int
16
+ horizon: int = field(default=None)
17
+ epsilon: float = field(default=None)
18
+ gap: float = field(default=None)
19
+ show_output: bool = False
20
+ save_sols: bool = False
21
+ bigM: float = 1000.0
22
+ dt_var: str = "Dt"
23
+ _defaults: dict[str, bool] = field(default_factory=dict, repr=False)
24
+
25
+ def __post_init__(self) -> None:
26
+ # Set all defaults to False and if the value is None then it will be updated to true
27
+ self._defaults = {
28
+ "domain": False,
29
+ "instance": False,
30
+ "horizon": False,
31
+ "epsilon": False,
32
+ "gap": False
33
+ }
34
+ if self.horizon is None:
35
+ print("Horizon is not provided, and is set to 1")
36
+ self.horizon = 1
37
+ self._defaults["horizon"] = True
38
+
39
+ if self.epsilon is None:
40
+ print("Epsilon is not provided, and is set to 0.1")
41
+ self.epsilon = 0.1
42
+ self._defaults["epsilon"] = True
43
+
44
+ if self.gap is None:
45
+ print("Gap is not provided, and is set to 10.0%")
46
+ self.gap = 0.1
47
+ self._defaults["gap"] = True
48
+
49
+ def __str__(self) -> str:
50
+ text = f"""
51
+ Configuration:
52
+
53
+ Display SCIP Output: {self.show_output}
54
+ Save Solutions: {self.show_output}
55
+ Dt Variable Name: {self.dt_var}
56
+
57
+ Domain (str): {self.domain}
58
+ Instance (int): {self.instance}
59
+ Horizon (int): {self.horizon} {'(default)' if self._defaults['horizon'] is True else ''}
60
+ Epsilon (float): {self.epsilon} {'(default)' if self._defaults['epsilon'] is True else ''}
61
+ Gap (float): {self.gap * 100}% {'(default)' if self._defaults['gap'] is True else ''}
62
+ BigM (float): {self.bigM}
63
+ """
64
+ return dedent(text)
65
+
66
+ def increment_horizon(self, value: int = 1):
67
+ self._defaults["horizon"] = False
68
+ self.horizon += value
69
+
70
+
71
+ @classmethod
72
+ def get_config(cls) -> Config:
73
+ parser = argparse.ArgumentParser(
74
+ prog="SCIPPlan"
75
+ )
76
+ parser.add_argument(
77
+ "-D",
78
+ "--domain",
79
+ required=True,
80
+ type=str,
81
+ help="This variable is the name of the domain (e.g. pandemic or navigation)"
82
+ )
83
+ parser.add_argument(
84
+ "-I",
85
+ "--instance",
86
+ required=True,
87
+ type=int,
88
+ help="This is the instance number of the domain (e.g. navigation has instances 1, 2 and 3)"
89
+ )
90
+ parser.add_argument(
91
+ "-H",
92
+ "--horizon",
93
+ required=False,
94
+ # default=1,
95
+ type=int,
96
+ help="The initial horizon. The solve method will initially begin with this horizon until it finds a feasible solution"
97
+ )
98
+ parser.add_argument(
99
+ "-E",
100
+ "--epsilon",
101
+ required=False,
102
+ # default=0.1,
103
+ type=float,
104
+ help="SCIPPlan iteratively checks solution for violations at each epsilon value"
105
+ )
106
+ parser.add_argument(
107
+ "-G",
108
+ "--gap",
109
+ required=False,
110
+ # default=0.1,
111
+ type=float,
112
+ help="SCIP will search for solution with an optimality gap by at least this value"
113
+ )
114
+
115
+ parser.add_argument(
116
+ "--bigM",
117
+ required=False,
118
+ default=1000.0,
119
+ type=float,
120
+ help="A large value which is used for some constraint encoding formulations, defaults to 1000.0 and can be changed as needed"
121
+ )
122
+
123
+ parser.add_argument(
124
+ "--dt-var",
125
+ required=False,
126
+ default="Dt",
127
+ type=str,
128
+ 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')"
129
+ )
130
+
131
+ parser.add_argument(
132
+ "--show-output",
133
+ action="store_true",
134
+ default=False,
135
+ help="Include this flag to show output from SCIP"
136
+ )
137
+
138
+ parser.add_argument(
139
+ "--save-sols",
140
+ action="store_true",
141
+ default=False,
142
+ 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)"
143
+ )
144
+
145
+ args = parser.parse_args()
146
+
147
+ return Config(**vars(args))
148
+
149
+
150
+ if __name__ == "__main__":
151
+
152
+ print(Config.get_config())
scipplan/helpers.py ADDED
@@ -0,0 +1,30 @@
1
+ import os
2
+ import csv
3
+
4
+ from typing import Generator
5
+ from .config import Config
6
+
7
+ class InfeasibilityError(Exception):
8
+ """Raise this error when there are no valid solutions for the given horizon"""
9
+
10
+ def iterate(start: float, stop: float, step: float = 1) -> Generator[float, None, None]:
11
+ n = start
12
+ while n <= stop:
13
+ yield n
14
+ n += step
15
+
16
+
17
+ def list_accessible_files(directory):
18
+ try:
19
+ files = os.listdir(directory)
20
+ return files
21
+ except FileNotFoundError:
22
+ return [] # Return an empty list if the directory doesn't exist
23
+
24
+ def write_to_csv(file_name: str, data: list[dict], config: Config) -> None:
25
+ with open(f"{file_name}_{config.domain}_{config.instance}.csv", 'w', encoding='utf8', newline='') as output_file:
26
+ fc = csv.DictWriter(output_file,
27
+ fieldnames=data[0].keys(),
28
+ )
29
+ fc.writeheader()
30
+ fc.writerows(data)
@@ -0,0 +1,275 @@
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
+ def switch_comparator(comparator):
14
+ if isinstance(comparator, ast.Eq): return ast.NotEq()
15
+ if isinstance(comparator, ast.NotEq): return ast.Eq()
16
+ if isinstance(comparator, ast.Lt): return ast.Gt()
17
+ if isinstance(comparator, ast.LtE): return ast.GtE()
18
+ if isinstance(comparator, ast.Gt): return ast.Lt()
19
+ if isinstance(comparator, ast.GtE): return ast.LtE()
20
+
21
+ @dataclass
22
+ class Expressions:
23
+ expr_str: str
24
+ horizon: int
25
+ expressions: list
26
+ aux_vars: list
27
+
28
+ def add_expressions(self, *expressions):
29
+ for expression in expressions:
30
+ self.expressions.append(expression)
31
+ def __len__(self) -> int:
32
+ return len(self.expressions)
33
+ def __iter__(self):
34
+ for expr in self.expressions:
35
+ yield expr
36
+
37
+
38
+ class ParserType(Enum):
39
+ """ParserType
40
+ "enum type CALCULATOR: Used to calculate using feastol
41
+ "enum type PARSER: Used to parse an expression and create the correct minlp constraints
42
+ """
43
+ CALCULATOR = "calculator"
44
+ PARSER = "parser"
45
+
46
+
47
+ @dataclass
48
+ class EvalParams:
49
+ variables: dict = field(default_factory={})
50
+ functions: dict = field(default_factory={})
51
+ operators: dict = field(default_factory={})
52
+ parser_type: ParserType = ParserType.PARSER
53
+ rounds_vars: bool = False
54
+ model: Union[Model, None] = None
55
+ add_aux_vars: bool = False
56
+
57
+ @classmethod
58
+ def as_calculator(cls, variables: dict, functions: dict, operators: dict, model: Model, add_aux_vars: bool = False):
59
+ return EvalParams(variables, functions, operators, ParserType.CALCULATOR, False, model, add_aux_vars)
60
+ @classmethod
61
+ def as_parser(cls, variables: dict, functions: dict, operators: dict, model: Model, add_aux_vars: bool = False):
62
+ return EvalParams(variables, functions, operators, ParserType.PARSER, True, model, add_aux_vars)
63
+
64
+
65
+ class ParseModel:
66
+
67
+ def __init__(self, eval_params: EvalParams):
68
+ self.params = eval_params
69
+ self.variables = eval_params.variables
70
+ self.functions = eval_params.functions
71
+
72
+ self.model_feastol = eval_params.model.feastol()
73
+ self.variables["feastol"] = self.model_feastol
74
+
75
+ # Calculate operators (use evaluate operators for non calculations)
76
+ feastol = self.model_feastol if eval_params.parser_type is ParserType.CALCULATOR else 0
77
+ self.operators = {
78
+ ast.Add: op.add,
79
+ ast.Sub: op.sub,
80
+ ast.Mult: op.mul,
81
+ ast.Div: op.truediv,
82
+ ast.Pow: op.pow,
83
+
84
+ ast.UAdd: op.pos,
85
+ ast.USub: op.neg,
86
+
87
+ ast.Not: op.not_,
88
+ ast.Eq: lambda x, y: op.eq(x, y) if self.params.parser_type is ParserType.PARSER else isclose(x, y, rel_tol=feastol),
89
+ # ast.Eq: op.eq,
90
+ 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),
91
+ # ast.NotEq: op.ne,
92
+ ast.Lt: lambda x, y: op.lt(x, y + feastol),
93
+ ast.LtE: lambda x, y: op.le(x, y + feastol),
94
+ ast.Gt: lambda x, y: op.gt(x + feastol, y),
95
+ ast.GtE: lambda x, y: op.ge(x + feastol, y),
96
+ } | eval_params.operators
97
+
98
+ self.expressions = Expressions("", -1, [], [])
99
+
100
+
101
+ def evaluate(self, eqtn, aux_vars: Union[list, None] = None, expr_name: Union[str, None] = None, horizon: int = -1):
102
+
103
+ if isinstance(eqtn, str):
104
+ # Reset self.expressions for every new equation evaluated
105
+ self.expressions = Expressions(
106
+ "" if expr_name is None else expr_name,
107
+ horizon,
108
+ [],
109
+ [] if aux_vars is None else aux_vars
110
+ )
111
+
112
+ return self.evaluate(ast.parse(eqtn))
113
+
114
+ if isinstance(eqtn, ast.Module):
115
+ self.expressions.add_expressions(*(self.evaluate(expr) for expr in eqtn.body))
116
+ return self.expressions
117
+
118
+ if isinstance(eqtn, ast.Expr):
119
+ return self.evaluate(eqtn.value)
120
+
121
+ if isinstance(eqtn, ast.BoolOp):
122
+ # And expressions are decomposed into their individual segments and evaluated separately
123
+ if isinstance(eqtn.op, ast.And):
124
+ # Evaluate and add all expressions except the first to the expressions object
125
+ self.expressions.add_expressions(*(self.evaluate(expr) for expr in eqtn.values[1:]))
126
+ # Evaluate and return the first expression
127
+ return self.evaluate(eqtn.values[0])
128
+
129
+ if isinstance(eqtn.op, ast.Or):
130
+ if self.params.parser_type is ParserType.CALCULATOR:
131
+ results = [self.evaluate(expr) for expr in eqtn.values]
132
+ return any(results)
133
+ else:
134
+ aux_vars = self.expressions.aux_vars
135
+ if self.params.add_aux_vars is True and len(aux_vars) > 0:
136
+ raise Exception("aux_vars should be empty to add new aux vars")
137
+
138
+ for idx, expr in enumerate(eqtn.values):
139
+ if isinstance(expr, ast.Compare):
140
+
141
+ if self.params.add_aux_vars is True:
142
+ aux_var = Variable.create_var(
143
+ model=self.params.model,
144
+ name=f"Aux_{idx}_{self.expressions.expr_str}",
145
+ vtype="auxiliary_boolean",
146
+ time=self.expressions.horizon,
147
+ const_vals={}
148
+ )
149
+ else:
150
+ aux_var: Variable = self.expressions.aux_vars[idx]
151
+
152
+ aux_vars.append(aux_var)
153
+ self.variables[aux_var.name] = aux_var.model_var
154
+
155
+ expr.left = ast.BinOp(
156
+ left=expr.left,
157
+ op=(ast.Add() if isinstance(expr.ops[0], (ast.Gt, ast.GtE)) else ast.Sub()),
158
+ right=ast.parse(f"bigM * {aux_var.name}").body[0].value
159
+ )
160
+ self.expressions.add_expressions(self.evaluate(expr))
161
+
162
+ expr.ops[0] = switch_comparator(expr.ops[0])
163
+
164
+ expr.comparators[0] = ast.BinOp(
165
+ left=expr.comparators[0],
166
+ op=(ast.Add() if isinstance(expr.ops[0], (ast.Gt, ast.GtE)) else ast.Sub()),
167
+ right=ast.parse(f"feastol - bigM").body[0].value
168
+ )
169
+
170
+ self.expressions.add_expressions(self.evaluate(expr))
171
+ else:
172
+ raise Exception("or expressions may only be made up of inequalities")
173
+ lhs = SumExpr()
174
+ for var in aux_vars:
175
+ lhs += var.model_var
176
+ return lhs <= len(aux_vars) - 1
177
+
178
+ if isinstance(eqtn, ast.IfExp):
179
+ raise Exception("Can't use if else")
180
+
181
+ if isinstance(eqtn, ast.Compare):
182
+ if len(eqtn.comparators) != 1:
183
+ raise Exception("Too many comparator operators, please don't use more than 1 per equation")
184
+
185
+ left = self.evaluate(eqtn.left)
186
+ right = self.evaluate(eqtn.comparators[0])
187
+ comp_type = type(eqtn.ops[0])
188
+ comparator = self.operators[comp_type]
189
+
190
+ return comparator(left, right)
191
+
192
+ if isinstance(eqtn, ast.Name):
193
+ return self.variables[eqtn.id]
194
+
195
+ if isinstance(eqtn, ast.BinOp):
196
+ left = self.evaluate(eqtn.left)
197
+ right = self.evaluate(eqtn.right)
198
+ operator = type(eqtn.op)
199
+ return self.operators[operator](left, right)
200
+
201
+ if isinstance(eqtn, ast.Call):
202
+ if not isinstance(eqtn.func, ast.Name):
203
+ raise Exception("Attributes such as main.secondary() functions are not allowed")
204
+
205
+ func_name = eqtn.func.id
206
+ args = (self.evaluate(arg) for arg in eqtn.args)
207
+ func = self.functions[func_name]
208
+ return func(*args)
209
+
210
+ if isinstance(eqtn, ast.Constant):
211
+ return eqtn.value
212
+
213
+ if isinstance(eqtn, ast.UnaryOp):
214
+ op_type = type(eqtn.op)
215
+ operator = self.operators[op_type]
216
+ return operator(self.evaluate(eqtn.operand))
217
+
218
+ raise Exception("Unknown ast type")
219
+
220
+
221
+ functions = {
222
+ 'exp': exp,
223
+ 'log': log,
224
+ 'sqrt': sqrt,
225
+ 'sin': sin,
226
+ 'cos': cos,
227
+ }
228
+ variables = {
229
+ 'c': 0.15,
230
+ 'Removed': 634.9055920077616,
231
+ 'Removed_dash': 1811.3128718631383,
232
+ 'b2': 0.1,
233
+ 'Makespan': 180.0,
234
+ 'Susceptible': 1218.7473517769768,
235
+ 'Susceptible_dash': 87.1003300803328,
236
+ 'DurationMax': 90.0,
237
+ 'N': 2000.0,
238
+ 'Dt': 0.0,
239
+ 'Infected': 146.34705621526638,
240
+ 'Infected_dash': 101.58679805652946,
241
+ 'TotalTime': 90.87927389935685,
242
+ 'TotalTime_dash': 136.34829117841537,
243
+ 'b': 0.19999999999999973,
244
+ 'Epsilon': 1.0,
245
+ 'Percentage': 0.8,
246
+ 'DurationMin': 30.0,
247
+ 'b1': 0.2,
248
+ 'Duration': 100.0,
249
+ 'D': 150.0,
250
+ 'InitialInfected': 50.0,
251
+ 'Lockdown': 0.0
252
+ }
253
+
254
+ constraint = "Infected * exp((b / (b - c)) * log(1 + (Infected / Susceptible))) * exp(-1.0 * (b / (b - c)) * log(1 + (Infected / Susceptible) * exp((b - c) * Dt))) * exp((b - c) * Dt) <= D"
255
+
256
+
257
+
258
+ if __name__ == "__main__":
259
+ params = EvalParams.as_calculator(variables, functions, {})
260
+ calc = ParseModel(params).evaluate
261
+
262
+ print(calc(constraint))
263
+ eqtns = """
264
+ 1 + 2
265
+ 1 + 5 < 5
266
+ """
267
+ print(calc(eqtns))
268
+ # print(calc(
269
+ # """
270
+ # a >= b if a + b + c >= 5 else a - b - c == 10
271
+ # """))
272
+ # print(calc(
273
+ # """
274
+ # a + b + c >= 5 or a - b - c == 10
275
+ # """))