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.
- psyke/__init__.py +231 -85
- psyke/clustering/__init__.py +9 -4
- psyke/clustering/cream/__init__.py +6 -10
- psyke/clustering/exact/__init__.py +17 -11
- psyke/clustering/utils.py +0 -1
- psyke/extraction/__init__.py +25 -0
- psyke/extraction/cart/CartPredictor.py +128 -0
- psyke/extraction/cart/FairTree.py +205 -0
- psyke/extraction/cart/FairTreePredictor.py +56 -0
- psyke/extraction/cart/__init__.py +48 -62
- psyke/extraction/hypercubic/__init__.py +187 -47
- psyke/extraction/hypercubic/cosmik/__init__.py +47 -0
- psyke/extraction/hypercubic/creepy/__init__.py +24 -29
- psyke/extraction/hypercubic/divine/__init__.py +86 -0
- psyke/extraction/hypercubic/ginger/__init__.py +100 -0
- psyke/extraction/hypercubic/gridex/__init__.py +45 -84
- psyke/extraction/hypercubic/gridrex/__init__.py +4 -4
- psyke/extraction/hypercubic/hex/__init__.py +104 -0
- psyke/extraction/hypercubic/hypercube.py +275 -72
- psyke/extraction/hypercubic/iter/__init__.py +45 -46
- psyke/extraction/hypercubic/strategy.py +13 -9
- psyke/extraction/real/__init__.py +24 -29
- psyke/extraction/real/utils.py +2 -2
- psyke/extraction/trepan/__init__.py +24 -19
- psyke/genetic/__init__.py +0 -0
- psyke/genetic/fgin/__init__.py +74 -0
- psyke/genetic/gin/__init__.py +144 -0
- psyke/hypercubepredictor.py +102 -0
- psyke/schema/__init__.py +230 -36
- psyke/tuning/__init__.py +40 -28
- psyke/tuning/crash/__init__.py +33 -64
- psyke/tuning/orchid/__init__.py +21 -23
- psyke/tuning/pedro/__init__.py +70 -56
- psyke/utils/logic.py +8 -8
- psyke/utils/plot.py +79 -3
- {psyke-0.4.9.dev6.dist-info → psyke-1.0.4.dev10.dist-info}/METADATA +42 -22
- psyke-1.0.4.dev10.dist-info/RECORD +46 -0
- {psyke-0.4.9.dev6.dist-info → psyke-1.0.4.dev10.dist-info}/WHEEL +1 -1
- {psyke-0.4.9.dev6.dist-info → psyke-1.0.4.dev10.dist-info/licenses}/LICENSE +2 -1
- psyke/extraction/cart/predictor.py +0 -73
- psyke-0.4.9.dev6.dist-info/RECORD +0 -36
- {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
|
-
|
|
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
|
-
|
|
17
|
+
SchemaException(f"Not implemented intersection between: {str(x)} and {str(y)}")
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
136
|
+
return second_value
|
|
109
137
|
elif isinstance(second_value, GreaterThan):
|
|
110
|
-
if second_value.value
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
170
|
+
raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
|
|
141
171
|
else:
|
|
142
|
-
raise
|
|
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
|
|
210
|
+
raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
|
|
181
211
|
else:
|
|
182
|
-
raise
|
|
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
|
|
230
|
+
raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
|
|
201
231
|
else:
|
|
202
|
-
raise
|
|
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
|
|
247
|
+
raise _OPERATION_WITH_WRONG_TYPE(first_value, second_value)
|
|
218
248
|
else:
|
|
219
|
-
raise
|
|
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
|
|
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:
|
|
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 (
|
|
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 (
|
|
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
|
|