reverting to basic cases

This commit is contained in:
Joshua Burman 2022-02-09 16:33:38 -05:00
parent 2232b3342c
commit ab9b5525a4
4 changed files with 72 additions and 157 deletions

View File

@ -1,16 +1,10 @@
from itertools import combinations from pulp import lpSum
from pulp import lpSum, LpProblem
from random import randint, sample from random import randint, sample
from models.bundle import Bundle
from models.item import Item
from models.solver_run import SolverRun
import logging import logging
from lib.errors.item_generation_error import ItemGenerationError 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: try:
total_form_items = solver_run.total_form_items total_form_items = solver_run.total_form_items
constraints = solver_run.constraints constraints = solver_run.constraints
@ -30,64 +24,33 @@ def build_constraints(solver_run: SolverRun, problem: LpProblem, items: list[Ite
problem += lpSum([con[item.id] problem += lpSum([con[item.id]
* items[item.id] * items[item.id]
for item in solver_run.items]) <= round(total_form_items * (max / 100)), f'{attribute.id} - {attribute.value} - max' for item in solver_run.items]) <= round(total_form_items * (max / 100)), f'{attribute.id} - {attribute.value} - max'
elif attribute.type == 'bundle' and bundles: elif attribute.type == 'bundle':
total_bundle_items = sum(bundle.count for bundle in bundles) # TODO: account for many different bundle types, since the id condition in L33 could yield duplicates
if solver_run.bundles != None:
for bundle in bundles: 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], con = dict(zip([item.id for item in solver_run.items],
[(getattr(item, bundle.type, False) == bundle.id) [(getattr(item, attribute.id, None) == None)
for item in solver_run.items])) for item in solver_run.items]))
problem += lpSum([con[item.id] problem += lpSum([con[item.id]
* items[item.id] * items[item.id]
for item in solver_run.items]) == bundle.count, f'Bundle constraint for {bundle.type} ({bundle.id})' for item in solver_run.items]) == solver_run.total_form_items - total_bundle_items, f'Remaining items are not of a bundle type'
# 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 return problem
except ValueError as error: except ValueError as error:
logging.error(error) logging.error(error)
raise ItemGenerationError("Bundle min and/or max larger than bundle amount provided", error.args[0]) 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)

View File

@ -1,10 +1,6 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import List
from models.item import Item
class Bundle(BaseModel): class Bundle(BaseModel):
id: int id: int
count: int count: int
items: List[Item]
type: str type: str

View File

@ -1,5 +1,3 @@
import logging
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Optional from typing import List, Optional
@ -22,19 +20,17 @@ class SolverRun(BaseModel):
advanced_options: Optional[AdvancedOptions] advanced_options: Optional[AdvancedOptions]
engine: str engine: str
def get_item(self, item_id: int) -> Item or bool: def get_item(self, item_id):
for item in self.items: for item in self.items:
if str(item.id) == item_id: if str(item.id) == item_id:
return item return item
return False 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] self.items = [item for item in self.items if item not in items]
return True return True
def generate_bundles(self): def generate_bundles(self):
logging.info('Generating Bundles...')
bundle_constraints = (constraint.reference_attribute for constraint in self.constraints if constraint.reference_attribute.type == 'bundle') bundle_constraints = (constraint.reference_attribute for constraint in self.constraints if constraint.reference_attribute.type == 'bundle')
for bundle_constraint in bundle_constraints: for bundle_constraint in bundle_constraints:
@ -57,24 +53,16 @@ class SolverRun(BaseModel):
self.bundles.append(Bundle( self.bundles.append(Bundle(
id=attribute_id, id=attribute_id,
count=1, count=1,
items=[item],
type=type_attribute type=type_attribute
)) ))
else: else:
self.bundles[bundle_index].count += 1 self.bundles[bundle_index].count += 1
self.bundles[bundle_index].items.append(item)
else: else:
self.bundles = [Bundle( self.bundles = [Bundle(
id=attribute_id, id=attribute_id,
count=1, count=1,
items=[item],
type=type_attribute 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) 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)

View File

@ -9,7 +9,6 @@ from models.solver_run import SolverRun
from models.solution import Solution from models.solution import Solution
from models.form import Form from models.form import Form
from models.item import Item from models.item import Item
from models.bundle import Bundle
from services.base import Base from services.base import Base
@ -26,7 +25,7 @@ class LoftService(Base):
logging.error(error) logging.error(error)
self.result = self.stream_to_s3_bucket(ItemGenerationError("Provided params causing error in calculation results")) 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...') logging.info('Retrieving attributes from message...')
# get s3 object # get s3 object
self.key = aws_helper.get_key_from_message(self.source) self.key = aws_helper.get_key_from_message(self.source)
@ -53,38 +52,56 @@ class LoftService(Base):
return solver_run return solver_run
def generate_solution(self) -> Solution: def generate_solution(self):
logging.info('Generating Solution...') # unsolved solution
solution = Solution(
response_id=random.randint(100, 5000),
forms=[]
)
# counter for number of forms # counter for number of forms
f = 0 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 # iterate for number of forms that require creation
# currently creates distinc forms with no item overlap # currently creates distinc forms with no item overlap
while f < self.solver_run.total_forms: while f < self.solver_run.total_forms:
# unsolved solution # setup vars
solution = Solution( items = LpVariable.dicts(
response_id=random.randint(100, 5000), "Item", [item.id for item in self.solver_run.items], lowBound=1, upBound=1, cat='Binary')
forms=[] bundles = LpVariable.dicts(
) "Bundle", [bundle.id for bundle in self.solver_run.bundles], lowBound=1, upBound=1, cat='Binary')
# initiate problem problem_objection_functions = []
problem = None
if bundle_constraint: # create problem
problem = self.recursive_solve(items, int(bundle_constraint.minimum), int(bundle_constraint.maximum)) problem = LpProblem("ata-form-generate", LpMinimize)
else: # no bundles
problem = self.solve(items)
# successfull form, increment and exit out of loop # dummy objective function, because it just makes things easier™
f += 1 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 # add return items and create as a form
form_items = service_helper.solution_items(problem.variables(), self.solver_run) form_items = service_helper.solution_items(problem.variables(), self.solver_run)
@ -92,59 +109,10 @@ class LoftService(Base):
# add form to solution # add form to solution
solution.forms.append(Form.create(form_items, self.solver_run, LpStatus[problem.status])) solution.forms.append(Form.create(form_items, self.solver_run, LpStatus[problem.status]))
return solution # successfull form, increment
f += 1
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
return solution
def stream_to_s3_bucket(self, error = None): def stream_to_s3_bucket(self, error = None):
self.file_name = f'{service_helper.key_to_uuid(self.key)}.csv' self.file_name = f'{service_helper.key_to_uuid(self.key)}.csv'