refactor in prep for enemies

This commit is contained in:
Joshua Burman 2023-11-09 16:12:26 -05:00
parent 49178380a4
commit 11979193de
7 changed files with 159 additions and 115 deletions

View File

@ -11,8 +11,9 @@ from models.item import Item
from lib.errors.item_generation_error import ItemGenerationError from lib.errors.item_generation_error import ItemGenerationError
# should probably be factored out into a bundle class method or a method in the solver run
def build_constraints(solver_run: SolverRun, problem: LpProblem, def build_constraints(solver_run: SolverRun, problem: LpProblem,
items: list[Item], bundles: list[Bundle], selected_items: list[Item], selected_bundles: list[Bundle]) -> LpProblem: items: list[Item], bundles: list[Bundle], selected_items: list[Item], selected_bundles: list[Bundle], current_drift: int) -> LpProblem:
logging.info('Creating Constraints...') logging.info('Creating Constraints...')
try: try:
@ -47,7 +48,7 @@ def build_constraints(solver_run: SolverRun, problem: LpProblem,
elif attribute.type == 'bundle': elif attribute.type == 'bundle':
logging.info('Bundles Constraint Generating...') logging.info('Bundles Constraint Generating...')
# TODO: account for many different bundle types, since the id condition in L33 could yield duplicates # TODO: account for many different bundle types, since the id condition in L33 could yield duplicates
if selected_bundles != None: if selected_bundles != None and selected_bundles > 0:
# make sure the total bundles used in generated form is limited between min-max set # make sure the total bundles used in generated form is limited between min-max set
problem += lpSum([ problem += lpSum([
bundles[bundle.id] for bundle in selected_bundles bundles[bundle.id] for bundle in selected_bundles
@ -55,6 +56,52 @@ def build_constraints(solver_run: SolverRun, problem: LpProblem,
int(constraint.maximum)) int(constraint.maximum))
logging.info('Constraints Created...') logging.info('Constraints Created...')
# Behold our very own Elastic constraints!
for tif_target in solver_run.objective_function.tif_targets:
problem += lpSum([
bundle.tif(solver_run.irt_model, tif_target.theta)
* bundles[bundle.id]
for bundle in selected_bundles
] + [
item.iif(solver_run, tif_target.theta) *
items[item.id]
for item in selected_items
]) >= tif_target.minimum(
), f'Min TIF theta({tif_target.theta}) at target {tif_target.value} drift at {current_drift}%'
problem += lpSum([
bundle.tif(solver_run.irt_model, tif_target.theta)
* bundles[bundle.id]
for bundle in selected_bundles
] + [
item.iif(solver_run, tif_target.theta) *
items[item.id]
for item in selected_items
]) <= tif_target.maximum(
), f'Max TIF theta({tif_target.theta}) at target {tif_target.value} drift at {current_drift}%'
for tcc_target in solver_run.objective_function.tcc_targets:
problem += lpSum([
bundle.trf(solver_run.irt_model, tcc_target.theta)
* bundles[bundle.id]
for bundle in selected_bundles
] + [
item.irf(solver_run, tcc_target.theta) *
items[item.id]
for item in selected_items
]) >= tcc_target.minimum(
), f'Min TCC theta({tcc_target.theta}) at target {tcc_target.value} drift at {current_drift}%'
problem += lpSum([
bundle.trf(solver_run.irt_model, tcc_target.theta)
* bundles[bundle.id]
for bundle in selected_bundles
] + [
item.irf(solver_run, tcc_target.theta) *
items[item.id]
for item in selected_items
]) <= tcc_target.maximum(
), f'Max TCC theta({tcc_target.theta}) at target {tcc_target.value} drift at {current_drift}%'
return problem return problem
except ValueError as error: except ValueError as error:
logging.error(error) logging.error(error)
@ -62,7 +109,7 @@ def build_constraints(solver_run: SolverRun, problem: LpProblem,
"Bundle min and/or max larger than bundle amount provided", "Bundle min and/or max larger than bundle amount provided",
error.args[0]) error.args[0])
# should probably be factored out into a bundle class method or a method in the solver run
def get_random_bundles(total_form_items: int, def get_random_bundles(total_form_items: int,
bundles: list[Bundle], bundles: list[Bundle],
min: int, min: int,

View File

@ -43,7 +43,7 @@ class ServiceListener(Consumer):
logging.error(f'action of type {action} does not exist.') logging.error(f'action of type {action} does not exist.')
def main(): def main():
logging.info('Starting IRT Service: That Was Rasch (v1.5.0)...') logging.info('Starting IRT Service: The Enemies Within (v1.7.0)...')
# ToDo: Figure out a much better way of doing this. # ToDo: Figure out a much better way of doing this.
# LocalStack wants 'endpoint_url', while prod doesnt :( # LocalStack wants 'endpoint_url', while prod doesnt :(

View File

@ -29,3 +29,10 @@ class Form(BaseModel):
tcc_results=irt_helper.generate_tcc_results(items, solver_run), tcc_results=irt_helper.generate_tcc_results(items, solver_run),
status=status, status=status,
solver_variables=solver_variables) solver_variables=solver_variables)
def has_item(self, item: Item) -> bool:
for i in self.items:
if item == i:
return True
return False

View File

@ -0,0 +1,3 @@
from models.problem import Problem
class IrtProblem(Problem):

79
app/models/problem.py Normal file
View File

@ -0,0 +1,79 @@
from pydantic import BaseModel
from typing import Any, List
from pulp import LpProblem, LpVariable, lpSum
import logging, math
from helpers import solver_helper
from models.solver_run import SolverRun
from models.solution import Solution
from models.item import Item
from models.bundle import Bundle
class Problem(BaseModel):
items: List[Item]
bundles: List[Bundle]
problem: Any
solver_items_var: Any = None
solver_bundles_var: Any = None
def __init__(self, **data) -> None:
super().__init__(**data)
# setup common Solver variables
self.solver_items_var = LpVariable.dicts("Item",
[item.id for item in self.items],
lowBound=0,
upBound=1,
cat='Binary')
self.solver_bundles_var = LpVariable.dicts("Bundle",
[bundle.id for bundle in self.bundles],
lowBound=0,
upBound=1,
cat='Binary')
# objective function
self.problem += lpSum([
bundle.count * self.solver_bundles_var[bundle.id]
for bundle in self.bundles
] + [
self.solver_items_var[item.id]
for item in self.items
])
def solve(self) -> LpProblem:
self.problem.solve()
return self.problem
def generate(self, solution: Solution, solver_run: SolverRun):
# Form Constraints
self.problem += lpSum(
[
bundle.count * self.solver_bundles_var[bundle.id]
for bundle in self.bundles
] + [
1 * self.solver_items_var[item.id]
for item in self.items
]
) == solver_run.total_form_items, f'Total bundle form items for form'
# each time a form is generated, we want to ensure
# that it is unique to all other forms generated before it
self.problem += lpSum(
[
solution.items_exist_in_forms(bundle.items) * self.solver_bundles_var[bundle.id]
for bundle in self.bundles
] + [
solution.items_exist_in_forms([item]) * self.solver_items_var[item.id]
for item in self.items
]
) <= solver_run.total_form_items - 1, f'Ensuring uniqueness for form'
def generate_constraints(self, solver_run: SolverRun, current_drift: int):
self.problem = solver_helper.build_constraints(
solver_run, self.problem, self.solver_items_var, self.solver_bundles_var, self.items, self.bundles, current_drift)

View File

@ -2,8 +2,21 @@ from pydantic import BaseModel
from typing import List from typing import List
from models.form import Form from models.form import Form
from models.item import Item
class Solution(BaseModel): class Solution(BaseModel):
response_id: int response_id: int
forms: List[Form] forms: List[Form]
def items_exist_in_forms(self, items: [Item]) -> bool:
items_found = 0
for item in items:
for form in self.forms:
if form.has_item(item):
items_found += 1
return items_found

View File

@ -8,6 +8,7 @@ from lib.errors.item_generation_error import ItemGenerationError
from models.solver_run import SolverRun from models.solver_run import SolverRun
from models.solution import Solution from models.solution import Solution
from models.problem import Problem
from models.form import Form from models.form import Form
from models.target import Target from models.target import Target
@ -71,24 +72,6 @@ class FormGenerationService(Base):
form_number = form_count + 1 form_number = form_count + 1
current_drift = 0 # FF Tokyo Drift current_drift = 0 # FF Tokyo Drift
# adding an element of randomness to the items and bundles used
# may need to change impl based on limit of items available
selected_items = self.solver_run.select_items_by_percent(30)
selected_bundles = self.solver_run.select_bundles_by_percent(
30)
# setup common Solver variables
items = LpVariable.dicts("Item",
[item.id for item in selected_items],
lowBound=0,
upBound=1,
cat='Binary')
bundles = LpVariable.dicts("Bundle",
[bundle.id for bundle in selected_bundles],
lowBound=0,
upBound=1,
cat='Binary')
logging.info(f'Generating Solution for Form {form_number} using the {self.solver_run.irt_model.model} IRT model') logging.info(f'Generating Solution for Form {form_number} using the {self.solver_run.irt_model.model} IRT model')
while current_drift <= Target.max_drift(): while current_drift <= Target.max_drift():
@ -97,99 +80,11 @@ class FormGenerationService(Base):
drift_percent) drift_percent)
# create problem # create problem
problem = LpProblem('ata-form-generate', LpMinimize) problem_handler = Problem(items = self.solver_run.items, bundles = self.solver_run.bundles, problem = LpProblem('ata-form-generate', LpMinimize))
problem_handler.generate(solution, self.solver_run)
problem_handler.generate_constraints(self.solver_run, current_drift)
# objective function problem = problem_handler.solve()
problem += lpSum([
bundle.count * bundles[bundle.id]
for bundle in selected_bundles
] + [
items[item.id]
for item in selected_items
])
# Form Constraints
problem += lpSum(
[
bundle.count * bundles[bundle.id]
for bundle in selected_bundles
] + [
1 * items[item.id]
for item in selected_items
]
) == self.solver_run.total_form_items, f'Total bundle form items for form {form_number}'
# Dynamic constraints.. currently we only support Metadata and Bundles(Cases/Passages)
problem = solver_helper.build_constraints(
self.solver_run, problem, items, bundles, selected_items, selected_bundles)
# form uniqueness constraints
# for form in solution.forms:
# form_item_options = [
# bundles[bundle.id]
# for bundle in selected_bundles
# ] + [
# items[item.id]
# for item in selected_items
# ]
# problem += len(
# set(form.solver_variables)
# & set(form_item_options)) / float(
# len(
# set(form.solver_variables)
# | set(form_item_options))) * 100 >= 10
logging.info('Creating TIF and TCC Elastic constraints')
# Behold our very own Elastic constraints!
for tif_target in self.solver_run.objective_function.tif_targets:
problem += lpSum([
bundle.tif(self.solver_run.irt_model, tif_target.theta)
* bundles[bundle.id]
for bundle in selected_bundles
] + [
item.iif(self.solver_run, tif_target.theta) *
items[item.id]
for item in selected_items
]) >= tif_target.minimum(
), f'Min TIF theta({tif_target.theta}) at target {tif_target.value} drift at {current_drift}%'
problem += lpSum([
bundle.tif(self.solver_run.irt_model, tif_target.theta)
* bundles[bundle.id]
for bundle in selected_bundles
] + [
item.iif(self.solver_run, tif_target.theta) *
items[item.id]
for item in selected_items
]) <= tif_target.maximum(
), f'Max TIF theta({tif_target.theta}) at target {tif_target.value} drift at {current_drift}%'
for tcc_target in self.solver_run.objective_function.tcc_targets:
problem += lpSum([
bundle.trf(self.solver_run.irt_model, tcc_target.theta)
* bundles[bundle.id]
for bundle in selected_bundles
] + [
item.irf(self.solver_run, tcc_target.theta) *
items[item.id]
for item in selected_items
]) >= tcc_target.minimum(
), f'Min TCC theta({tcc_target.theta}) at target {tcc_target.value} drift at {current_drift}%'
problem += lpSum([
bundle.trf(self.solver_run.irt_model, tcc_target.theta)
* bundles[bundle.id]
for bundle in selected_bundles
] + [
item.irf(self.solver_run, tcc_target.theta) *
items[item.id]
for item in selected_items
]) <= tcc_target.maximum(
), f'Max TCC theta({tcc_target.theta}) at target {tcc_target.value} drift at {current_drift}%'
logging.info(
f'Solving for Form {form_number} with a drift of {current_drift}%'
)
problem.solve()
if LpStatus[problem.status] == 'Infeasible': if LpStatus[problem.status] == 'Infeasible':
logging.info( logging.info(