enemy management v2.0

This commit is contained in:
Joshua Burman 2023-11-16 12:52:37 -05:00
parent 5b4387a04b
commit 6e320d7cbc
7 changed files with 131 additions and 55 deletions

View File

@ -2,6 +2,6 @@ from pydantic import BaseModel
from typing import Optional, Union from typing import Optional, Union
class Attribute(BaseModel): class Attribute(BaseModel):
value: Optional[Union[str,int]] value: Optional[Union[str,int,list]]
type: Optional[str] type: Optional[str]
id: str id: str

View File

@ -59,3 +59,13 @@ class Bundle(BaseModel):
def ordered_items(self) -> List[Item]: def ordered_items(self) -> List[Item]:
return sorted(self.items, key=lambda item: item.position) return sorted(self.items, key=lambda item: item.position)
# are there enemys in the bundle?
def enemy_pair_count(self, pair: List[Item]) -> int:
pair_count = 0
for item in self.items:
if pair in item.enemy_pairs():
pair_count += 1
return pair_count

View File

@ -0,0 +1,31 @@
from __future__ import annotations
from typing import List
from models.constraints.generic_constraint import *
class EnemyPairConstraint(GenericConstraint):
@classmethod
def create(cls: Type[_T], pair: List[int]) -> _T:
return cls(
minimum=0,
maximum=0,
reference_attribute=Attribute(
value=pair,
type='enemy_pair',
id='enemy_pair'
)
)
def build(self, problem_handler: Problem, **_) -> None:
logging.info('Enemy Pair Constraint Generating...')
pair = self.reference_attribute.value
problem_handler.problem += lpSum(
[
bundle.enemy_pair_count(pair) * problem_handler.solver_bundles_var[bundle.id]
for bundle in problem_handler.bundles
] + [
(pair in item.enemy_pairs()).real * problem_handler.solver_items_var[item.id]
for item in problem_handler.items
]
) <= 1, f'Enemy Pair constraint for pair: {pair}'

View File

@ -1,5 +1,6 @@
import logging
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from typing import List, Optional from typing import List, Optional, Tuple
from models.attribute import Attribute from models.attribute import Attribute
@ -61,3 +62,15 @@ class Item(BaseModel):
total += self.irf(solver_run.irt_model, target.theta) total += self.irf(solver_run.irt_model, target.theta)
return total return total
def enemy_pairs(self, sort: bool = True) -> List[List[int]]:
pairs = []
for enemy_id in self.enemies:
pair = [self.id, enemy_id]
if sort: pair.sort()
pairs.append(pair)
return pairs

View File

@ -43,59 +43,64 @@ class Problem(BaseModel):
def solve(self, solver_run: SolverRun, enemy_ids: List[int] = []) -> LpProblem: def solve(self, solver_run: SolverRun, enemy_ids: List[int] = []) -> LpProblem:
logging.info('solving problem...') logging.info('solving problem...')
self.problem.solve()
# NOTICE: Legacy enemies implementation
# leaving this in, just in case the current impl fails to function
# and we need an immediate solution
# if we allow enemies, go through the normal solving process # if we allow enemies, go through the normal solving process
if solver_run.allow_enemies: # if solver_run.allow_enemies:
logging.info('enemes allowed, so just solving') # logging.info('enemes allowed, so just solving')
self.problem.solve() # self.problem.solve()
# otherwise begin the process of filtering enemies # # otherwise begin the process of filtering enemies
else: # else:
self.problem.solve() # self.problem.solve()
# however, if the solve was infeasible, kick it back # # however, if the solve was infeasible, kick it back
# to the normal process # # to the normal process
if LpStatus[self.problem.status] == 'Infeasible': # if LpStatus[self.problem.status] == 'Infeasible':
return self.problem # return self.problem
# otherwise continue # # otherwise continue
else: # else:
# get items from solution # # get items from solution
solved_items, _ = service_helper.solution_items(self.problem.variables(), solver_run) # solved_items, _ = service_helper.solution_items(self.problem.variables(), solver_run)
# sacred items will remain the same (with new items added each run) between solve attempts # # sacred items will remain the same (with new items added each run) between solve attempts
# but new enemies will be appended # # but new enemies will be appended
sacred_ids, new_enemy_ids = sanctify(solved_items) # sacred_ids, new_enemy_ids = sanctify(solved_items)
# the current solve run found new enemies # # the current solve run found new enemies
if new_enemy_ids: # if new_enemy_ids:
logging.info('enemies found, adding constraints...') # logging.info('enemies found, adding constraints...')
# append the new enemies to the enemies_id list # # append the new enemies to the enemies_id list
enemy_ids = list(set(enemy_ids+new_enemy_ids)) # enemy_ids = list(set(enemy_ids+new_enemy_ids))
# remove old enemy/sacred constraints # # remove old enemy/sacred constraints
if 'Exclude_enemy_items' in self.problem.constraints.keys(): self.problem.constraints.pop('Exclude_enemy_items') # if 'Exclude_enemy_items' in self.problem.constraints.keys(): self.problem.constraints.pop('Exclude_enemy_items')
if 'Include_sacred_items' in self.problem.constraints.keys(): self.problem.constraints.pop('Include_sacred_items') # if 'Include_sacred_items' in self.problem.constraints.keys(): self.problem.constraints.pop('Include_sacred_items')
# add constraint to not allow enemy items # # add constraint to not allow enemy items
self.problem += lpSum([ # self.problem += lpSum([
len(bundle.find_items(enemy_ids)) * self.solver_bundles_var[bundle.id] # len(bundle.find_items(enemy_ids)) * self.solver_bundles_var[bundle.id]
for bundle in self.bundles # for bundle in self.bundles
] + [ # ] + [
(item.id in enemy_ids) * self.solver_items_var[item.id] # (item.id in enemy_ids) * self.solver_items_var[item.id]
for item in self.items # for item in self.items
]) == 0, 'Exclude enemy items' # ]) == 0, 'Exclude enemy items'
# add constraint to use sacred items # # add constraint to use sacred items
self.problem += lpSum([ # self.problem += lpSum([
len(bundle.find_items(sacred_ids)) * self.solver_bundles_var[bundle.id] # len(bundle.find_items(sacred_ids)) * self.solver_bundles_var[bundle.id]
for bundle in self.bundles # for bundle in self.bundles
] + [ # ] + [
(item.id in sacred_ids) * self.solver_items_var[item.id] # (item.id in sacred_ids) * self.solver_items_var[item.id]
for item in self.items # for item in self.items
]) == len(sacred_ids), 'Include sacred items' # ]) == len(sacred_ids), 'Include sacred items'
# recursively solve until no enemies exist or infeasible # # recursively solve until no enemies exist or infeasible
logging.info('recursively solving...') # logging.info('recursively solving...')
self.solve(solver_run) # self.solve(solver_run)
return self.problem return self.problem

View File

@ -1,9 +1,9 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, List, Literal, Optional, Union, TypeVar from typing import TYPE_CHECKING, List, Literal, Optional, Tuple, Union, TypeVar
from pulp import lpSum from pulp import lpSum
from pydantic import BaseModel from pydantic import BaseModel
import itertools
import logging import logging
from models.item import Item from models.item import Item
@ -12,6 +12,7 @@ from models.constraints.metadata_constraint import MetadataConstraint
from models.constraints.bundle_constraint import BundleConstraint from models.constraints.bundle_constraint import BundleConstraint
from models.constraints.form_uniqueness_constraint import FormUniquenessConstraint from models.constraints.form_uniqueness_constraint import FormUniquenessConstraint
from models.constraints.total_form_items_constraint import TotalFormItemsConstraint from models.constraints.total_form_items_constraint import TotalFormItemsConstraint
from models.constraints.enemy_pair_constraint import EnemyPairConstraint
from models.irt_model import IRTModel from models.irt_model import IRTModel
from models.bundle import Bundle from models.bundle import Bundle
from models.objective_function import ObjectiveFunction from models.objective_function import ObjectiveFunction
@ -45,13 +46,6 @@ class SolverRun(BaseModel):
# ideally we'd change the payload to determine what type it is # ideally we'd change the payload to determine what type it is
constraints: [ConstraintType] = [] constraints: [ConstraintType] = []
# total form items
constraints.append(TotalFormItemsConstraint.create(self.total_form_items))
# ensure form uniqueness
if self.advanced_options.ensure_form_uniqueness:
constraints.append(FormUniquenessConstraint.create(self.total_form_items - 1))
# repackage to create appropriate constraint types # repackage to create appropriate constraint types
for constraint in self.constraints: for constraint in self.constraints:
if constraint.reference_attribute.type == 'metadata': if constraint.reference_attribute.type == 'metadata':
@ -80,6 +74,18 @@ class SolverRun(BaseModel):
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_constraints(self) -> None:
# total form items
self.constraints.append(TotalFormItemsConstraint.create(self.total_form_items))
# ensure form uniqueness
if self.advanced_options.ensure_form_uniqueness:
self.constraints.append(FormUniquenessConstraint.create(self.total_form_items - 1))
# enemies constraints
for pair in self.enemy_pairs():
self.constraints.append(EnemyPairConstraint.create(pair))
def generate_bundles(self): def generate_bundles(self):
logging.info('Generating Bundles...') logging.info('Generating Bundles...')
# confirms bundle constraints exists # confirms bundle constraints exists
@ -149,3 +155,13 @@ class SolverRun(BaseModel):
else: else:
return self.items return self.items
def enemy_pairs(self) -> List[List[int]]:
pairs = []
for item in self.items:
# add enemy pairs for item to pairs
pairs += item.enemy_pairs()
# remove duplicates
pairs.sort()
return list(k for k,_ in itertools.groupby(pairs))

View File

@ -21,6 +21,7 @@ class FormGenerationService(Base):
try: try:
self.solver_run = self.create_solver_run_from_attributes() self.solver_run = self.create_solver_run_from_attributes()
self.solver_run.generate_bundles() self.solver_run.generate_bundles()
self.solver_run.generate_constraints()
self.solution = self.generate_solution() self.solution = self.generate_solution()
self.result = self.stream_to_s3_bucket() self.result = self.stream_to_s3_bucket()
except ItemGenerationError as error: except ItemGenerationError as error: