From 8667bec8d505c9eb2b56413fefc5e2171b8745ce Mon Sep 17 00:00:00 2001 From: Joshua Burman Date: Mon, 13 Nov 2023 16:37:35 -0500 Subject: [PATCH] solve without enemies, initial impl. still requires factoring out --- app/helpers/irt_helper.py | 3 + app/models/bundle.py | 10 ++++ app/models/item.py | 2 +- app/models/problem.py | 73 +++++++++++++++++++++++-- app/models/solver_run.py | 1 + app/services/form_generation_service.py | 4 +- 6 files changed, 85 insertions(+), 8 deletions(-) diff --git a/app/helpers/irt_helper.py b/app/helpers/irt_helper.py index 5a9821e..19cba93 100644 --- a/app/helpers/irt_helper.py +++ b/app/helpers/irt_helper.py @@ -1,8 +1,11 @@ +from typing import List + from lib.irt.test_response_function import TestResponseFunction from lib.irt.test_information_function import TestInformationFunction from models.targets.tif_target import TifTarget from models.targets.tcc_target import TccTarget +from models.item import Item def generate_tif_results(items, solver_run): targets = [] diff --git a/app/models/bundle.py b/app/models/bundle.py index 475d710..bd778ff 100644 --- a/app/models/bundle.py +++ b/app/models/bundle.py @@ -14,6 +14,16 @@ class Bundle(BaseModel): items: List[Item] type: str + def find_items(self, requested_items_ids: [int]) -> [Item]: + found_items = [] + + for item in self.items: + if item.id in requested_items_ids: + found_items.append(item) + + return found_items + + def tif(self, irt_model: IRTModel, theta: float) -> float: return TestInformationFunction(irt_model).calculate(self.items, theta=theta) diff --git a/app/models/item.py b/app/models/item.py index f084b78..b82dd03 100644 --- a/app/models/item.py +++ b/app/models/item.py @@ -10,7 +10,7 @@ class Item(BaseModel): id: int position: Optional[int] = None passage_id: Optional[int] = None - enemies: Optional[int] = None + enemies: List[int] = [] workflow_state: Optional[str] = None attributes: List[Attribute] = None b_param: float = 0.00 diff --git a/app/models/problem.py b/app/models/problem.py index 23e0cb4..5ddd33a 100644 --- a/app/models/problem.py +++ b/app/models/problem.py @@ -3,10 +3,12 @@ from typing import TYPE_CHECKING from pydantic import BaseModel from typing import Any, List -from pulp import LpProblem, LpVariable, lpSum +from pulp import LpProblem, LpVariable, LpStatus, lpSum import logging +from helpers import service_helper, irt_helper + from models.solution import Solution from models.item import Item from models.bundle import Bundle @@ -38,8 +40,71 @@ class Problem(BaseModel): upBound=1, cat='Binary') - def solve(self) -> LpProblem: - self.problem.solve() + def solve(self, solver_run: SolverRun) -> LpProblem: + logging.info('solving problem...') + # if we allow enemies, go through the normal solving process + if solver_run.allow_enemies: + logging.info('enemes allowed, so just solving') + self.problem.solve() + # otherwise begin the process of filtering enemies + else: + self.problem.solve() + + # however, if the solve was infeasible, kick it back + # to the normal process + if LpStatus[self.problem.status] == 'Infeasible': + return self.problem + # otherwise continue + else: + # get items from solution + solved_items, _ = service_helper.solution_items(self.problem.variables(), solver_run) + + sacred_ids = [] + enemy_ids = [] + + # get all enemies + for item in solved_items: + # if it has enemies, check if it exists as part of the solved items + for enemy_id in item.enemies: + # if it does, it's a true enemy + if enemy_id in (item.id for item in solved_items): + enemy_ids.append(enemy_id) + # remove enemy from solved items, + # lest it has this sacred item added to enemies + solved_items = [i for i in solved_items if i.id != enemy_id] + + # the item is cleansed, now it's sacred + sacred_ids.append(item.id) + + if enemy_ids: + logging.info('enemies found, adding constraints...') + + # remove old enemy/sacred constraints + self.problem.constrants.pop('Exclude_enemy_items') + self.problem.constrants.pop('Include_sacred_items') + + # add constraint to not allow enemy items + self.problem += lpSum([ + len(bundle.find_items(enemy_ids)) * self.solver_bundles_var[bundle.id] + for bundle in self.bundles + ] + [ + (item.id in enemy_ids) * self.solver_items_var[item.id] + for item in self.items + ]) == 0, 'Exclude enemy items' + + # add constraint to use sacred items + self.problem += lpSum([ + len(bundle.find_items(sacred_ids)) * self.solver_bundles_var[bundle.id] + for bundle in self.bundles + ] + [ + (item.id in sacred_ids) * self.solver_items_var[item.id] + for item in self.items + ]) == len(sacred_ids), 'Include sacred items' + + # recursively solve until no enemies exist or infeasible + logging.info('recursively solving...') + self.solve(solver_run) + return self.problem def generate(self, solution: Solution, solver_run: SolverRun) -> None: @@ -63,5 +128,3 @@ class Problem(BaseModel): error.msg, error.args[0]) - - diff --git a/app/models/solver_run.py b/app/models/solver_run.py index 77df041..02653db 100644 --- a/app/models/solver_run.py +++ b/app/models/solver_run.py @@ -33,6 +33,7 @@ class SolverRun(BaseModel): total_forms: int = 1 theta_cut_score: float = 0.00 drift_style: Literal['constant', 'variable'] = 'constant' + allow_enemies: bool = False advanced_options: Optional[AdvancedOptions] engine: str diff --git a/app/services/form_generation_service.py b/app/services/form_generation_service.py index 082b10d..4b3a09a 100644 --- a/app/services/form_generation_service.py +++ b/app/services/form_generation_service.py @@ -1,6 +1,6 @@ import json, random, io, logging -from pulp import LpProblem, LpVariable, LpMinimize, LpStatus, lpSum +from pulp import LpProblem, LpMinimize, LpStatus from lib.application_configs import ApplicationConfigs from helpers import aws_helper, tar_helper, csv_helper, service_helper @@ -82,7 +82,7 @@ class FormGenerationService(Base): # create problem problem_handler = Problem(items = self.solver_run.unbundled_items(), bundles = self.solver_run.bundles, problem = LpProblem('ata-form-generate', LpMinimize)) problem_handler.generate(solution, self.solver_run) - problem = problem_handler.solve() + problem = problem_handler.solve(self.solver_run) if LpStatus[problem.status] == 'Infeasible': logging.info(