From ab9b5525a4890ee7bca57d425191cbf8f71d1bb6 Mon Sep 17 00:00:00 2001 From: Joshua Burman Date: Wed, 9 Feb 2022 16:33:38 -0500 Subject: [PATCH] reverting to basic cases --- app/helpers/solver_helper.py | 85 +++++++----------------- app/models/bundle.py | 4 -- app/models/solver_run.py | 18 +----- app/services/loft_service.py | 122 +++++++++++++---------------------- 4 files changed, 72 insertions(+), 157 deletions(-) diff --git a/app/helpers/solver_helper.py b/app/helpers/solver_helper.py index d21e2ce..bbd6e89 100644 --- a/app/helpers/solver_helper.py +++ b/app/helpers/solver_helper.py @@ -1,16 +1,10 @@ -from itertools import combinations -from pulp import lpSum, LpProblem +from pulp import lpSum 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: +def build_constraints(solver_run, problem, items, bundles): try: total_form_items = solver_run.total_form_items constraints = solver_run.constraints @@ -30,64 +24,33 @@ def build_constraints(solver_run: SolverRun, problem: LpProblem, items: list[Ite 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: + elif attribute.type == 'bundle': + # TODO: account for many different bundle types, since the id condition in L33 could yield duplicates + if solver_run.bundles != None: + total_bundles = randint(constraint.minimum, constraint.maximum) + selected_bundles = sample(solver_run.bundles, total_bundles) + total_bundle_items = 0 + + for bundle in selected_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})' + total_bundle_items += bundle.count + + # 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, bundle.type, False) == bundle.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]) == 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' + 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) \ No newline at end of file diff --git a/app/models/bundle.py b/app/models/bundle.py index e3ede4c..48eea7e 100644 --- a/app/models/bundle.py +++ b/app/models/bundle.py @@ -1,10 +1,6 @@ from pydantic import BaseModel -from typing import List - -from models.item import Item class Bundle(BaseModel): id: int count: int - items: List[Item] type: str diff --git a/app/models/solver_run.py b/app/models/solver_run.py index 49c32b3..4694ad3 100644 --- a/app/models/solver_run.py +++ b/app/models/solver_run.py @@ -1,5 +1,3 @@ -import logging - from pydantic import BaseModel from typing import List, Optional @@ -22,19 +20,17 @@ class SolverRun(BaseModel): advanced_options: Optional[AdvancedOptions] engine: str - def get_item(self, item_id: int) -> Item or bool: + def get_item(self, item_id): for item in self.items: if str(item.id) == item_id: return item return False - def remove_items(self, items: list[Item]) -> bool: + def remove_items(self, items): self.items = [item for item in self.items if item not in items] return True def generate_bundles(self): - logging.info('Generating Bundles...') - bundle_constraints = (constraint.reference_attribute for constraint in self.constraints if constraint.reference_attribute.type == 'bundle') for bundle_constraint in bundle_constraints: @@ -57,24 +53,16 @@ class SolverRun(BaseModel): self.bundles.append(Bundle( id=attribute_id, count=1, - items=[item], type=type_attribute )) else: self.bundles[bundle_index].count += 1 - self.bundles[bundle_index].items.append(item) else: self.bundles = [Bundle( id=attribute_id, count=1, - items=[item], type=type_attribute )] - def get_constraint(self, name: str) -> Constraint or None: + def get_constraint(self, name): return next((constraint for constraint in self.constraints if constraint.reference_attribute.id == name), None) - - # temp function until we build out bundles to more than just for cases - # for now it treats "bundle" attributes as a single unique constraint - def get_constraint_by_type(self, type: str) -> Constraint or None: - return next((constraint for constraint in self.constraints if constraint.reference_attribute.type == type), None) \ No newline at end of file diff --git a/app/services/loft_service.py b/app/services/loft_service.py index 3b6c207..99dd63c 100644 --- a/app/services/loft_service.py +++ b/app/services/loft_service.py @@ -9,7 +9,6 @@ from models.solver_run import SolverRun from models.solution import Solution from models.form import Form from models.item import Item -from models.bundle import Bundle from services.base import Base @@ -26,7 +25,7 @@ class LoftService(Base): logging.error(error) self.result = self.stream_to_s3_bucket(ItemGenerationError("Provided params causing error in calculation results")) - def create_solver_run_from_attributes(self) -> SolverRun: + def create_solver_run_from_attributes(self): logging.info('Retrieving attributes from message...') # get s3 object self.key = aws_helper.get_key_from_message(self.source) @@ -53,38 +52,56 @@ class LoftService(Base): return solver_run - def generate_solution(self) -> Solution: - logging.info('Generating Solution...') - + def generate_solution(self): + # unsolved solution + solution = Solution( + response_id=random.randint(100, 5000), + forms=[] + ) + # counter for number of forms f = 0 - - # setup vars - items = LpVariable.dicts( - "Item", [item.id for item in self.solver_run.items], lowBound=1, upBound=1, cat='Binary') - - # check if problem request has bundles - bundle_constraint = self.solver_run.get_constraint_by_type('bundle') # iterate for number of forms that require creation # currently creates distinc forms with no item overlap while f < self.solver_run.total_forms: - # unsolved solution - solution = Solution( - response_id=random.randint(100, 5000), - forms=[] - ) + # setup vars + items = LpVariable.dicts( + "Item", [item.id for item in self.solver_run.items], lowBound=1, upBound=1, cat='Binary') + bundles = LpVariable.dicts( + "Bundle", [bundle.id for bundle in self.solver_run.bundles], lowBound=1, upBound=1, cat='Binary') - # initiate problem - problem = None + problem_objection_functions = [] - if bundle_constraint: - problem = self.recursive_solve(items, int(bundle_constraint.minimum), int(bundle_constraint.maximum)) - else: # no bundles - problem = self.solve(items) + # create problem + problem = LpProblem("ata-form-generate", LpMinimize) - # successfull form, increment and exit out of loop - f += 1 + # dummy objective function, because it just makes things easierâ„¢ + problem += lpSum([items[item.id] + for item in self.solver_run.items]) + + # constraints + problem += lpSum([items[item.id] + for item in self.solver_run.items]) == self.solver_run.total_form_items, 'Total form items' + + # dynamic constraints + problem = solver_helper.build_constraints(self.solver_run, problem, items, bundles) + + # multi-objective constraints + for target in self.solver_run.objective_function.tif_targets: + problem += lpSum([item.iif(self.solver_run, target.theta)*items[item.id] + for item in self.solver_run.items]) >= target.value - 8, f'max tif theta ({target.theta}) target value {target.value}' + problem += lpSum([item.iif(self.solver_run, target.theta)*items[item.id] + for item in self.solver_run.items]) <= target.value + 8, f'min tif theta ({target.theta}) target value {target.value}' + + for target in self.solver_run.objective_function.tcc_targets: + problem += lpSum([item.irf(self.solver_run, target.theta)*items[item.id] + for item in self.solver_run.items]) >= target.value - 20, f'max tcc theta ({target.theta}) target value {target.value}' + problem += lpSum([item.irf(self.solver_run, target.theta)*items[item.id] + for item in self.solver_run.items]) <= target.value + 20, f'min tcc theta ({target.theta}) target value {target.value}' + + # solve problem + problem.solve() # add return items and create as a form form_items = service_helper.solution_items(problem.variables(), self.solver_run) @@ -92,59 +109,10 @@ class LoftService(Base): # add form to solution solution.forms.append(Form.create(form_items, self.solver_run, LpStatus[problem.status])) - return solution - - def recursive_solve(self, items, min, max, attempts = 800) -> LpProblem: - bundles_amount = random.randint(min, max) - logging.info(f'min: {min}, max: {max}, bundles amount: {bundles_amount}') - selected_bundles = solver_helper.get_random_bundles( - self.solver_run.total_form_items, - bundles_amount, - self.solver_run.bundles) - - problem = self.solve(items, selected_bundles) - - # if optimal solution found, end recursion - if LpStatus[problem.status] == 'Optimal' or attempts == 0: - return problem - else: - logging.info('recursing...') - logging.info(attempts) - return self.recursive_solve(items, min, max, attempts - 1) - - def solve(self, items: list[Item], bundles: list[Bundle] or None = None) -> LpProblem: - # create problem - problem = LpProblem("ata-form-generate", LpMinimize) - - # dummy objective function, because it just makes things easierâ„¢ - problem += lpSum([items[item.id] - for item in self.solver_run.items]) - - # constraints - problem += lpSum([items[item.id] - for item in self.solver_run.items]) == self.solver_run.total_form_items, 'Total form items' - - # dynamic constraints - problem = solver_helper.build_constraints(self.solver_run, problem, items, bundles) - - # multi-objective constraints - for target in self.solver_run.objective_function.tif_targets: - problem += lpSum([item.iif(self.solver_run, target.theta)*items[item.id] - for item in self.solver_run.items]) >= target.value - 4, f'max tif theta ({target.theta}) target value {target.value}' - problem += lpSum([item.iif(self.solver_run, target.theta)*items[item.id] - for item in self.solver_run.items]) <= target.value + 4, f'min tif theta ({target.theta}) target value {target.value}' - - for target in self.solver_run.objective_function.tcc_targets: - problem += lpSum([item.irf(self.solver_run, target.theta)*items[item.id] - for item in self.solver_run.items]) >= target.value - 18, f'max tcc theta ({target.theta}) target value {target.value}' - problem += lpSum([item.irf(self.solver_run, target.theta)*items[item.id] - for item in self.solver_run.items]) <= target.value + 18, f'min tcc theta ({target.theta}) target value {target.value}' - - # solve problem - problem.solve() - - return problem + # successfull form, increment + f += 1 + return solution def stream_to_s3_bucket(self, error = None): self.file_name = f'{service_helper.key_to_uuid(self.key)}.csv'