psyke 0.4.9.dev6__py3-none-any.whl → 1.0.4.dev10__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 (42) hide show
  1. psyke/__init__.py +231 -85
  2. psyke/clustering/__init__.py +9 -4
  3. psyke/clustering/cream/__init__.py +6 -10
  4. psyke/clustering/exact/__init__.py +17 -11
  5. psyke/clustering/utils.py +0 -1
  6. psyke/extraction/__init__.py +25 -0
  7. psyke/extraction/cart/CartPredictor.py +128 -0
  8. psyke/extraction/cart/FairTree.py +205 -0
  9. psyke/extraction/cart/FairTreePredictor.py +56 -0
  10. psyke/extraction/cart/__init__.py +48 -62
  11. psyke/extraction/hypercubic/__init__.py +187 -47
  12. psyke/extraction/hypercubic/cosmik/__init__.py +47 -0
  13. psyke/extraction/hypercubic/creepy/__init__.py +24 -29
  14. psyke/extraction/hypercubic/divine/__init__.py +86 -0
  15. psyke/extraction/hypercubic/ginger/__init__.py +100 -0
  16. psyke/extraction/hypercubic/gridex/__init__.py +45 -84
  17. psyke/extraction/hypercubic/gridrex/__init__.py +4 -4
  18. psyke/extraction/hypercubic/hex/__init__.py +104 -0
  19. psyke/extraction/hypercubic/hypercube.py +275 -72
  20. psyke/extraction/hypercubic/iter/__init__.py +45 -46
  21. psyke/extraction/hypercubic/strategy.py +13 -9
  22. psyke/extraction/real/__init__.py +24 -29
  23. psyke/extraction/real/utils.py +2 -2
  24. psyke/extraction/trepan/__init__.py +24 -19
  25. psyke/genetic/__init__.py +0 -0
  26. psyke/genetic/fgin/__init__.py +74 -0
  27. psyke/genetic/gin/__init__.py +144 -0
  28. psyke/hypercubepredictor.py +102 -0
  29. psyke/schema/__init__.py +230 -36
  30. psyke/tuning/__init__.py +40 -28
  31. psyke/tuning/crash/__init__.py +33 -64
  32. psyke/tuning/orchid/__init__.py +21 -23
  33. psyke/tuning/pedro/__init__.py +70 -56
  34. psyke/utils/logic.py +8 -8
  35. psyke/utils/plot.py +79 -3
  36. {psyke-0.4.9.dev6.dist-info → psyke-1.0.4.dev10.dist-info}/METADATA +42 -22
  37. psyke-1.0.4.dev10.dist-info/RECORD +46 -0
  38. {psyke-0.4.9.dev6.dist-info → psyke-1.0.4.dev10.dist-info}/WHEEL +1 -1
  39. {psyke-0.4.9.dev6.dist-info → psyke-1.0.4.dev10.dist-info/licenses}/LICENSE +2 -1
  40. psyke/extraction/cart/predictor.py +0 -73
  41. psyke-0.4.9.dev6.dist-info/RECORD +0 -36
  42. {psyke-0.4.9.dev6.dist-info → psyke-1.0.4.dev10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,144 @@
1
+ from statistics import mode
2
+
3
+ import numpy as np
4
+ from deap import base, creator, tools, algorithms
5
+ import random
6
+ from sklearn.linear_model import LinearRegression
7
+ from sklearn.metrics import mean_absolute_error, r2_score, mean_squared_error, f1_score, accuracy_score
8
+ from sklearn.preprocessing import PolynomialFeatures
9
+
10
+ from psyke import Target
11
+
12
+
13
+ class GIn:
14
+
15
+ def __init__(self, train, valid, features, sigmas, slices, min_rules=1, poly=1, alpha=0.5, indpb=0.5, tournsize=3,
16
+ metric='R2', output=Target.REGRESSION, warm=False):
17
+ self.X, self.y = train
18
+ self.valid = valid
19
+ self.output = output
20
+
21
+ self.features = features
22
+ self.sigmas = sigmas
23
+ self.slices = slices
24
+ self.min_rules = min_rules
25
+ self.poly = PolynomialFeatures(degree=poly, include_bias=False)
26
+
27
+ self.alpha = alpha
28
+ self.indpb = indpb
29
+ self.tournsize = tournsize
30
+ self.metric = metric
31
+
32
+ self.toolbox = None
33
+ self.stats = None
34
+ self.hof = None
35
+ self.best = None
36
+
37
+ self.__setup(warm)
38
+
39
+ def _region(self, x, cuts):
40
+ indices = [np.searchsorted(np.array(cut), x[f].to_numpy(), side='right')
41
+ for cut, f in zip(cuts, self.features)]
42
+
43
+ regions = np.zeros(len(x), dtype=int)
44
+ multiplier = 1
45
+ for idx, n in zip(reversed(indices), reversed([len(cut) + 1 for cut in cuts])):
46
+ regions += idx * multiplier
47
+ multiplier *= n
48
+
49
+ return regions
50
+
51
+ def _output_estimation(self, mask, to_pred):
52
+ if self.output == Target.REGRESSION:
53
+ return LinearRegression().fit(self.poly.fit_transform(self.X)[mask], self.y[mask]).predict(
54
+ self.poly.fit_transform(to_pred))
55
+ if self.output == Target.CONSTANT:
56
+ return np.mean(self.y[mask])
57
+ if self.output == Target.CLASSIFICATION:
58
+ return mode(self.y[mask])
59
+ raise ValueError('Supported outputs are Target.{REGRESSION, CONSTANT, CLASSIFICATION}')
60
+
61
+ def _score(self, true, pred):
62
+ if self.metric == 'R2':
63
+ return r2_score(true, pred)
64
+ if self.metric == 'MAE':
65
+ return -mean_absolute_error(true, pred)
66
+ if self.metric == 'MSE':
67
+ return -mean_squared_error(true, pred)
68
+ if self.metric == 'F1':
69
+ return f1_score(true, pred, average='weighted')
70
+ if self.metric == 'ACC':
71
+ return accuracy_score(true, pred)
72
+ raise ValueError('Supported metrics are R2, MAE, MSE, F1, ACC')
73
+
74
+ def predict(self, to_pred):
75
+ return self.__predict(to_pred=to_pred)[0]
76
+
77
+ def _get_cuts(self, individual):
78
+ boundaries = np.cumsum([0] + list(self.slices))
79
+ return [sorted(individual[boundaries[i]:boundaries[i + 1]]) for i in range(len(self.slices))]
80
+
81
+ def __predict(self, individual=None, to_pred=None):
82
+ cuts = self._get_cuts(individual or self.best)
83
+
84
+ regions = self._region(to_pred, cuts)
85
+ regionsT = self._region(self.X, cuts)
86
+
87
+ pred = np.empty(len(to_pred), dtype=f'U{self.y.str.len().max()}') if self.output == Target.CLASSIFICATION \
88
+ else np.zeros(len(to_pred))
89
+ valid_regions = 0
90
+
91
+ for r in range(np.prod([s + 1 for s in self.slices])):
92
+ mask = regions == r
93
+ maskT = regionsT == r
94
+ if min(mask.sum(), maskT.sum()) < 3:
95
+ if self.output != Target.CLASSIFICATION:
96
+ pred[mask] = np.mean(self.y)
97
+ continue
98
+ pred[mask] = self._output_estimation(maskT, to_pred[mask])
99
+ valid_regions += 1
100
+
101
+ return pred, valid_regions
102
+
103
+ def _evaluate(self, individual=None):
104
+ y_pred, valid_regions = self.__predict(individual or self.best, self.X if self.valid is None else self.valid[0])
105
+ if valid_regions < self.min_rules:
106
+ return -9999,
107
+ return self._score(self.y if self.valid is None else self.valid[1], y_pred),
108
+
109
+ def __setup(self, warm=False):
110
+ if not warm:
111
+ creator.create("FitnessMax", base.Fitness, weights=(1.0,))
112
+ creator.create("Individual", list, fitness=creator.FitnessMax)
113
+
114
+ self.toolbox = base.Toolbox()
115
+ for f in self.features:
116
+ self.toolbox.register(f, random.uniform, self.X[f].min(), self.X[f].max())
117
+
118
+ self.toolbox.register("individual", tools.initCycle, creator.Individual,
119
+ (sum([[getattr(self.toolbox, f) for i in range(s)]
120
+ for f, s in zip(self.features, self.slices)], [])), n=1)
121
+
122
+ self.toolbox.register("population", tools.initRepeat, list, self.toolbox.individual)
123
+
124
+ self.toolbox.register("mate", tools.cxBlend, alpha=self.alpha)
125
+ self.toolbox.register("mutate", tools.mutGaussian, indpb=self.indpb, mu=0,
126
+ sigma=sum([[sig] * s for sig, s in zip(self.sigmas, self.slices)], []))
127
+ self.toolbox.register("select", tools.selTournament, tournsize=self.tournsize)
128
+ self.toolbox.register("evaluate", self._evaluate)
129
+
130
+ self.stats = tools.Statistics(lambda ind: ind.fitness.values[0])
131
+ self.stats.register("avg", np.mean)
132
+ # self.stats.register("min", np.min)
133
+ self.stats.register("max", np.max)
134
+ # self.stats.register("std", np.std)
135
+
136
+ self.hof = tools.HallOfFame(1)
137
+
138
+ def run(self, n_pop=30, cxpb=0.8, mutpb=0.5, n_gen=50, seed=123):
139
+ random.seed(seed)
140
+ pop = self.toolbox.population(n=n_pop)
141
+ result, log = algorithms.eaSimple(pop, self.toolbox, cxpb=cxpb, mutpb=mutpb, ngen=n_gen,
142
+ stats=self.stats, halloffame=self.hof, verbose=False)
143
+ self.best = tools.selBest(pop, 1)[0]
144
+ return self.best, self._evaluate()[0], result, log
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from sklearn.neighbors import BallTree
8
+
9
+ from psyke import EvaluableModel, Target, get_int_precision
10
+ from psyke.extraction.hypercubic import RegressionCube, GenericCube, Point
11
+
12
+
13
+ class HyperCubePredictor(EvaluableModel):
14
+ def __init__(self, output=Target.CONSTANT, discretization=None, normalization=None):
15
+ super().__init__(discretization, normalization)
16
+ self._hypercubes = []
17
+ self._dimensions_to_ignore = set()
18
+ self._output = output
19
+ self._surrounding = None
20
+
21
+ def _predict(self, dataframe: pd.DataFrame) -> Iterable:
22
+ return np.array([self._predict_from_cubes(row.to_dict()) for _, row in dataframe.iterrows()])
23
+
24
+ def _brute_predict(self, dataframe: pd.DataFrame, criterion: str = 'corner', n: int = 2) -> Iterable:
25
+ predictions = np.array(self._predict(dataframe))
26
+ idx = [prediction is None for prediction in predictions]
27
+ if sum(idx) > 0:
28
+ if criterion == 'default':
29
+ predictions[idx] = np.array([HyperCubePredictor._get_cube_output(
30
+ self._surrounding, row
31
+ ) for _, row in dataframe[idx].iterrows()])
32
+ elif criterion == 'surface':
33
+ predictions[idx] = np.array([HyperCubePredictor._get_cube_output(self._brute_predict_surface(row), row)
34
+ for _, row in dataframe[idx].iterrows()])
35
+ else:
36
+ tree, cubes = self._create_brute_tree(criterion, n)
37
+ predictions[idx] = np.array([HyperCubePredictor._brute_predict_from_cubes(
38
+ row.to_dict(), tree, cubes
39
+ ) for _, row in dataframe[idx].iterrows()])
40
+ return np.array(predictions)
41
+
42
+ @staticmethod
43
+ def _brute_predict_from_cubes(row: dict[str, float], tree: BallTree,
44
+ cubes: list[GenericCube]) -> float | str:
45
+ idx = tree.query([list(row.values())], k=1)[1][0][0]
46
+ return HyperCubePredictor._get_cube_output(cubes[idx], row)
47
+
48
+ def _brute_predict_surface(self, row: pd.Series) -> GenericCube:
49
+ return min([(
50
+ cube.surface_distance(Point(list(row.keys()), list(row.values))), cube.volume(), cube
51
+ ) for cube in self._hypercubes])[-1]
52
+
53
+ def _create_brute_tree(self, criterion: str = 'center', n: int = 2) -> (BallTree, list[GenericCube]):
54
+ admissible_criteria = ['surface', 'center', 'corner', 'perimeter', 'density', 'default']
55
+ if criterion not in admissible_criteria:
56
+ raise NotImplementedError(
57
+ "'criterion' should be chosen in " + str(admissible_criteria)
58
+ )
59
+
60
+ points = [(cube.center, cube) for cube in self._hypercubes] if criterion == 'center' else \
61
+ [(cube.barycenter, cube) for cube in self._hypercubes] if criterion == 'density' else \
62
+ [(corner, cube) for cube in self._hypercubes for corner in cube.corners()] if criterion == 'corner' else \
63
+ [(point, cube) for cube in self._hypercubes for point in cube.perimeter_samples(n)] \
64
+ if criterion == 'perimeter' else None
65
+
66
+ return BallTree(pd.concat([point[0].to_dataframe() for point in points], ignore_index=True)), \
67
+ [point[1] for point in points]
68
+
69
+ def _predict_from_cubes(self, data: dict[str, float]) -> float | str | None:
70
+ cube = self._find_cube(data)
71
+ if cube is None:
72
+ return None
73
+ elif self._output == Target.CLASSIFICATION:
74
+ return HyperCubePredictor._get_cube_output(cube, data)
75
+ else:
76
+ return round(HyperCubePredictor._get_cube_output(cube, data), get_int_precision())
77
+
78
+ def _find_cube(self, data: dict[str, float]) -> GenericCube | None:
79
+ if not self._hypercubes:
80
+ return None
81
+ data = data.copy()
82
+ for dimension in self._dimensions_to_ignore:
83
+ if dimension in data:
84
+ del data[dimension]
85
+ for cube in self._hypercubes:
86
+ if data in cube:
87
+ return cube.copy()
88
+ if self._hypercubes[-1].is_default:
89
+ return self._hypercubes[-1].copy()
90
+
91
+ @property
92
+ def n_rules(self):
93
+ return len(list(self._hypercubes))
94
+
95
+ @property
96
+ def volume(self):
97
+ return sum([cube.volume() for cube in self._hypercubes])
98
+
99
+ @staticmethod
100
+ def _get_cube_output(cube, data: dict[str, float]) -> float:
101
+ return cube.output.predict(pd.DataFrame([data])).flatten()[0] if \
102
+ isinstance(cube, RegressionCube) else cube.output
psyke/schema/__init__.py CHANGED
@@ -3,14 +3,27 @@ import math
3
3
  from typing import Callable
4
4
  from psyke.utils import get_int_precision
5
5
 
6
+
7
+ class SchemaException(Exception):
8
+
9
+ def __init__(self, message: str):
10
+ super().__init__(message)
11
+
12
+
6
13
  _EMPTY_INTERSECTION_EXCEPTION: Callable = lambda x, y: \
7
- Exception("Empty intersection between two Value: " + str(x) + ' and ' + str(y))
14
+ SchemaException(f"Empty intersection between two Value: {str(x)} and {str(y)}")
8
15
 
9
16
  _NOT_IMPLEMENTED_INTERSECTION: Callable = lambda x, y: \
10
- Exception("Not implemented intersection between: " + str(x) + ' and ' + str(y))
17
+ SchemaException(f"Not implemented intersection between: {str(x)} and {str(y)}")
11
18
 
12
- _INTERSECTION_WITH_WRONG_TYPE: Callable = lambda x, y: \
13
- Exception("Calling method with wrong type argument: " + str(x) + ' and ' + str(y))
19
+ _OPERATION_WITH_WRONG_TYPE: Callable = lambda x, y: \
20
+ SchemaException("Calling method with wrong type argument: " + str(x) + ' and ' + str(y))
21
+
22
+ _EMPTY_UNION_EXCEPTION: Callable = lambda x, y: \
23
+ SchemaException(f"Empty union between two Value: {str(x)} and {str(y)}")
24
+
25
+ _NOT_IMPLEMENTED_UNION: Callable = lambda x, y: \
26
+ SchemaException("Not implemented union between: " + str(x) + ' and ' + str(y))
14
27
 
15
28
  PRECISION = get_int_precision()
16
29
  STRING_PRECISION = str(PRECISION)
@@ -85,61 +98,78 @@ class Value:
85
98
  else:
86
99
  return False
87
100
 
101
+ def __neg__(self) -> Value:
102
+ if isinstance(self, Constant):
103
+ return self
104
+ elif isinstance(self, GreaterThan):
105
+ return LessThan(self.value, self.standard)
106
+ elif isinstance(self, LessThan):
107
+ return GreaterThan(self.value, self.standard)
108
+ elif isinstance(self, Between):
109
+ return Outside(self.lower, self.upper, self.standard)
110
+ elif isinstance(self, Outside):
111
+ return Between(self.lower, self.upper, self.standard)
112
+ else:
113
+ raise TypeError
114
+
88
115
  # TODO: handle convention (low priority).
89
116
  def __mul__(self, other) -> Value:
90
117
 
91
118
  def intersection_with_constant(first_value: Constant, second_value: Value) -> Value:
92
119
  if isinstance(first_value, Constant):
93
- if second_value.is_in(first_value.value):
120
+ if first_value in second_value:
94
121
  return first_value
95
122
  else:
96
123
  raise _EMPTY_INTERSECTION_EXCEPTION(first_value, second_value)
97
124
  else:
98
- raise _INTERSECTION_WITH_WRONG_TYPE(first_value, second_value)
125
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
99
126
 
100
127
  def intersection_with_outside(first_value: Outside, second_value: Value) -> Value:
101
128
  if isinstance(first_value, Outside):
102
129
  if isinstance(second_value, LessThan):
103
- if second_value.value <= first_value.lower:
104
- return second_value
105
- elif first_value.is_in(second_value.value):
130
+ if second_value.value > first_value.upper:
131
+ # LessThan(first_value.lower) + Between(first_value.lower, second_value.value)
132
+ raise _NOT_IMPLEMENTED_INTERSECTION(first_value, second_value)
133
+ elif second_value.value > first_value.lower:
106
134
  return LessThan(first_value.lower)
107
135
  else:
108
- raise _NOT_IMPLEMENTED_INTERSECTION(first_value, second_value)
136
+ return second_value
109
137
  elif isinstance(second_value, GreaterThan):
110
- if second_value.value >= first_value.lower:
138
+ if second_value.value < first_value.lower:
139
+ # Between(second_value.value, first_value.lower) + GreaterThan(first_value.upper)
140
+ raise _NOT_IMPLEMENTED_INTERSECTION(first_value, second_value)
141
+ elif second_value.value < first_value.upper:
111
142
  return GreaterThan(first_value.upper)
112
- elif first_value.is_in(second_value.value):
113
- return second_value
114
143
  else:
115
- raise _NOT_IMPLEMENTED_INTERSECTION(first_value, second_value)
116
- elif isinstance(second_value, Constant):
117
- if not first_value.is_in(second_value.value):
118
144
  return second_value
119
- else:
120
- raise _EMPTY_INTERSECTION_EXCEPTION(first_value, second_value)
121
145
  elif isinstance(second_value, Between):
122
- if second_value in first_value:
146
+ if second_value.upper <= first_value.lower or second_value.lower >= first_value.upper:
123
147
  return second_value
124
148
  elif second_value.lower <= first_value.lower <= second_value.upper <= first_value.upper:
125
149
  return Between(second_value.lower, first_value.lower)
126
150
  elif first_value.lower <= second_value.lower <= first_value.upper <= second_value.upper:
127
151
  return Between(first_value.upper, second_value.upper)
128
- else:
152
+ elif second_value.lower <= first_value.lower <= first_value.upper <= second_value.upper:
129
153
  raise _NOT_IMPLEMENTED_INTERSECTION(first_value, second_value)
154
+ else:
155
+ raise _EMPTY_INTERSECTION_EXCEPTION(first_value, second_value)
130
156
  elif isinstance(second_value, Outside):
131
- if second_value.lower <= first_value.lower and second_value.upper >= first_value.upper:
157
+ if second_value.lower <= first_value.lower <= first_value.upper <= second_value.upper:
132
158
  return second_value
133
- elif first_value.lower <= second_value.lower and first_value.upper >= second_value.upper:
159
+ elif first_value.lower <= second_value.lower <= second_value.upper <= first_value.upper:
134
160
  return first_value
161
+ elif second_value.lower <= first_value.lower <= second_value.upper <= first_value.upper:
162
+ return Outside(second_value.lower, first_value.upper)
163
+ elif first_value.lower <= second_value.lower <= first_value.upper <= second_value.upper:
164
+ return Outside(first_value.lower, second_value.upper)
135
165
  else:
136
166
  raise _EMPTY_INTERSECTION_EXCEPTION(first_value, second_value)
137
167
  elif isinstance(second_value, Constant):
138
- intersection_with_constant(second_value, first_value)
168
+ return intersection_with_constant(second_value, first_value)
139
169
  else:
140
- raise _INTERSECTION_WITH_WRONG_TYPE(first_value, second_value)
170
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
141
171
  else:
142
- raise _INTERSECTION_WITH_WRONG_TYPE(first_value, second_value)
172
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
143
173
 
144
174
  def intersection_with_between(first_value: Between, second_value: Value) -> Value:
145
175
  if isinstance(first_value, Between):
@@ -173,13 +203,13 @@ class Value:
173
203
  else:
174
204
  raise _EMPTY_INTERSECTION_EXCEPTION(first_value, second_value)
175
205
  elif isinstance(second_value, Constant):
176
- intersection_with_constant(second_value, first_value)
206
+ return intersection_with_constant(second_value, first_value)
177
207
  elif isinstance(second_value, Outside):
178
208
  return intersection_with_outside(second_value, first_value)
179
209
  else:
180
- raise _INTERSECTION_WITH_WRONG_TYPE(first_value, second_value)
210
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
181
211
  else:
182
- raise _INTERSECTION_WITH_WRONG_TYPE(first_value, second_value)
212
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
183
213
 
184
214
  def intersection_with_less_than(first_value: LessThan, second_value: Value) -> Value:
185
215
  if isinstance(first_value, LessThan):
@@ -197,9 +227,9 @@ class Value:
197
227
  elif isinstance(second_value, Between):
198
228
  return intersection_with_between(second_value, first_value)
199
229
  else:
200
- raise _INTERSECTION_WITH_WRONG_TYPE(first_value, second_value)
230
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
201
231
  else:
202
- raise _INTERSECTION_WITH_WRONG_TYPE(first_value, second_value)
232
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
203
233
 
204
234
  def intersection_with_greater_than(first_value: GreaterThan, second_value: Value) -> Value:
205
235
  if isinstance(first_value, GreaterThan):
@@ -214,9 +244,9 @@ class Value:
214
244
  elif isinstance(second_value, LessThan):
215
245
  return intersection_with_less_than(second_value, first_value)
216
246
  else:
217
- raise _INTERSECTION_WITH_WRONG_TYPE(first_value, second_value)
247
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
218
248
  else:
219
- raise _INTERSECTION_WITH_WRONG_TYPE(first_value, second_value)
249
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
220
250
 
221
251
  if other is None:
222
252
  return self
@@ -231,7 +261,154 @@ class Value:
231
261
  elif isinstance(self, GreaterThan):
232
262
  return intersection_with_greater_than(self, other)
233
263
  else:
234
- raise _INTERSECTION_WITH_WRONG_TYPE(self, other)
264
+ raise _OPERATION_WITH_WRONG_TYPE(self, other)
265
+
266
+ def __add__(self, other) -> Value:
267
+
268
+ def union_with_constant(first_value: Constant, second_value: Value) -> Value:
269
+ if isinstance(first_value, Constant):
270
+ if first_value in second_value:
271
+ return second_value
272
+ else:
273
+ raise _NOT_IMPLEMENTED_UNION(first_value, second_value)
274
+ else:
275
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
276
+
277
+ def union_with_outside(first_value: Outside, second_value: Value) -> Value:
278
+ if isinstance(first_value, Outside):
279
+ if isinstance(second_value, LessThan):
280
+ if second_value.value > first_value.upper:
281
+ return Between(-math.inf, math.inf)
282
+ elif second_value.value > first_value.lower:
283
+ return Outside(second_value.value, first_value.upper)
284
+ else:
285
+ return first_value
286
+ elif isinstance(second_value, GreaterThan):
287
+ if second_value.value < first_value.lower:
288
+ return Between(-math.inf, math.inf)
289
+ elif second_value.value < first_value.upper:
290
+ return Outside(first_value.lower, second_value.value)
291
+ else:
292
+ return first_value
293
+ elif isinstance(second_value, Between):
294
+ if second_value.upper <= first_value.lower or second_value.lower >= first_value.upper:
295
+ return first_value
296
+ elif second_value.lower <= first_value.lower <= second_value.upper <= first_value.upper:
297
+ return Outside(second_value.upper, first_value.lower)
298
+ elif first_value.lower <= second_value.lower <= first_value.upper <= second_value.upper:
299
+ return Outside(first_value.upper, second_value.lower)
300
+ elif second_value.lower <= first_value.lower <= first_value.upper <= second_value.upper:
301
+ return Between(-math.inf, math.inf)
302
+ else:
303
+ raise _NOT_IMPLEMENTED_UNION(first_value, second_value)
304
+ elif isinstance(second_value, Outside):
305
+ if second_value.lower <= first_value.lower <= first_value.upper <= second_value.upper:
306
+ return first_value
307
+ elif first_value.lower <= second_value.lower <= second_value.upper <= first_value.upper:
308
+ return second_value
309
+ elif second_value.lower <= first_value.lower <= second_value.upper <= first_value.upper:
310
+ return Outside(first_value.lower, second_value.upper)
311
+ elif first_value.lower <= second_value.lower <= first_value.upper <= second_value.upper:
312
+ return Outside(second_value.lower, first_value.upper)
313
+ else:
314
+ return Between(-math.inf, math.inf)
315
+ elif isinstance(second_value, Constant):
316
+ return union_with_constant(second_value, first_value)
317
+ else:
318
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
319
+ else:
320
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
321
+
322
+ def union_with_between(first_value: Between, second_value: Value) -> Value:
323
+ if isinstance(first_value, Between):
324
+ if isinstance(second_value, LessThan):
325
+ if second_value.value <= first_value.lower:
326
+ raise _NOT_IMPLEMENTED_UNION(first_value, second_value)
327
+ elif first_value.lower <= second_value.value <= first_value.upper:
328
+ return LessThan(first_value.upper)
329
+ else:
330
+ return second_value
331
+ elif isinstance(second_value, GreaterThan):
332
+ if second_value.value <= first_value.lower:
333
+ return second_value
334
+ elif first_value.lower <= second_value.value <= first_value.upper:
335
+ return GreaterThan(first_value.lower)
336
+ else:
337
+ raise _NOT_IMPLEMENTED_UNION(first_value, second_value)
338
+ elif isinstance(second_value, Between):
339
+ if second_value in first_value:
340
+ return first_value
341
+ elif first_value in second_value:
342
+ return second_value
343
+ elif first_value.lower <= second_value.lower <= first_value.upper:
344
+ return Between(first_value.lower, second_value.upper)
345
+ elif second_value.lower <= first_value.lower <= second_value.upper <= first_value.upper:
346
+ return Between(second_value.lower, first_value.upper)
347
+ else:
348
+ raise _NOT_IMPLEMENTED_UNION(first_value, second_value)
349
+ elif isinstance(second_value, Constant):
350
+ return union_with_constant(second_value, first_value)
351
+ elif isinstance(second_value, Outside):
352
+ return union_with_outside(second_value, first_value)
353
+ else:
354
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
355
+ else:
356
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
357
+
358
+ def union_with_less_than(first_value: LessThan, second_value: Value) -> Value:
359
+ if isinstance(first_value, LessThan):
360
+ if isinstance(second_value, LessThan):
361
+ return second_value if first_value in second_value else first_value
362
+ elif isinstance(second_value, GreaterThan):
363
+ if second_value.value <= first_value.value:
364
+ return Between(-math.inf, math.inf)
365
+ else:
366
+ return Outside(first_value.value, second_value.value)
367
+ elif isinstance(second_value, Constant):
368
+ return union_with_constant(second_value, first_value)
369
+ elif isinstance(second_value, Outside):
370
+ return union_with_outside(second_value, first_value)
371
+ elif isinstance(second_value, Between):
372
+ return union_with_between(second_value, first_value)
373
+ else:
374
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
375
+ else:
376
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
377
+
378
+ def union_with_greater_than(first_value: GreaterThan, second_value: Value) -> Value:
379
+ if isinstance(first_value, GreaterThan):
380
+ if isinstance(second_value, GreaterThan):
381
+ return second_value if first_value in second_value else first_value
382
+ elif isinstance(second_value, Constant):
383
+ return union_with_constant(second_value, first_value)
384
+ elif isinstance(second_value, Outside):
385
+ return union_with_outside(second_value, first_value)
386
+ elif isinstance(second_value, Between):
387
+ return union_with_between(second_value, first_value)
388
+ elif isinstance(second_value, LessThan):
389
+ return union_with_less_than(second_value, first_value)
390
+ else:
391
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
392
+ else:
393
+ raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
394
+
395
+ if other is None:
396
+ return self
397
+ elif isinstance(self, Constant):
398
+ return union_with_constant(self, other)
399
+ elif isinstance(self, Outside):
400
+ return union_with_outside(self, other)
401
+ elif isinstance(self, Between):
402
+ return union_with_between(self, other)
403
+ elif isinstance(self, LessThan):
404
+ return union_with_less_than(self, other)
405
+ elif isinstance(self, GreaterThan):
406
+ return union_with_greater_than(self, other)
407
+ else:
408
+ raise _OPERATION_WITH_WRONG_TYPE(self, other)
409
+
410
+ def print(self) -> str:
411
+ pass
235
412
 
236
413
 
237
414
  class Interval(Value):
@@ -248,7 +425,7 @@ class Interval(Value):
248
425
  def __repr__(self):
249
426
  return f"Interval({self.lower:.2f}, {self.upper:.2f})"
250
427
 
251
- def __eq__(self, other: Between) -> bool:
428
+ def __eq__(self, other: Interval) -> bool:
252
429
  return (self.upper == other.upper) and (self.lower == other.lower) and (self.standard == other.standard)
253
430
 
254
431
 
@@ -264,6 +441,9 @@ class LessThan(Interval):
264
441
  def value(self) -> float:
265
442
  return self.upper
266
443
 
444
+ def print(self) -> str:
445
+ return f"below {round(self.upper, 1)}"
446
+
267
447
  def __str__(self):
268
448
  return f"]-∞, {self.upper:.2f}" + ("]" if self.standard else "[")
269
449
 
@@ -271,7 +451,8 @@ class LessThan(Interval):
271
451
  return f"LessThan({self.upper:.2f})"
272
452
 
273
453
  def __eq__(self, other: LessThan) -> bool:
274
- return (self.upper == other.upper) and (self.value == other.value) and (self.standard == other.standard)
454
+ return isinstance(other, LessThan) and (self.upper == other.upper) and \
455
+ (self.value == other.value) and (self.standard == other.standard)
275
456
 
276
457
 
277
458
  class GreaterThan(Interval):
@@ -286,6 +467,9 @@ class GreaterThan(Interval):
286
467
  def value(self) -> float:
287
468
  return self.lower
288
469
 
470
+ def print(self) -> str:
471
+ return f"above {round(self.lower, 1)}"
472
+
289
473
  def __str__(self):
290
474
  return ("]" if self.standard else "[") + f"{self.lower:.2f}, ∞["
291
475
 
@@ -293,7 +477,8 @@ class GreaterThan(Interval):
293
477
  return f"GreaterThan({self.lower:.2f})"
294
478
 
295
479
  def __eq__(self, other: GreaterThan) -> bool:
296
- return (self.lower == other.lower) and (self.value == other.value) and (self.standard == other.standard)
480
+ return isinstance(other, GreaterThan) and (self.lower == other.lower) and \
481
+ (self.value == other.value) and (self.standard == other.standard)
297
482
 
298
483
 
299
484
  class Between(Interval):
@@ -304,6 +489,9 @@ class Between(Interval):
304
489
  def is_in(self, other: float) -> bool:
305
490
  return self.lower <= other < self.upper if self.standard else self.lower < other <= self.upper
306
491
 
492
+ def print(self) -> str:
493
+ return f"between {round(self.lower, 1)} and {round(self.upper, 1)}"
494
+
307
495
  def __str__(self):
308
496
  return ("[" if self.standard else "]") + f"{self.lower:.2f}, {self.upper:.2f}" + ("[" if self.standard else "]")
309
497
 
@@ -319,6 +507,9 @@ class Outside(Interval):
319
507
  def is_in(self, other: float) -> bool:
320
508
  return other < self.lower or self.upper <= other if self.standard else other <= self.lower or self.upper < other
321
509
 
510
+ def print(self) -> str:
511
+ return f"not between {round(self.lower, 1)} and {round(self.upper, 1)}"
512
+
322
513
  def __str__(self):
323
514
  return f"]-∞, {self.lower:.2f}" + ("[" if self.standard else "]") + ' U '\
324
515
  + ("[" if self.standard else "]") + f"{self.upper:.2f}, ∞["
@@ -336,6 +527,9 @@ class Constant(Value):
336
527
  def is_in(self, other: float) -> bool:
337
528
  return math.isclose(other, self.value)
338
529
 
530
+ def print(self) -> str:
531
+ return f"equal {round(self.value, 1)}"
532
+
339
533
  def __str__(self):
340
534
  return "{" + str(self.value) + "}"
341
535