From cf3b94dd92f6362982c5c65623a3dad75ae66ba2 Mon Sep 17 00:00:00 2001 From: ModischFabrications Date: Mon, 6 May 2024 23:42:39 +0200 Subject: [PATCH] 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 --- app/settings.py | 2 +- app/solver/solver.py | 11 ++++++----- app/solver/utils.py | 7 +++---- tests/res/out/testresult_s.json | 14 +++++++------- tests/solver/test_large.py | 9 ++++++--- tests/solver/test_special.py | 16 ++++++++++++++++ 6 files changed, 39 insertions(+), 20 deletions(-) diff --git a/app/settings.py b/app/settings.py index 90c0024..59314aa 100644 --- a/app/settings.py +++ b/app/settings.py @@ -2,7 +2,7 @@ from pydantic_settings import BaseSettings # constant; used for git tags -version = "v1.1.0" +version = "v1.1.1" class SolverSettings(BaseSettings): diff --git a/app/solver/solver.py b/app/solver/solver.py index 740663a..f6ab896 100644 --- a/app/solver/solver.py +++ b/app/solver/solver.py @@ -33,9 +33,9 @@ def solve(job: Job) -> Result: # slowest, but perfect solver; originally O(n!), now much faster (see Job.n_combinations()) def _solve_bruteforce(job: Job) -> tuple[ResultEntry, ...]: minimal_trimmings = float('inf') - best_results = [] + best_results: set[tuple[ResultEntry, ...]] = set() - required_orderings = distinct_permutations(job.iterate_required()) + required_orderings = list(distinct_permutations(job.iterate_required())) for stock_ordering in distinct_permutations(job.iterate_stocks()): for required_ordering in required_orderings: result = _group_into_lengths(stock_ordering, required_ordering, job.cut_width) @@ -45,12 +45,13 @@ def _solve_bruteforce(job: Job) -> tuple[ResultEntry, ...]: trimmings = sum(lt.trimming for lt in result) if trimmings < minimal_trimmings: minimal_trimmings = trimmings - best_results = [result] + best_results = set() + best_results.add(sort_entries(result)) elif trimmings == minimal_trimmings: - best_results.append(result) + best_results.add(sort_entries(result)) assert best_results, "No valid solution found" - return sort_entries(find_best_solution(best_results)) + return find_best_solution(best_results) def _group_into_lengths(stocks: tuple[NS, ...], sizes: tuple[NS, ...], cut_width: int) \ diff --git a/app/solver/utils.py b/app/solver/utils.py index 8c5b78c..679df8a 100644 --- a/app/solver/utils.py +++ b/app/solver/utils.py @@ -20,13 +20,12 @@ def calc_trimming(stock_length: int, lengths: Collection[NS], cut_width: int) -> return trimmings -def find_best_solution(solutions: Sequence): +def find_best_solution(solutions: set[tuple[ResultEntry, ...]]): if len(solutions) <= 0: raise ValueError("no solution to search") # TODO evaluate which one aligns with user expectations best (see #68) - # always sort for determinism! - return sorted(solutions, reverse=True)[0] + return sorted(solutions, key=lambda x: max(x), reverse=True)[0] 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 ) -def sort_entries(result_entries: list[ResultEntry]) -> tuple[ResultEntry, ...]: +def sort_entries(result_entries: Sequence[ResultEntry]) -> tuple[ResultEntry, ...]: if len(result_entries) <= 0: raise ValueError("no entries to sort") return tuple(sorted(result_entries)) diff --git a/tests/res/out/testresult_s.json b/tests/res/out/testresult_s.json index 610491d..78aa6d9 100644 --- a/tests/res/out/testresult_s.json +++ b/tests/res/out/testresult_s.json @@ -21,7 +21,7 @@ ] }, "solver_type": "bruteforce", - "time_us": 1868, + "time_us": 1683, "layout": [ { "stock": { @@ -34,8 +34,8 @@ "name": "Part1" }, { - "length": 200, - "name": "Part2" + "length": 500, + "name": "Part1" }, { "length": 200, @@ -46,7 +46,7 @@ "name": "Part2" } ], - "trimming": 392 + "trimming": 92 }, { "stock": { @@ -55,15 +55,15 @@ }, "cuts": [ { - "length": 500, - "name": "Part1" + "length": 200, + "name": "Part2" }, { "length": 200, "name": "Part2" } ], - "trimming": 796 + "trimming": 1096 } ] } diff --git a/tests/solver/test_large.py b/tests/solver/test_large.py index fea18a1..f3f8db3 100644 --- a/tests/solver/test_large.py +++ b/tests/solver/test_large.py @@ -15,6 +15,8 @@ def test_m(solver): solved = solver(testjob_m) + assert sum(lt.trimming for lt in solved) == 855 + # I don't care about ordering here assert sorted([r.cuts for r in solved]) == sorted([ (NS(length=500), NS(length=300), NS(length=100)), @@ -27,7 +29,7 @@ def test_m(solver): # close to the max for bruteforce! @pytest.mark.parametrize("solver", [_solve_bruteforce, _solve_FFD, _solve_gapfill]) def test_m_multi(solver): - testjob_m = Job(stocks=(INS(length=900), INS(length=500, quantity=2), INS(length=100, quantity=1)), + testjob_m = Job(stocks=(INS(length=900, quantity=3), INS(length=500, quantity=2), INS(length=100, quantity=1)), cut_width=10, required=( QNS(length=500, quantity=4), QNS(length=300, quantity=3), @@ -41,10 +43,11 @@ def test_m_multi(solver): perfect_trimmings = 520 perfect_result = ( ResultEntry(stock=NS(length=500), cuts=(NS(length=500),), trimming=0), - ResultEntry(stock=NS(length=500), cuts=(NS(length=300),), trimming=190), + ResultEntry(stock=NS(length=500), cuts=(NS(length=300), NS(length=100)), trimming=80), ResultEntry(stock=NS(length=900), cuts=(NS(length=500), NS(length=300)), trimming=80), ResultEntry(stock=NS(length=900), cuts=(NS(length=500), NS(length=300)), trimming=80), - ResultEntry(stock=NS(length=900), cuts=(NS(length=500), NS(length=100), NS(length=100)), trimming=170)) + ResultEntry(stock=NS(length=900), cuts=(NS(length=500), NS(length=100)), trimming=280) + ) if solver == _solve_bruteforce: assert trimmings == perfect_trimmings diff --git a/tests/solver/test_special.py b/tests/solver/test_special.py index 0b5d9b3..a0991e1 100644 --- a/tests/solver/test_special.py +++ b/tests/solver/test_special.py @@ -123,3 +123,19 @@ def test_close_stocks(solver): ResultEntry(stock=NS(length=100), cuts=(NS(length=100),), trimming=0), ResultEntry(stock=NS(length=100), cuts=(NS(length=100),), trimming=0), ResultEntry(stock=NS(length=100), cuts=(NS(length=100),), trimming=0)) + + +# @pytest.mark.xfail(reason="bug #68") +def test_solution_priorities(): + testjob_equal = Job(stocks=(INS(length=2400, quantity=1), (INS(length=6000, quantity=1))), cut_width=20, + required=(QNS(length=1820, quantity=1), QNS(length=666, quantity=3))) + # other solvers have singular result, so no priorities + solved = _solve_bruteforce(testjob_equal) + + # assert sum(lt.trimming for lt in solved) == 2102 + + assert solved == ( + ResultEntry(stock=NS(length=6000), + cuts=(NS(length=1820), NS(length=666), NS(length=666), NS(length=666)), + trimming=2102), + )