From 5ed14c913ad4b7e6ec01610e9fe640021f5218e6 Mon Sep 17 00:00:00 2001 From: Jesse Myrberg Date: Sun, 7 Aug 2022 01:17:31 +0300 Subject: [PATCH] Add MTHG for Generalized Assignment Problem (#29) * Add MTHG for Generalized Assignment Problem * Correct README example order * Fix README typo --- README.md | 16 ++-- mknapsack/_algos.f | 2 + mknapsack/_exceptions.py | 7 ++ mknapsack/_generalized_assignment.py | 80 +++++++++++++++---- tests/test__generalized_assignment.py | 107 ++++++++++++++++++-------- 5 files changed, 153 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index e9d08ee..be69c6e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Solving knapsack problems with Python using algorithms by [Martello and Toth](ht * Multiple 0-1 knapsack problem: MTM, MTHM * Change-making problem: MTC2 * Bounded change-making problem: MTCB -* Generalized assignment problem: MTG +* Generalized assignment problem: MTG, MTHG Documentation is available [here](https://mknapsack.readthedocs.io). @@ -141,19 +141,19 @@ res = solve_bounded_change_making(weights, n_items, capacity) ```python from mknapsack import solve_generalized_assignment -# Given seven item types with the following knapsack dependent weights: -weights = [[4, 1, 2, 1, 4, 3, 8], - [9, 9, 8, 1, 3, 8, 7]] - -# ...and the following knapsack dependent weights: +# Given seven item types with the following knapsack dependent profits: profits = [[6, 9, 4, 2, 10, 3, 6], [4, 8, 9, 1, 7, 5, 4]] -# ...and two knapsack with the following capacities: +# ...and the following knapsack dependent weights: +weights = [[4, 1, 2, 1, 4, 3, 8], + [9, 9, 8, 1, 3, 8, 7]] + +# ...and two knapsacks with the following capacities: capacities = [11, 22] # Assign items into the knapsacks while maximizing profits -res = solve_generalized_assignment(weights, profits, capacities) +res = solve_generalized_assignment(profits, weights, capacities) ``` diff --git a/mknapsack/_algos.f b/mknapsack/_algos.f index ea07980..a78349b 100644 --- a/mknapsack/_algos.f +++ b/mknapsack/_algos.f @@ -7854,6 +7854,8 @@ subroutine mthg ( n, m, p, w, c, minmax, z, xstar, jck ) c parameters are unchanged, but p(i,j) is set to 0 for all pairs c (i,j) such that w(i,j) .gt. c(i) . c +cf2py intent(in) n, m, p, w, c, minmax, jck +cf2py intent(out) z, xstar integer p(50,500),w(50,500),c(50),xstar(500),z integer zm integer best(500) diff --git a/mknapsack/_exceptions.py b/mknapsack/_exceptions.py index 5eac277..dc73a60 100644 --- a/mknapsack/_exceptions.py +++ b/mknapsack/_exceptions.py @@ -92,6 +92,13 @@ class FortranInputCheckError(Exception): -5: 'One or more of weights is greater than knapsack capacity', -6: 'One or more knapsacks cannot fit any items', -7: 'Number of branching trees is too small for the problem size' + }, + 'mthg': { + -1: 'Number of knapsacks is less than 2', + -2: 'Number of items is less than 2', + -3: 'Profit, weight, or capacity is <= 0', + -4: 'One or more of weights is greater than knapsack capacity', + -5: 'One or more knapsacks cannot fit any items' } } diff --git a/mknapsack/_generalized_assignment.py b/mknapsack/_generalized_assignment.py index 7241fb4..103e49d 100644 --- a/mknapsack/_generalized_assignment.py +++ b/mknapsack/_generalized_assignment.py @@ -7,7 +7,7 @@ import numpy as np -from mknapsack._algos import mtg +from mknapsack._algos import mtg, mthg from mknapsack._exceptions import FortranInputCheckError, NoSolutionError, \ ProblemSizeError from mknapsack._utils import preprocess_array, pad_array @@ -21,7 +21,7 @@ def solve_generalized_assignment( weights: List[List[int]], capacities: List[int], maximize: bool = True, - method: str = 'mtg', + method: str = 'mthg', method_kwargs: Optional[dict] = None, verbose: bool = False ) -> np.ndarray: @@ -41,15 +41,22 @@ def solve_generalized_assignment( method: Algorithm to use for solving, should be one of - - 'mtg' - provides a fast heuristical solution that might not - be the global optimum, but is suitable for larger problems, - or an exact solution if required + - 'mtg' - provides a fast heuristical solution or an exact + solution if required, but is not the most suitable for larger + problems The problem size is limited as follows: * Number of knapsacks <= 10 * Number of items <= 100 - Defaults to 'mtg'. + - 'mthg' - provides a fast heuristical solution that might not + be the global optimum, but is suitable for larger problems + + The problem size is limited as follows: + * Number of knapsacks <= 50 + * Number of items <= 500 + + Defaults to 'mthg'. method_kwargs: Keyword arguments to pass to a given `method`. @@ -62,6 +69,10 @@ def solve_generalized_assignment( * **check_inputs** (int, optional) - Whether to check inputs or not (0=no, 1=yes). Defaults to 1. + - 'mthg' + * **check_inputs** (int, optional) - Whether to check + inputs or not (0=no, 1=yes). Defaults to 1. + Defaults to None. Returns: @@ -81,10 +92,10 @@ def solve_generalized_assignment( from mknapsack import solve_generalized_assignment res = solve_generalized_assignment( - weights=[[4, 1, 2, 1, 4, 3, 8], - [9, 9, 8, 1, 3, 8, 7]], profits=[[6, 9, 4, 2, 10, 3, 6], [4, 8, 9, 1, 7, 5, 4]], + weights=[[4, 1, 2, 1, 4, 3, 8], + [9, 9, 8, 1, 3, 8, 7]], capacities=[11, 22], maximize=True ) @@ -117,17 +128,17 @@ def solve_generalized_assignment( raise ValueError('Profits length must be equal to weights ' f'({n_profits != n_weights}') + # The problem size is limited on Fortran side + error_msg = ( + 'The number of {0} {1} exceeds {2}, and there is no guarantee ' + 'that the algorithm will work as intended or provides a solution ' + 'at all') + method = method.lower() method_kwargs = method_kwargs or {} if method == 'mtg': maxm = 10 maxn = 100 - - # The problem size is limited on Fortran side - error_msg = ( - 'The number of {0} {1} exceeds {2}, and there is no guarantee ' - 'that the algorithm will work as intended or provides a solution ' - 'at all') if m > maxm: raise ProblemSizeError(error_msg.format('knapsacks', m, maxm)) if n > maxn: @@ -152,9 +163,7 @@ def solve_generalized_assignment( back=back, jck=method_kwargs.get('check_inputs', 1) ) - print(z) - print(x) - print(jb) + if z == 0: raise NoSolutionError('No feasible solution found') elif z < 0: @@ -166,6 +175,43 @@ def solve_generalized_assignment( logger.info(f'Solution vector: {x}') bound_name = 'Upper' if maximize else 'Lower' logger.info(f'{bound_name} bound: {jb}') + elif method == 'mthg': + maxm = 50 + maxn = 500 + + # The problem size is limited on Fortran side + error_msg = ( + 'The number of {0} {1} exceeds {2}, and there is no guarantee ' + 'that the algorithm will work as intended or provides a solution ' + 'at all') + if m > maxm: + raise ProblemSizeError(error_msg.format('knapsacks', m, maxm)) + if n > maxn: + raise ProblemSizeError(error_msg.format('items', n, maxn)) + + p = pad_array(profits, (maxm, maxn)) + w = pad_array(weights, (maxm, maxn)) + c = pad_array(capacities, maxm) + + z, x = mthg( + n=n, + m=m, + p=p, + w=w, + c=c, + minmax=2 if maximize else 1, + jck=method_kwargs.get('check_inputs', 1) + ) + + if z == 0: + raise NoSolutionError('No feasible solution found') + elif z < 0: + raise FortranInputCheckError(method=method, z=z) + + if verbose: + logger.info(f'Method: "{method}"') + logger.info(f'Total profit: {z}') + logger.info(f'Solution vector: {x}') else: raise ValueError(f'Given method "{method}" not known') diff --git a/tests/test__generalized_assignment.py b/tests/test__generalized_assignment.py index f4e5dcc..4d4210e 100644 --- a/tests/test__generalized_assignment.py +++ b/tests/test__generalized_assignment.py @@ -45,8 +45,8 @@ 'solution': [2, 1, 1, 2, 1] } -generalized_assignment_case_large = { # yagiura2004/c10400 - 'case': 'large', +generalized_assignment_case_medium = { # yagiura2004/c10400 + 'case': 'medium', 'weights': [ [12, 15, 25, 13, 15, 6, 14, 9, 22, 24, 16, 18, 10, 14, 9, 23, 11, 19, 9, 18, 14, 23, 10, 15, 18, 8, 24, 20, 20, 19, 16, 24, @@ -202,11 +202,11 @@ 4, 9, 9, 5, 2, 8, 1, 5, 3, 4, 3, 4, 5, 4, 6] } -generalized_assignment_case_large_minimize = { - 'case': 'large-minimize', - 'weights': generalized_assignment_case_large['weights'], - 'profits': generalized_assignment_case_large['profits'], - 'capacities': generalized_assignment_case_large['capacities'], +generalized_assignment_case_medium_minimize = { + 'case': 'medium-minimize', + 'weights': generalized_assignment_case_medium['weights'], + 'profits': generalized_assignment_case_medium['profits'], + 'capacities': generalized_assignment_case_medium['capacities'], 'maximize': False, 'total_profit': 1326, 'solution': @@ -218,49 +218,84 @@ 3, 4, 8, 7, 3, 2, 4, 1, 9, 9, 4, 9, 8, 8, 7] } + +generalized_assignment_case_large = { + 'case': 'large', + 'weights': np.tile(generalized_assignment_case_medium['weights'], (4, 4)), + 'profits': np.tile(generalized_assignment_case_medium['profits'], (4, 4)), + 'capacities': generalized_assignment_case_medium['capacities'] * 4, + 'total_profit': None, + 'solution': None +} + generalized_assignment_success_cases = [ {'method': 'mtg', **generalized_assignment_case_small}, {'method': 'mtg', **generalized_assignment_case_small_reverse}, {'method': 'mtg', **generalized_assignment_case_small2}, - {'method': 'mtg', **generalized_assignment_case_large}, - {'method': 'mtg', **generalized_assignment_case_large_minimize}, - {'method': 'mtg', **generalized_assignment_case_large, + {'method': 'mtg', **generalized_assignment_case_medium}, + {'method': 'mtg', **generalized_assignment_case_medium_minimize}, + {'method': 'mtg', **generalized_assignment_case_medium, 'method_kwargs': {'require_exact': 1}}, - {'method': 'mtg', **generalized_assignment_case_large_minimize, + {'method': 'mtg', **generalized_assignment_case_medium_minimize, 'method_kwargs': {'require_exact': 1}}, {'method': 'mtg', **generalized_assignment_case_small, - 'method_kwargs': {'max_backtracks': 1, 'require_exact': 0}, - 'tolerance': 0} + 'method_kwargs': {'max_backtracks': 1, 'require_exact': 0}}, + {'method': 'mthg', **generalized_assignment_case_small}, + {'method': 'mthg', **generalized_assignment_case_small_reverse, + 'tolerance': 0.03}, + {'method': 'mthg', **generalized_assignment_case_small2}, + {'method': 'mthg', **generalized_assignment_case_medium}, + {'method': 'mthg', **generalized_assignment_case_medium_minimize}, + {'method': 'mthg', **generalized_assignment_case_large} ] - generalized_assignment_fail_cases_base = [ { 'case': 'mtg_too_many_items', 'methods': ['mtg'], 'weights': np.concatenate([ - generalized_assignment_case_large['weights'], - np.array(generalized_assignment_case_large['weights'])[:, -1:] + generalized_assignment_case_medium['weights'], + np.array(generalized_assignment_case_medium['weights'])[:, -1:] ], axis=1), 'profits': np.concatenate([ - generalized_assignment_case_large['profits'], - np.array(generalized_assignment_case_large['profits'])[:, -1:] + generalized_assignment_case_medium['profits'], + np.array(generalized_assignment_case_medium['profits'])[:, -1:] ], axis=1), - 'capacities': generalized_assignment_case_large['capacities'], + 'capacities': generalized_assignment_case_medium['capacities'], 'fail_type': ProblemSizeError }, { 'case': 'mtg_too_many_knapsacks', 'methods': ['mtg'], 'weights': np.concatenate([ - generalized_assignment_case_large['weights'], - generalized_assignment_case_large['weights'] + generalized_assignment_case_medium['weights'], + generalized_assignment_case_medium['weights'] ], axis=0), 'profits': np.concatenate([ - generalized_assignment_case_large['profits'], - generalized_assignment_case_large['profits'] + generalized_assignment_case_medium['profits'], + generalized_assignment_case_medium['profits'] ], axis=0), - 'capacities': generalized_assignment_case_large['capacities'] * 2, + 'capacities': generalized_assignment_case_medium['capacities'] * 2, + 'fail_type': ProblemSizeError + }, + { + 'case': 'mthg_too_many_items', + 'methods': ['mthg'], + 'weights': np.tile( + generalized_assignment_case_medium['weights'], (1, 6)), + 'profits': np.tile( + generalized_assignment_case_medium['profits'], (1, 6)), + 'capacities': generalized_assignment_case_medium['capacities'], + 'fail_type': ProblemSizeError + }, + { + 'case': 'mthg_too_many_knapsacks', + 'methods': ['mthg'], + 'weights': np.tile( + generalized_assignment_case_medium['weights'], (6, 1)), + 'profits': np.tile( + generalized_assignment_case_medium['profits'], (6, 1)), + 'capacities': generalized_assignment_case_medium['capacities'] * 6, 'fail_type': ProblemSizeError }, { @@ -285,7 +320,7 @@ }, { 'case': 'only_one_knapsack', - 'methods': ['mtg'], + 'methods': ['mtg', 'mthg'], 'weights': np.array(generalized_assignment_case_small['weights'])[:1, :], 'profits': @@ -295,7 +330,7 @@ }, { 'case': 'only_one_item', - 'methods': ['mtg'], + 'methods': ['mtg', 'mthg'], 'weights': np.array(generalized_assignment_case_small['weights'])[:, :1], 'profits': @@ -305,7 +340,7 @@ }, { 'case': 'weight_lte_0', - 'methods': ['mtg'], + 'methods': ['mtg', 'mthg'], 'weights': [[4, 1, 2, 1, 4, 3, 0], [9, 9, 8, 1, 3, 8, 7]], 'profits': [[6, 9, 4, 2, 10, 3, 6], @@ -315,7 +350,7 @@ }, { 'case': 'profit_lte_0', - 'methods': ['mtg'], + 'methods': ['mtg', 'mthg'], 'weights': [[4, 1, 2, 1, 4, 3, 8], [9, 9, 8, 1, 3, 8, 7]], 'profits': [[6, 9, 4, 2, 10, 0, 6], @@ -325,7 +360,7 @@ }, { 'case': 'capacity_lte_0', - 'methods': ['mtg'], + 'methods': ['mtg', 'mthg'], 'weights': [[4, 1, 2, 1, 4, 3, 8], [9, 9, 8, 1, 3, 8, 7]], 'profits': [[6, 9, 4, 2, 10, 3, 6], @@ -335,7 +370,7 @@ }, { 'case': 'item_weight_gt_capacity', - 'methods': ['mtg'], + 'methods': ['mtg', 'mthg'], 'weights': [[4, 1, 2, 1, 4, 3, 12], [9, 9, 8, 1, 3, 8, 23]], 'profits': [[6, 9, 4, 2, 10, 3, 6], @@ -345,7 +380,7 @@ }, { 'case': 'capacity_lt_weights', - 'methods': ['mtg'], + 'methods': ['mtg', 'mthg'], 'weights': [[4, 1, 2, 1, 4, 3, 8], [9, 9, 8, 7, 6, 8, 7]], 'profits': [[6, 9, 4, 2, 10, 3, 6], @@ -355,7 +390,7 @@ }, { 'case': 'no_solution', - 'methods': ['mtg'], + 'methods': ['mtg', 'mthg'], 'weights': [[4, 10, 2, 1, 4, 3, 8], [9, 21, 8, 7, 6, 8, 7]], 'profits': [[6, 9, 4, 2, 10, 3, 6], @@ -395,14 +430,18 @@ def test_solve_generalized_assignment(params): assert isinstance(res, np.ndarray) assert len(res) == len(weights[0]) + def choose(a, c): + return a[c, range(a.shape[1])] + # Ensure no overweight in knapsacks weights = np.array(weights) - res_weights = np.choose(res - 1, weights) + res_weights = choose(weights, res - 1) for i, capacity in enumerate(capacities): assert res_weights[res == i + 1].sum() <= capacity # Ensure profit within given limits - res_profit = np.choose(res - 1, profits).sum() + profits = np.array(profits) + res_profit = choose(profits, res - 1).sum() if total_profit is not None: assert res_profit >= (1 - tolerance) * total_profit and \ res_profit <= (1 + tolerance) * total_profit