Skip to content

Commit eb4fdf4

Browse files
committed
enh: performance optimalisation
enh: add more test enh: resolver for operator only enh: resolver exceptions
1 parent 8ded17e commit eb4fdf4

7 files changed

+183
-69
lines changed

Diff for: README.md

+31-21
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
11
# Crossmath algorithm prototype in Python
22

33
```
4-
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
5-
0
6-
1
7-
2
8-
3 0.2 / 0.2 = 1.0
9-
4 -
10-
5 0.2 + 2.8 = 3.0 1.0 + 0.8 = 1.8
11-
6 = * * +
12-
7 0.0 6.0 + 7.0 = 13.0 4.2 71.2 - 34.4 = 36.8
13-
8 = = * + =
14-
9 7.8 4.2 + 16.8 = 21.0 5.0 + 68.0 = 73.0
15-
10 - / = =
16-
11 4.2 4.2 7.2 + 65.0 = 72.2
17-
12 = = +
18-
13 3.6 / 1.0 = 3.6 5.0 - 1.4 = 3.6
19-
14 + = - -
20-
15 3.2 12.2 0.2 1.0
21-
16 = = =
22-
17 6.8 1.2 2.6
23-
18
24-
19
4+
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
5+
0 4.9 - -10.1 = 15.0 6.3 + -27.3 = -21.0 3.2 + 5.2 = 8.4
6+
1 + + - -
7+
2 2.3 + 8.0 = 10.3 4.2 / 0.1 = 42.0 9.7 - 4.7 = 5.0 0.7 + 7.5 = 8.2
8+
3 / = - + = = =
9+
4 2.3 + -2.1 = 0.2 2.1 / 0.1 = 21.0 2.0 * 0.1 = 0.2 7.7 - 3.0 = 4.7
10+
5 = = = -
11+
6 1.0 10.1 - 6.3 = 3.8 6.0 * 4.8 = 28.8 3.7 + 0.9 = 4.6
12+
7 / + = +
13+
8 8.5 - 0.1 = 8.4 9.2 - 1.8 = 7.4 -28.7 - -28.7 = 0.0
14+
9 / = - + = = *
15+
10 8.5 38.0 + 2.6 = 40.6 7.8 - 0.3 = 7.5 -25.0 * 1.9 = -47.5
16+
11 = = = =
17+
12 1.0 + 4.8 = 5.8 49.8 - 30.5 = 19.3 0.0 + 0.4 = 0.4
18+
13 - + /
19+
14 1.9 + 2.5 = 4.4 -26.4 - 19.3 = -45.7
20+
15 + = = = +
21+
16 5.4 2.3 - 0.2 = 2.1 4.1 / 1.0 = 4.1
22+
17 = / =
23+
18 7.3 + -7.2 = 0.1 8.6 + -50.2 = -41.6
24+
19 - = +
25+
20 -7.3 2.0 - 1.0 = 1.0 7.0 * 1.8 = 12.6
26+
21 = - = / +
27+
22 0.1 4.9 + 0.9 = 5.8 -43.2 + 1.8 = -41.4
28+
23 / = - = =
29+
24 0.2 - 0.1 = 0.1 -28.8 * 1.0 = -28.8
30+
25 = = -
31+
26 24.5 5.7 -28.8
32+
27 =
33+
28 0.0
34+
29
2535
```
2636

2737
## Author

Diff for: crossmath.py

+52-25
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,42 @@
33
from typing import Tuple
44

55
from expression import Expression, ExpressionValidator
6-
from expression_map import ExpressionMap, ExpressionItem, Direction
7-
from expression_resolver import ExpressionResolver, ExpressionResolverException
6+
from expression_map import (
7+
ExpressionMap,
8+
ExpressionItem,
9+
Direction,
10+
ExpressionMapCellValueMissmatch,
11+
)
12+
from expression_resolver import (
13+
ExpressionResolver,
14+
ExpressionResolverException,
15+
)
816
from number_factory import NumberFactory
917

1018

19+
class DeadPoints:
20+
def __init__(self):
21+
self._data: dict[Tuple[int, int], list[Direction]] = {}
22+
23+
def add(self, x: int, y: int, direction: Direction):
24+
if (x, y) not in self._data:
25+
self._data[(x, y)] = []
26+
self._data[(x, y)].append(direction)
27+
28+
def is_dead(self, x: int, y: int, direction: Direction) -> bool:
29+
if (x, y) not in self._data:
30+
return False
31+
return direction in self._data[(x, y)]
32+
33+
def is_dead_full(self, x: int, y: int) -> bool:
34+
if (x, y) not in self._data:
35+
return False
36+
return len(self._data[(x, y)]) == len(Direction.all())
37+
38+
def clear(self):
39+
self._data.clear()
40+
41+
1142
class CrossMath:
1243
def __init__(self, exp_map: ExpressionMap, number_factory: NumberFactory):
1344
self._map = exp_map
@@ -18,13 +49,14 @@ def __init__(self, exp_map: ExpressionMap, number_factory: NumberFactory):
1849
),
1950
number_factory=number_factory,
2051
)
52+
self._dead_points = DeadPoints()
2153

2254
def _find_potential_positions(self) -> list[Tuple[int, int]]:
23-
for y in range(self._map.height()):
24-
for x in range(self._map.width()):
25-
value = self._map.get(x, y)
26-
if isinstance(value, float):
27-
yield x, y
55+
for point in self._map.get_all_operand_points():
56+
x, y = point
57+
if self._dead_points.is_dead_full(x, y):
58+
continue
59+
yield point
2860

2961
def _check_expression_frame(
3062
self, x: int, y: int, direction: Direction, length: int
@@ -63,14 +95,11 @@ def _check_x_y_overflow(self, x: int, y: int, length: int) -> bool:
6395
def _find_potential_values(
6496
self,
6597
potential_positions: list[Tuple[int, int]],
66-
dead_positions: list[Tuple[Direction, int, int]],
6798
) -> list[Tuple[Direction, int, int, list]]:
6899
max_expression_length = max(Expression.SUPPORTED_LENGTHS)
69100
for next_position in potential_positions:
70101
x, y = next_position
71102
for direction in Direction.all():
72-
if (direction, x, y) in dead_positions:
73-
continue
74103
for expression_offset in range(0, -max_expression_length - 1, -2):
75104
values_x_offset = (
76105
0 if direction.is_vertical() else expression_offset
@@ -97,6 +126,8 @@ def _find_potential_values(
97126
if all([value is not None for value in values]):
98127
# already filled
99128
continue
129+
if self._dead_points.is_dead(values_x, values_y, direction):
130+
continue
100131

101132
yield (
102133
direction,
@@ -119,32 +150,28 @@ def _init_generate(self):
119150
self._map.put(item)
120151

121152
def generate(self):
153+
self._dead_points.clear()
122154
self._init_generate()
123-
dead_positions = []
124-
while True:
125-
potential_positions = self._find_potential_positions()
126-
potential_values = list(
127-
self._find_potential_values(potential_positions, dead_positions)
155+
latest_version = None
156+
while latest_version != self._map.get_version():
157+
latest_version = self._map.get_version()
158+
potential_values = self._find_potential_values(
159+
potential_positions=self._find_potential_positions(),
128160
)
129-
random.shuffle(potential_values)
130-
is_expression_appended = False
131-
for desc in potential_values:
132-
print(".", end="")
161+
pvs = list(potential_values)
162+
random.shuffle(pvs)
163+
for desc in pvs:
133164
direction, _x, _y, values = desc
134-
# print("desc:", desc, "x:", _x, "y:", _y)
135165
try:
136166
expression = self._expression_resolver.resolve(
137167
Expression.from_values(values)
138168
)
139169
except ExpressionResolverException as e:
140-
print(f"ExpressionResolverException: {e}")
141-
dead_positions.append((direction, _x, _y))
170+
print(e)
171+
self._dead_points.add(_x, _y, direction)
142172
continue
143173
expression_item = ExpressionItem(_x, _y, direction, expression)
144174
self._map.put(expression_item)
145-
is_expression_appended = True
146-
break
147-
if not is_expression_appended:
148175
break
149176

150177
def print(self):

Diff for: expression.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,11 @@ def validate(self, expression: Expression) -> bool:
139139
return False
140140
if not self._check_range(expression.result):
141141
return False
142-
if expression.operator == Operator.DIV and self._number_factory.is_equal(
143-
expression.operand2, 0.0
142+
if expression.operator == Operator.DIV and NumberFactory.is_zero(
143+
expression.operand2
144144
):
145145
return False
146-
return self._number_factory.is_equal(
146+
return NumberFactory.is_equal(
147147
eval(f"{expression.operand1} {expression.operator} {expression.operand2}"),
148148
expression.result,
149149
)

Diff for: expression_map.py

+32-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import random
12
from enum import Enum
3+
from typing import Tuple
24

35
import pandas
46

@@ -14,6 +16,12 @@ class Direction(Enum):
1416
def all() -> list:
1517
return [Direction.HORIZONTAL, Direction.VERTICAL]
1618

19+
@staticmethod
20+
def all_random() -> list:
21+
directions = Direction.all()
22+
random.shuffle(directions)
23+
return directions
24+
1725
def is_horizontal(self) -> bool:
1826
return self == Direction.HORIZONTAL
1927

@@ -62,13 +70,29 @@ def __str__(self):
6270
return f"x: {self.x()}, y: {self.y()}, direction: {self.direction()}, length: {self.length()}, expression: {self.expression()}"
6371

6472

73+
class ExpressionMapException(Exception):
74+
pass
75+
76+
77+
class ExpressionMapCellValueMissmatch(ExpressionMapException):
78+
pass
79+
80+
6581
class ExpressionMap:
6682
def __init__(self, width: int, height: int):
6783
self._width = width
6884
self._height = height
6985
self._map: list[list[Operator | None | float]] = [
7086
[None for _ in range(width)] for _ in range(height)
7187
]
88+
self._operands_point: list[Tuple[int, int]] = []
89+
self._version: int = 0
90+
91+
def get_version(self) -> int:
92+
return self._version
93+
94+
def get_all_operand_points(self) -> list[Tuple[int, int]]:
95+
return self._operands_point
7296

7397
def width(self) -> int:
7498
return self._width
@@ -110,19 +134,21 @@ def put(self, item: ExpressionItem):
110134
for i in range(len(values)):
111135
if item.is_horizontal():
112136
if self._map[y][x + i] is not None and self._map[y][x + i] != values[i]:
113-
raise ValueError(
137+
raise ExpressionMapCellValueMissmatch(
114138
f"Illegal override x, y, values: {x}, {y}, {self._map[y][x + i]}, {values[i]}"
115139
)
116140
else:
117141
if self._map[y + i][x] is not None and self._map[y + i][x] != values[i]:
118-
raise ValueError(
142+
raise ExpressionMapCellValueMissmatch(
119143
f"Illegal override x, y, values: {x}, {y}, {self._map[y + i][x]}, {values[i]}"
120144
)
121145
for i in range(len(values)):
122-
if item.is_horizontal():
123-
self._map[y][x + i] = values[i]
124-
else:
125-
self._map[y + i][x] = values[i]
146+
target_x = x + i if item.is_horizontal() else x
147+
target_y = y + i if item.is_vertical() else y
148+
if isinstance(values[i], float):
149+
self._operands_point.append((target_x, target_y))
150+
self._map[target_y][target_x] = values[i]
151+
self._version += 1
126152

127153
def print(self, number_factory: NumberFactory | None = None):
128154
pandas.set_option("display.max_rows", None)

Diff for: expression_resolver.py

+49-10
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,48 @@ def _resolve_result_is_none(self, expression: Expression) -> Expression:
9595
self._check_result(expression, exp_result)
9696
if not self._validator.validate(exp_result):
9797
# TODO time or count limit
98-
if time.time() - start > 1.0:
99-
raise ExpressionResolverMaybeNotResolvable(expression=expression)
98+
time_diff = time.time() - start
99+
if time_diff > 1.0:
100+
raise ExpressionResolverMaybeNotResolvable(
101+
message=f"Too slow: {time_diff:.1f}s", expression=expression
102+
)
100103
continue
101104
self._fly_back(exp_result)
102105
return exp_result
103106

107+
def _resolve_only_operator_missing(self, expression: Expression) -> Expression:
108+
if (
109+
expression.operand1 is None
110+
or expression.operand2 is None
111+
or expression.result is None
112+
or expression.operator is not None
113+
):
114+
raise RuntimeError(
115+
f"Invalid state, only operator missing is allowed: {expression}"
116+
)
117+
operators = Operator.get_operators_without_eq()
118+
random.shuffle(operators)
119+
for operator in operators:
120+
if operator == Operator.DIV and NumberFactory.is_zero(expression.operand2):
121+
# zero division
122+
continue
123+
exp_result = expression.clone()
124+
exp_result.operator = operator
125+
exp_result.result = self._number_factory.fix(
126+
eval(
127+
f"{exp_result.operand1} {exp_result.operator} {exp_result.operand2}"
128+
)
129+
)
130+
if not NumberFactory.is_equal(exp_result.result, expression.result):
131+
continue
132+
self._fix_result(exp_result)
133+
self._check_result(expression, exp_result)
134+
if not self._validator.validate(exp_result):
135+
continue
136+
self._fly_back(exp_result)
137+
return exp_result
138+
raise ExpressionResolverNotResolvable(expression=expression)
139+
104140
def _resolve_result_is_available(self, expression: Expression) -> Expression:
105141
start = time.time()
106142
while True:
@@ -178,15 +214,16 @@ def _resolve_result_is_available(self, expression: Expression) -> Expression:
178214
eval(f"{exp_calc.result} {exp_calc.operator} {exp_calc.operand1}")
179215
)
180216
else:
181-
raise ExpressionResolverException(
182-
"Operator resolver mode not supported", expression=expression
183-
)
217+
raise RuntimeError("Invalid state: only one operand is missing")
184218
self._check_result(expression, exp_result)
185219

186220
if not self._validator.validate(exp_result):
187221
# TODO time or count limit
188-
if time.time() - start > 1.0:
189-
raise ExpressionResolverMaybeNotResolvable(expression=expression)
222+
time_diff = time.time() - start
223+
if time_diff > 1.0:
224+
raise ExpressionResolverMaybeNotResolvable(
225+
message=f"Too slow: {time_diff:.1f}s", expression=expression
226+
)
190227
continue
191228
self._fly_back(exp_result)
192229
return exp_result
@@ -196,6 +233,8 @@ def resolve(self, expression: Expression) -> Expression | None:
196233
try:
197234
if expression.result is None:
198235
return self._resolve_result_is_none(expression)
236+
if expression.operand1 is not None and expression.operand2 is not None:
237+
return self._resolve_only_operator_missing(expression)
199238
return self._resolve_result_is_available(expression)
200239
finally:
201240
end_time = time.time()
@@ -205,13 +244,13 @@ def resolve(self, expression: Expression) -> Expression | None:
205244

206245
def _check_result(self, expression: Expression, result: Expression):
207246
if expression.operand1 is not None:
208-
if not self._number_factory.is_equal(expression.operand1, result.operand1):
247+
if not NumberFactory.is_equal(expression.operand1, result.operand1):
209248
raise RuntimeError(f"Invalid operand1: {expression} -> {result}")
210249
if expression.operand2 is not None:
211-
if not self._number_factory.is_equal(expression.operand2, result.operand2):
250+
if not NumberFactory.is_equal(expression.operand2, result.operand2):
212251
raise RuntimeError(f"Invalid operand2: {expression} -> {result}")
213252
if expression.result is not None:
214-
if not self._number_factory.is_equal(expression.result, result.result):
253+
if not NumberFactory.is_equal(expression.result, result.result):
215254
raise RuntimeError(f"Invalid result: {expression} -> {result}")
216255
if expression.operator is not None:
217256
if expression.operator != result.operator:

0 commit comments

Comments
 (0)