From d02137112ab0824b425a02cc8a0a0f5a29b40ac1 Mon Sep 17 00:00:00 2001 From: Jared Numrab <jarednumrab@Jareds-MacBook-Pro.local> Date: Thu, 18 Nov 2021 22:46:29 -0500 Subject: [PATCH] actual solving, minus constraints for non tcc/tif constraints --- app/lib/irt/test_information_function.py | 3 +- app/lib/irt/test_response_function.py | 3 +- app/main.py | 2 +- app/models/advanced_options.py | 4 +- app/models/item.py | 13 +++- app/models/solver_run.py | 1 + app/services/loft_service.py | 76 ++++++++++++++++++++---- 7 files changed, 82 insertions(+), 20 deletions(-) diff --git a/app/lib/irt/test_information_function.py b/app/lib/irt/test_information_function.py index aef771d..00fcac3 100644 --- a/app/lib/irt/test_information_function.py +++ b/app/lib/irt/test_information_function.py @@ -10,7 +10,6 @@ class TestInformationFunction(): for item in items: result = self.iif.calculate(b_param=item.b_param, theta=kwargs['theta']) - item.iif = result - sum += item.iif + sum += result return sum diff --git a/app/lib/irt/test_response_function.py b/app/lib/irt/test_response_function.py index 81df8fd..f40bca9 100644 --- a/app/lib/irt/test_response_function.py +++ b/app/lib/irt/test_response_function.py @@ -11,7 +11,6 @@ class TestResponseFunction(): for item in items: result = self.irf.calculate(b_param=item.b_param, theta=kwargs['theta']) - item.irf = result - sum += item.irf + sum += result return sum diff --git a/app/main.py b/app/main.py index 748323b..ef2cd1f 100644 --- a/app/main.py +++ b/app/main.py @@ -19,7 +19,7 @@ class ServiceListener(SqsListener): logging.info('Process complete for %s', service.file_name) def main(): - logging.info('Starting Solver Service (v0.4.3)...') + logging.info('Starting Solver Service (v0.6.0)...') listener = ServiceListener( 'measure-development-solver-ingest', region_name=os.environ['AWS_REGION'], diff --git a/app/models/advanced_options.py b/app/models/advanced_options.py index 1a9fb34..1d7d420 100644 --- a/app/models/advanced_options.py +++ b/app/models/advanced_options.py @@ -2,8 +2,8 @@ from pydantic import BaseModel from typing import List, Optional, Dict class AdvancedOptions(BaseModel): - linearity_check: bool - show_progress: bool + linearity_check: Optional[bool] + show_progress: Optional[bool] max_solution_time: Optional[int] brand_bound_tolerance: Optional[float] max_forms: Optional[int] diff --git a/app/models/item.py b/app/models/item.py index d94c5cd..86beb61 100644 --- a/app/models/item.py +++ b/app/models/item.py @@ -3,9 +3,16 @@ from typing import List from models.attribute import Attribute +from lib.irt.item_response_function import ItemResponseFunction +from lib.irt.item_information_function import ItemInformationFunction + class Item(BaseModel): id: int attributes: List[Attribute] - b_param: int - irf: float = 0.00 - iif: float = 0.00 + b_param: float = 0.00 + + def iif(self, solver_run, theta): + return ItemInformationFunction(solver_run.irt_model).calculate(b_param=self.b_param,theta=theta) + + def irf(self, solver_run, theta): + return ItemResponseFunction(solver_run.irt_model).calculate(b_param=self.b_param,theta=theta) diff --git a/app/models/solver_run.py b/app/models/solver_run.py index 1c0727b..22f49c7 100644 --- a/app/models/solver_run.py +++ b/app/models/solver_run.py @@ -13,6 +13,7 @@ class SolverRun(BaseModel): irt_model: IRTModel objective_function: ObjectiveFunction total_form_items: int + total_forms: int theta_cut_score: float = 0.00 advanced_options: Optional[AdvancedOptions] engine: str diff --git a/app/services/loft_service.py b/app/services/loft_service.py index 9ff0711..74edc20 100644 --- a/app/services/loft_service.py +++ b/app/services/loft_service.py @@ -1,5 +1,7 @@ import os, json, random, io, logging +from pulp import * + from helpers import aws_helper, tar_helper, csv_helper, service_helper, irt_helper from models.solver_run import SolverRun @@ -37,20 +39,74 @@ class LoftService(Base): return attributes def generate_solution(self): - logging.info('Processing Solution...') - # temporary data for mocks - form_count = 10 - - # items will be generated from real solver process, this is for mock purposes - # real solver will return N forms and process a cut score, this is for mock purposes - return Solution( - response_id=random.randint(100,5000), - forms=[Form.create(random.sample(self.solver_run.items, self.solver_run.total_form_items), self.solver_run) for x in range(form_count)] + # unsolved solution + solution = Solution( + response_id=random.randint(100, 5000), + forms=[] ) + # counter for number of forms + f = 0 + + # iterate for number of forms that require creation + # currently creates distinc forms with no item overlap + while f < self.solver_run.total_forms: + # setup vars + items = LpVariable.dicts( + "Item", [item.id for item in self.solver_run.items], lowBound=1, upBound=1, cat='Binary') + used = dict(zip([item.id for item in self.solver_run.items], + [1 for item in self.solver_run.items])) + problem_objection_functions = [] + + # create problem + problem = LpProblem("ata-form-generate", LpMinimize) + + # constraints + problem += lpSum([used[i]*items[i] + for i in items]) == self.solver_run.total_form_items + + # multi-objective functions and constraints + for target in self.solver_run.objective_function.tif_targets: + tif = lpSum([item.iif(self.solver_run, target.theta)*items[item.id] + for item in self.solver_run.items]) + problem += lpSum([item.iif(self.solver_run, target.theta)*items[item.id] + for item in self.solver_run.items]) <= target.value + problem_objection_functions.append(tif) + + for target in self.solver_run.objective_function.tcc_targets: + tcc = lpSum([item.irf(self.solver_run, target.theta)*items[item.id] + for item in self.solver_run.items]) + problem += lpSum([item.irf(self.solver_run, target.theta)*items[item.id] + for item in self.solver_run.items]) <= target.value + problem_objection_functions.append(tcc) + + # solve problem + problem.sequentialSolve(problem_objection_functions) + + # add return items and create as a form + form_items = [] + for v in problem.variables(): + count = 0 + if v.varValue > 0: + item_id = v.name.replace('Item_', '') + for item in self.solver_run.items: + if str(item.id) == item_id: + # add item to list + form_items.append(item) + # remove ids from master items list + self.solver_run.items.remove(item) + + # add form to solution + solution.forms.append(Form.create(form_items, self.solver_run)) + + # successfull form, increment + f += 1 + + return solution + def stream_to_s3_bucket(self): self.file_name = f'{service_helper.key_to_uuid(self.key)}.csv' - logging.info('Streaming to %s s3 bucket %s', self.file_name, os.environ['S3_PROCESSED_BUCKET']) + logging.info('Streaming %s to s3 bucket - %s', self.file_name, os.environ['S3_PROCESSED_BUCKET']) # setup writer buffer and write processed forms to file buffer = io.StringIO() solution_file = service_helper.solution_to_file(buffer, self.solver_run.total_form_items, self.solution.forms)