Skip to content

Commit cf3b94d

Browse files
fix bruteforce not checking every stock sorting, use sorted set for best results to skip duplicates, sort results by biggest trimmings; update and fix tests, add test for #68; update v1.1.1
1 parent 6ecbe7e commit cf3b94d

File tree

6 files changed

+39
-20
lines changed

6 files changed

+39
-20
lines changed

app/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from pydantic_settings import BaseSettings
33

44
# constant; used for git tags
5-
version = "v1.1.0"
5+
version = "v1.1.1"
66

77

88
class SolverSettings(BaseSettings):

app/solver/solver.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ def solve(job: Job) -> Result:
3333
# slowest, but perfect solver; originally O(n!), now much faster (see Job.n_combinations())
3434
def _solve_bruteforce(job: Job) -> tuple[ResultEntry, ...]:
3535
minimal_trimmings = float('inf')
36-
best_results = []
36+
best_results: set[tuple[ResultEntry, ...]] = set()
3737

38-
required_orderings = distinct_permutations(job.iterate_required())
38+
required_orderings = list(distinct_permutations(job.iterate_required()))
3939
for stock_ordering in distinct_permutations(job.iterate_stocks()):
4040
for required_ordering in required_orderings:
4141
result = _group_into_lengths(stock_ordering, required_ordering, job.cut_width)
@@ -45,12 +45,13 @@ def _solve_bruteforce(job: Job) -> tuple[ResultEntry, ...]:
4545
trimmings = sum(lt.trimming for lt in result)
4646
if trimmings < minimal_trimmings:
4747
minimal_trimmings = trimmings
48-
best_results = [result]
48+
best_results = set()
49+
best_results.add(sort_entries(result))
4950
elif trimmings == minimal_trimmings:
50-
best_results.append(result)
51+
best_results.add(sort_entries(result))
5152

5253
assert best_results, "No valid solution found"
53-
return sort_entries(find_best_solution(best_results))
54+
return find_best_solution(best_results)
5455

5556

5657
def _group_into_lengths(stocks: tuple[NS, ...], sizes: tuple[NS, ...], cut_width: int) \

app/solver/utils.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,12 @@ def calc_trimming(stock_length: int, lengths: Collection[NS], cut_width: int) ->
2020
return trimmings
2121

2222

23-
def find_best_solution(solutions: Sequence):
23+
def find_best_solution(solutions: set[tuple[ResultEntry, ...]]):
2424
if len(solutions) <= 0:
2525
raise ValueError("no solution to search")
2626

2727
# TODO evaluate which one aligns with user expectations best (see #68)
28-
# always sort for determinism!
29-
return sorted(solutions, reverse=True)[0]
28+
return sorted(solutions, key=lambda x: max(x), reverse=True)[0]
3029

3130

3231
def create_result_entry(stock: NS, cuts: list[NS], cut_width: int) -> ResultEntry:
@@ -37,7 +36,7 @@ def create_result_entry(stock: NS, cuts: list[NS], cut_width: int) -> ResultEntr
3736
)
3837

3938

40-
def sort_entries(result_entries: list[ResultEntry]) -> tuple[ResultEntry, ...]:
39+
def sort_entries(result_entries: Sequence[ResultEntry]) -> tuple[ResultEntry, ...]:
4140
if len(result_entries) <= 0:
4241
raise ValueError("no entries to sort")
4342
return tuple(sorted(result_entries))

tests/res/out/testresult_s.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
]
2222
},
2323
"solver_type": "bruteforce",
24-
"time_us": 1868,
24+
"time_us": 1683,
2525
"layout": [
2626
{
2727
"stock": {
@@ -34,8 +34,8 @@
3434
"name": "Part1"
3535
},
3636
{
37-
"length": 200,
38-
"name": "Part2"
37+
"length": 500,
38+
"name": "Part1"
3939
},
4040
{
4141
"length": 200,
@@ -46,7 +46,7 @@
4646
"name": "Part2"
4747
}
4848
],
49-
"trimming": 392
49+
"trimming": 92
5050
},
5151
{
5252
"stock": {
@@ -55,15 +55,15 @@
5555
},
5656
"cuts": [
5757
{
58-
"length": 500,
59-
"name": "Part1"
58+
"length": 200,
59+
"name": "Part2"
6060
},
6161
{
6262
"length": 200,
6363
"name": "Part2"
6464
}
6565
],
66-
"trimming": 796
66+
"trimming": 1096
6767
}
6868
]
6969
}

tests/solver/test_large.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ def test_m(solver):
1515

1616
solved = solver(testjob_m)
1717

18+
assert sum(lt.trimming for lt in solved) == 855
19+
1820
# I don't care about ordering here
1921
assert sorted([r.cuts for r in solved]) == sorted([
2022
(NS(length=500), NS(length=300), NS(length=100)),
@@ -27,7 +29,7 @@ def test_m(solver):
2729
# close to the max for bruteforce!
2830
@pytest.mark.parametrize("solver", [_solve_bruteforce, _solve_FFD, _solve_gapfill])
2931
def test_m_multi(solver):
30-
testjob_m = Job(stocks=(INS(length=900), INS(length=500, quantity=2), INS(length=100, quantity=1)),
32+
testjob_m = Job(stocks=(INS(length=900, quantity=3), INS(length=500, quantity=2), INS(length=100, quantity=1)),
3133
cut_width=10,
3234
required=(
3335
QNS(length=500, quantity=4), QNS(length=300, quantity=3),
@@ -41,10 +43,11 @@ def test_m_multi(solver):
4143
perfect_trimmings = 520
4244
perfect_result = (
4345
ResultEntry(stock=NS(length=500), cuts=(NS(length=500),), trimming=0),
44-
ResultEntry(stock=NS(length=500), cuts=(NS(length=300),), trimming=190),
46+
ResultEntry(stock=NS(length=500), cuts=(NS(length=300), NS(length=100)), trimming=80),
4547
ResultEntry(stock=NS(length=900), cuts=(NS(length=500), NS(length=300)), trimming=80),
4648
ResultEntry(stock=NS(length=900), cuts=(NS(length=500), NS(length=300)), trimming=80),
47-
ResultEntry(stock=NS(length=900), cuts=(NS(length=500), NS(length=100), NS(length=100)), trimming=170))
49+
ResultEntry(stock=NS(length=900), cuts=(NS(length=500), NS(length=100)), trimming=280)
50+
)
4851

4952
if solver == _solve_bruteforce:
5053
assert trimmings == perfect_trimmings

tests/solver/test_special.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,19 @@ def test_close_stocks(solver):
123123
ResultEntry(stock=NS(length=100), cuts=(NS(length=100),), trimming=0),
124124
ResultEntry(stock=NS(length=100), cuts=(NS(length=100),), trimming=0),
125125
ResultEntry(stock=NS(length=100), cuts=(NS(length=100),), trimming=0))
126+
127+
128+
# @pytest.mark.xfail(reason="bug #68")
129+
def test_solution_priorities():
130+
testjob_equal = Job(stocks=(INS(length=2400, quantity=1), (INS(length=6000, quantity=1))), cut_width=20,
131+
required=(QNS(length=1820, quantity=1), QNS(length=666, quantity=3)))
132+
# other solvers have singular result, so no priorities
133+
solved = _solve_bruteforce(testjob_equal)
134+
135+
# assert sum(lt.trimming for lt in solved) == 2102
136+
137+
assert solved == (
138+
ResultEntry(stock=NS(length=6000),
139+
cuts=(NS(length=1820), NS(length=666), NS(length=666), NS(length=666)),
140+
trimming=2102),
141+
)

0 commit comments

Comments
 (0)