from itertools import combinations from pulp import lpSum, LpProblem from random import randint, sample from models.bundle import Bundle from models.item import Item from models.solver_run import SolverRun import logging from lib.errors.item_generation_error import ItemGenerationError def build_constraints(solver_run: SolverRun, problem: LpProblem, items: list[Item], bundles: list[Bundle] or None) -> LpProblem: try: total_form_items = solver_run.total_form_items constraints = solver_run.constraints for constraint in constraints: attribute = constraint.reference_attribute min = constraint.minimum max = constraint.maximum if attribute.type == 'metadata': con = dict(zip([item.id for item in solver_run.items], [item.attribute_exists(attribute) for item in solver_run.items])) problem += lpSum([con[item.id] * items[item.id] for item in solver_run.items]) >= round(total_form_items * (min / 100)), f'{attribute.id} - {attribute.value} - min' problem += lpSum([con[item.id] * items[item.id] for item in solver_run.items]) <= round(total_form_items * (max / 100)), f'{attribute.id} - {attribute.value} - max' elif attribute.type == 'bundle' and bundles: total_bundle_items = sum(bundle.count for bundle in bundles) for bundle in bundles: con = dict(zip([item.id for item in solver_run.items], [(getattr(item, bundle.type, False) == bundle.id) for item in solver_run.items])) problem += lpSum([con[item.id] * items[item.id] for item in solver_run.items]) == bundle.count, f'Bundle constraint for {bundle.type} ({bundle.id})' # make sure all other items added to the form # are not a part of any bundle # currently only supports single bundle constraints, will need refactoring for multiple bundle constraints con = dict(zip([item.id for item in solver_run.items], [(getattr(item, attribute.id, None) == None) for item in solver_run.items])) problem += lpSum([con[item.id] * items[item.id] for item in solver_run.items]) == solver_run.total_form_items - total_bundle_items, f'Remaining items are not of a bundle type' return problem except ValueError as error: logging.error(error) raise ItemGenerationError("Bundle min and/or max larger than bundle amount provided", error.args[0]) def get_random_bundles(total_form_items: int, total_bundles: int, bundles: list[Bundle], found_bundles = False) -> list[Bundle]: selected_bundles = None total_bundle_items = 0 while found_bundles == False: selected_bundles = sample(bundles, total_bundles) total_bundle_items = sum(bundle.count for bundle in selected_bundles) if total_bundle_items <= total_form_items: found_bundles = True if found_bundles == True: return selected_bundles else: return get_random_bundles(total_form_items, total_bundles - 1, bundles) # legacy solution, keep because it may be usefull def valid_bundle_combinations(total_form_items: int, total_bundles: int, min_bundles: int, bundles: list[Bundle], selected_bundle_combinations: list[list[Bundle]] = []) -> list[list[Bundle]]: if total_bundles < min_bundles: return selected_bundle_combinations else: # generate all bundle combinations bundle_combinations = [list(combination) for combination in combinations(bundles, total_bundles)] # iterate through all the combinations # if the combination item count is less than or equal to # the total items on a form, add it to selected bundles for bundle_combination in bundle_combinations: total_bundle_items = sum(bundle.count for bundle in bundle_combination) if total_bundle_items <= total_form_items: selected_bundle_combinations.append(bundle_combination) # recurse to continue generating combinations # all the way to the minimum amount of bundles allowed return valid_bundle_combinations(total_form_items, total_bundles - 1, min_bundles, bundles, selected_bundle_combinations)