From dd3188b798b94bad673886d0565f27b264abc1ae Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Fri, 29 Oct 2021 05:43:32 +0000 Subject: [PATCH 01/23] construct based irt library and add item response function --- app/lib/irt/item_information_function.py | 0 app/lib/irt/item_response_function.py | 12 ++++++++++++ app/lib/irt/models/three_parameter_logistic.py | 13 +++++++++++++ app/lib/irt/test_information_function.py | 0 app/lib/irt/test_response_function.py | 0 5 files changed, 25 insertions(+) create mode 100644 app/lib/irt/item_information_function.py create mode 100644 app/lib/irt/item_response_function.py create mode 100644 app/lib/irt/models/three_parameter_logistic.py create mode 100644 app/lib/irt/test_information_function.py create mode 100644 app/lib/irt/test_response_function.py diff --git a/app/lib/irt/item_information_function.py b/app/lib/irt/item_information_function.py new file mode 100644 index 0000000..e69de29 diff --git a/app/lib/irt/item_response_function.py b/app/lib/irt/item_response_function.py new file mode 100644 index 0000000..a844f08 --- /dev/null +++ b/app/lib/irt/item_response_function.py @@ -0,0 +1,12 @@ +from models.three_parameter_logitistc import ThreeParameterLogistic + +class ItemResponseFunction(): + def __init__(self, irt_model): + self.model_data = irt_model + + def calculate(self, **kwargs): + if self.model_data.model == '3PL': + return ThreeParameterLogistic.new(self.model_data, kwargs).result + else: + # potentially error out + return None diff --git a/app/lib/irt/models/three_parameter_logistic.py b/app/lib/irt/models/three_parameter_logistic.py new file mode 100644 index 0000000..a5e2986 --- /dev/null +++ b/app/lib/irt/models/three_parameter_logistic.py @@ -0,0 +1,13 @@ +class ThreeParameterLogistic: + def __init__(self, model_params, kwargs): + self.model_params = model_params + # check if exists, if not error out + self.b_param = kwargs['b_param'] + self.e = 2.71828 + self.theta = kwargs['theta'] + + def result(self): + a = self.model_params.a + c = self.model_params.c + + return c + (1 - c) * (1 / (1 + e**(-a * (self.theta - self.b_param)))) diff --git a/app/lib/irt/test_information_function.py b/app/lib/irt/test_information_function.py new file mode 100644 index 0000000..e69de29 diff --git a/app/lib/irt/test_response_function.py b/app/lib/irt/test_response_function.py new file mode 100644 index 0000000..e69de29 From 6b1bbda169ea7fddef5a1b6148aaaa9ce9a00441 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Fri, 29 Oct 2021 05:57:11 +0000 Subject: [PATCH 02/23] add tcc --- app/lib/irt/test_response_function.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/lib/irt/test_response_function.py b/app/lib/irt/test_response_function.py index e69de29..d0a83fa 100644 --- a/app/lib/irt/test_response_function.py +++ b/app/lib/irt/test_response_function.py @@ -0,0 +1,12 @@ +# otherwise known as the Test Characteristic Curve (TCC) +class TestResponseFunction(): + def __init__(self, irf): + self.irf = irf + + def calculate(self, items, **kwargs): + result = 0 + + for item in items: + result += irf.calculate(b_param=item.b_param, theta=kwargs['theta']) + + return result From 258915b08f6d24b350b18a5ef643ca368aedacb7 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Fri, 29 Oct 2021 18:43:56 +0000 Subject: [PATCH 03/23] working TCC (Test response funciton) --- app/helpers/service_helper.py | 2 +- app/lib/irt/item_response_function.py | 4 ++-- app/lib/irt/models/three_parameter_logistic.py | 7 +++---- app/lib/irt/test_response_function.py | 15 ++++++++++----- app/models/form.py | 2 +- app/models/item.py | 1 + app/services/loft_service.py | 13 +++++++------ 7 files changed, 25 insertions(+), 19 deletions(-) diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index 07b6900..188a6d1 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -40,7 +40,7 @@ def solution_to_file(buffer, total_form_items, forms): # add each form as row to processed csv for form in forms: # provide generated items and cut score - row = form.items + [form.cut_score] + row = [item.id for item in form.items] + [form.cut_score] wr.writerow(row) buff2 = io.BytesIO(buffer.getvalue().encode()) diff --git a/app/lib/irt/item_response_function.py b/app/lib/irt/item_response_function.py index a844f08..df66a0e 100644 --- a/app/lib/irt/item_response_function.py +++ b/app/lib/irt/item_response_function.py @@ -1,4 +1,4 @@ -from models.three_parameter_logitistc import ThreeParameterLogistic +from lib.irt.models.three_parameter_logistic import ThreeParameterLogistic class ItemResponseFunction(): def __init__(self, irt_model): @@ -6,7 +6,7 @@ class ItemResponseFunction(): def calculate(self, **kwargs): if self.model_data.model == '3PL': - return ThreeParameterLogistic.new(self.model_data, kwargs).result + return ThreeParameterLogistic(self.model_data, kwargs).result() else: # potentially error out return None diff --git a/app/lib/irt/models/three_parameter_logistic.py b/app/lib/irt/models/three_parameter_logistic.py index a5e2986..d440c86 100644 --- a/app/lib/irt/models/three_parameter_logistic.py +++ b/app/lib/irt/models/three_parameter_logistic.py @@ -7,7 +7,6 @@ class ThreeParameterLogistic: self.theta = kwargs['theta'] def result(self): - a = self.model_params.a - c = self.model_params.c - - return c + (1 - c) * (1 / (1 + e**(-a * (self.theta - self.b_param)))) + a = self.model_params.a_param + c = self.model_params.c_param + return c + (1 - c) * (1 / (1 + self.e**(-a * (self.theta - self.b_param)))) diff --git a/app/lib/irt/test_response_function.py b/app/lib/irt/test_response_function.py index d0a83fa..81df8fd 100644 --- a/app/lib/irt/test_response_function.py +++ b/app/lib/irt/test_response_function.py @@ -1,12 +1,17 @@ +from lib.irt.item_response_function import ItemResponseFunction + # otherwise known as the Test Characteristic Curve (TCC) class TestResponseFunction(): - def __init__(self, irf): - self.irf = irf + def __init__(self, irt_model): + self.irt_model = irt_model + self.irf = ItemResponseFunction(irt_model) def calculate(self, items, **kwargs): - result = 0 + sum = 0 for item in items: - result += irf.calculate(b_param=item.b_param, theta=kwargs['theta']) + result = self.irf.calculate(b_param=item.b_param, theta=kwargs['theta']) + item.irf = result + sum += item.irf - return result + return sum diff --git a/app/models/form.py b/app/models/form.py index ea7844f..57b4e50 100644 --- a/app/models/form.py +++ b/app/models/form.py @@ -4,5 +4,5 @@ from typing import List from models.item import Item class Form(BaseModel): - items: List[int] + items: List[Item] cut_score: float diff --git a/app/models/item.py b/app/models/item.py index 8ae3b70..eca5421 100644 --- a/app/models/item.py +++ b/app/models/item.py @@ -7,3 +7,4 @@ class Item(BaseModel): id: int attributes: List[Attribute] b_param: int + irf: float = 0.00 diff --git a/app/services/loft_service.py b/app/services/loft_service.py index d571a61..f67f0f3 100644 --- a/app/services/loft_service.py +++ b/app/services/loft_service.py @@ -45,12 +45,7 @@ class LoftService(Base): # 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( - items=[item.id for item in random.sample(self.solver_run.items, self.solver_run.total_form_items)], - cut_score=120 - ) for x in range(form_count) - ] + forms=[self.generate_forms(random.sample(self.solver_run.items, self.solver_run.total_form_items)) for x in range(form_count)] ) def stream_to_s3_bucket(self): @@ -60,3 +55,9 @@ class LoftService(Base): # upload generated file to s3 and return result return aws_helper.file_stream_upload(solution_file, f'{service_helper.key_to_uuid(self.key)}.csv', os.environ['MEASURE_PROCESSED_BUCKET']) + + def generate_forms(self, items): + return Form( + items=items, + cut_score=TestResponseFunction(self.solver_run.irt_model).calculate(items, theta=self.solver_run.theta_cut_score) + ) From 3b83aad6bdbe9fd36e26e2e89f125857d4a90456 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Fri, 29 Oct 2021 19:19:12 +0000 Subject: [PATCH 04/23] added iif and tif functions --- app/lib/irt/item_information_function.py | 14 ++++++++++++++ app/lib/irt/test_information_function.py | 16 ++++++++++++++++ app/main.py | 2 +- app/models/item.py | 1 + app/services/loft_service.py | 3 +++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/lib/irt/item_information_function.py b/app/lib/irt/item_information_function.py index e69de29..f38512e 100644 --- a/app/lib/irt/item_information_function.py +++ b/app/lib/irt/item_information_function.py @@ -0,0 +1,14 @@ +from lib.irt.models.three_parameter_logistic import ThreeParameterLogistic + +class ItemInformationFunction(): + def __init__(self, irt_model): + self.model_data = irt_model + + def calculate(self, **kwargs): + if self.model_data.model == '3PL': + p = ThreeParameterLogistic(self.model_data, kwargs).result() + q = 1 - p + return self.model_data.a_param**2 * ((q / p) * ((p - (self.model_data.c_param**2)) / (1 - (self.model_data.c_param**2)))) + else: + # potentially error out + return None diff --git a/app/lib/irt/test_information_function.py b/app/lib/irt/test_information_function.py index e69de29..aef771d 100644 --- a/app/lib/irt/test_information_function.py +++ b/app/lib/irt/test_information_function.py @@ -0,0 +1,16 @@ +from lib.irt.item_information_function import ItemInformationFunction + +class TestInformationFunction(): + def __init__(self, irt_model): + self.irt_model = irt_model + self.iif = ItemInformationFunction(irt_model) + + def calculate(self, items, **kwargs): + sum = 0 + + for item in items: + result = self.iif.calculate(b_param=item.b_param, theta=kwargs['theta']) + item.iif = result + sum += item.iif + + return sum diff --git a/app/main.py b/app/main.py index 1308bda..b05b631 100644 --- a/app/main.py +++ b/app/main.py @@ -8,7 +8,7 @@ from sqs_listener.daemon import Daemon print("Starting Solver Service (v0.3.1)...") -# # listen to the solver queue +# listen to the solver queue while True: msg = aws_helper.receive_message(os.environ['SOLVER_SQS_INGEST_QUEUE']) diff --git a/app/models/item.py b/app/models/item.py index eca5421..d94c5cd 100644 --- a/app/models/item.py +++ b/app/models/item.py @@ -8,3 +8,4 @@ class Item(BaseModel): attributes: List[Attribute] b_param: int irf: float = 0.00 + iif: float = 0.00 diff --git a/app/services/loft_service.py b/app/services/loft_service.py index f67f0f3..d189958 100644 --- a/app/services/loft_service.py +++ b/app/services/loft_service.py @@ -9,6 +9,9 @@ from models.solver_run import SolverRun from models.solution import Solution from models.form import Form +from lib.irt.test_response_function import TestResponseFunction +from lib.irt.test_information_function import TestInformationFunction + from services.base import Base class LoftService(Base): From 5bd3eb7b908bb53a317349289fecf661a137cac1 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Sat, 30 Oct 2021 07:15:49 +0000 Subject: [PATCH 05/23] added tcc and tif results to form --- app/helpers/irt_helper.py | 22 ++++++++++++++++++++++ app/models/form.py | 16 ++++++++++++++++ app/models/target.py | 2 ++ app/services/loft_service.py | 13 ++----------- 4 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 app/helpers/irt_helper.py diff --git a/app/helpers/irt_helper.py b/app/helpers/irt_helper.py new file mode 100644 index 0000000..401e875 --- /dev/null +++ b/app/helpers/irt_helper.py @@ -0,0 +1,22 @@ +from lib.irt.test_response_function import TestResponseFunction +from lib.irt.test_information_function import TestInformationFunction + +from models.target import Target + +def generate_tif_results(items, solver_run): + targets = [] + + for target in solver_run.objective_function.tif_targets: + tif = TestInformationFunction(solver_run.irt_model).calculate(items, theta=target.theta) + targets.append(Target(theta=target.theta, value=target.value, result=tif)) + + return targets + +def generate_tcc_results(items, solver_run): + targets = [] + + for target in solver_run.objective_function.tcc_targets: + tcc = TestResponseFunction(solver_run.irt_model).calculate(items, theta=target.theta) + targets.append(Target(theta=target.theta, value=target.value, result=tcc)) + + return targets diff --git a/app/models/form.py b/app/models/form.py index 57b4e50..cae4073 100644 --- a/app/models/form.py +++ b/app/models/form.py @@ -1,8 +1,24 @@ from pydantic import BaseModel from typing import List +from helpers import irt_helper + from models.item import Item +from models.target import Target + +from lib.irt.test_response_function import TestResponseFunction class Form(BaseModel): items: List[Item] cut_score: float + tif_results: List[Target] + tcc_results: List[Target] + + @classmethod + def create(cls, items, solver_run): + return cls( + items=items, + cut_score=TestResponseFunction(solver_run.irt_model).calculate(items, theta=solver_run.theta_cut_score), + tif_results=irt_helper.generate_tif_results(items, solver_run), + tcc_results=irt_helper.generate_tcc_results(items, solver_run) + ) diff --git a/app/models/target.py b/app/models/target.py index 1383208..3999d83 100644 --- a/app/models/target.py +++ b/app/models/target.py @@ -1,5 +1,7 @@ from pydantic import BaseModel +from typing import Optional class Target(BaseModel): theta: float value: float + result: Optional[float] diff --git a/app/services/loft_service.py b/app/services/loft_service.py index d189958..eee5d6d 100644 --- a/app/services/loft_service.py +++ b/app/services/loft_service.py @@ -3,15 +3,12 @@ import json import random import io -from helpers import aws_helper, tar_helper, csv_helper, service_helper +from helpers import aws_helper, tar_helper, csv_helper, service_helper, irt_helper from models.solver_run import SolverRun from models.solution import Solution from models.form import Form -from lib.irt.test_response_function import TestResponseFunction -from lib.irt.test_information_function import TestInformationFunction - from services.base import Base class LoftService(Base): @@ -48,7 +45,7 @@ class LoftService(Base): # 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=[self.generate_forms(random.sample(self.solver_run.items, self.solver_run.total_form_items)) for x in range(form_count)] + forms=[Form.create(random.sample(self.solver_run.items, self.solver_run.total_form_items), self.solver_run) for x in range(form_count)] ) def stream_to_s3_bucket(self): @@ -58,9 +55,3 @@ class LoftService(Base): # upload generated file to s3 and return result return aws_helper.file_stream_upload(solution_file, f'{service_helper.key_to_uuid(self.key)}.csv', os.environ['MEASURE_PROCESSED_BUCKET']) - - def generate_forms(self, items): - return Form( - items=items, - cut_score=TestResponseFunction(self.solver_run.irt_model).calculate(items, theta=self.solver_run.theta_cut_score) - ) From d02137112ab0824b425a02cc8a0a0f5a29b40ac1 Mon Sep 17 00:00:00 2001 From: Jared Numrab Date: Thu, 18 Nov 2021 22:46:29 -0500 Subject: [PATCH 06/23] 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) From 63e501630761a7678484adcb64fe6eaf6c4f4286 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Fri, 19 Nov 2021 07:11:35 +0000 Subject: [PATCH 07/23] better csv created with additions of tccs, tifs, rounding, and reordering --- app/helpers/service_helper.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index 188a6d1..c004175 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -33,14 +33,29 @@ def solution_to_file(buffer, total_form_items, forms): wr = csv.writer(buffer, dialect='excel', delimiter=',') # write header row for first row utilizing the total items all forms will have - # and the cut score as the last item - header = [x + 1 for x in range(total_form_items)] + ['cut score'] + # fill the rows with the targets and cut score then the items + header = [] + for result in forms[0].tif_results: + header += [f'tif @ {round(result.theta, 2)}'] + + for result in forms[0].tcc_results: + header += [f'tcc @ {round(result.theta, 2)}'] + + header += ['cut score'] + [x + 1 for x in range(total_form_items)] wr.writerow(header) # add each form as row to processed csv for form in forms: + row = [] + + for result in form.tif_results: + row += [f'value - {result.value}\nresult - {round(result.result, 2)}'] + + for result in form.tcc_results: + row += [f'value - {result.value}\nresult - {round(result.result, 2)}'] + # provide generated items and cut score - row = [item.id for item in form.items] + [form.cut_score] + row += [round(form.cut_score, 2)] + [item.id for item in form.items] wr.writerow(row) buff2 = io.BytesIO(buffer.getvalue().encode()) From 1d9b2c694cb21b834d86cc1e92332d2dd0cdb256 Mon Sep 17 00:00:00 2001 From: Jared Numrab Date: Sun, 21 Nov 2021 21:52:18 -0500 Subject: [PATCH 08/23] add remaining constraints --- app/models/constraint.py | 4 ++-- app/models/item.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/models/constraint.py b/app/models/constraint.py index 84eb1b2..2c84a60 100644 --- a/app/models/constraint.py +++ b/app/models/constraint.py @@ -5,5 +5,5 @@ from models.attribute import Attribute class Constraint(BaseModel): reference_attribute: Attribute - minimum: int - maximum: int + minimum: float + maximum: float diff --git a/app/models/item.py b/app/models/item.py index 86beb61..71bffcf 100644 --- a/app/models/item.py +++ b/app/models/item.py @@ -16,3 +16,15 @@ class Item(BaseModel): def irf(self, solver_run, theta): return ItemResponseFunction(solver_run.irt_model).calculate(b_param=self.b_param,theta=theta) + + def get_attribute(self, id, value): + for attribute in self.attributes: + if attribute.id == id and attribute.value == value: + return attribute.value + return False + + def attribute_exists(self, id, value): + for attribute in self.attributes: + if attribute.id == id and attribute.value == value: + return True + return False \ No newline at end of file From e37fc958f383869bd53a68a9b02b1147b94e4938 Mon Sep 17 00:00:00 2001 From: Jared Numrab Date: Sun, 21 Nov 2021 22:40:44 -0500 Subject: [PATCH 09/23] fix trailing whitespace --- app/services/loft_service.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/app/services/loft_service.py b/app/services/loft_service.py index 74edc20..0e0db85 100644 --- a/app/services/loft_service.py +++ b/app/services/loft_service.py @@ -62,24 +62,35 @@ class LoftService(Base): problem = LpProblem("ata-form-generate", LpMinimize) # constraints - problem += lpSum([used[i]*items[i] - for i in items]) == self.solver_run.total_form_items - + problem += lpSum([items[item.id] + for item in self.solver_run.items]) == self.solver_run.total_form_items, 'Total form items' + + for constraint in self.solver_run.constraints: + con = dict(zip([item.id for item in self.solver_run.items], + [item.attribute_exists(constraint.reference_attribute.id, constraint.reference_attribute.value) + for item in self.solver_run.items])) + problem += lpSum([con[item.id] + * items[item.id] + for item in self.solver_run.items]) >= round(self.solver_run.total_form_items * (constraint.minimum / 100)), f'{constraint.reference_attribute.id} - {constraint.reference_attribute.value} - min' + problem += lpSum([con[item.id] + * items[item.id] + for item in self.solver_run.items]) <= round(self.solver_run.total_form_items * (constraint.maximum / 100)), f'{constraint.reference_attribute.id} - {constraint.reference_attribute.value} - max' + # 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 + for item in self.solver_run.items]) <= target.value, f'min tif theta ({target.theta}) target value {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 + for item in self.solver_run.items]) <= target.value, f'min tcc theta ({target.theta}) target value {target.value}' problem_objection_functions.append(tcc) - + # solve problem problem.sequentialSolve(problem_objection_functions) From f1a6b5326570b38877e04152c3b73ed3f293f8cc Mon Sep 17 00:00:00 2001 From: Jared Numrab Date: Sun, 21 Nov 2021 22:43:14 -0500 Subject: [PATCH 10/23] handel non optimized solves --- app/services/loft_service.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/app/services/loft_service.py b/app/services/loft_service.py index 0e0db85..364a68e 100644 --- a/app/services/loft_service.py +++ b/app/services/loft_service.py @@ -94,21 +94,22 @@ class LoftService(Base): # 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) + if LpStatus[problem.status] == 'Optimized': + # 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)) + # add form to solution + solution.forms.append(Form.create(form_items, self.solver_run)) # successfull form, increment f += 1 From c2d1ccb2bd41a4b76f19eeefc44348927ebc1af7 Mon Sep 17 00:00:00 2001 From: Jared Numrab Date: Mon, 22 Nov 2021 04:10:53 -0500 Subject: [PATCH 11/23] more better well factoredness --- app/helpers/service_helper.py | 12 ++++++++++++ app/helpers/solver_helper.py | 23 +++++++++++++++++++++++ app/models/item.py | 8 ++++---- app/models/solver_run.py | 10 ++++++++++ app/services/loft_service.py | 31 ++++++------------------------- 5 files changed, 55 insertions(+), 29 deletions(-) create mode 100644 app/helpers/solver_helper.py diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index c004175..4f7f23c 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -64,3 +64,15 @@ def solution_to_file(buffer, total_form_items, forms): def key_to_uuid(key): return re.split("_", key)[0] + +def solution_items(variables, solver_run): + form_items = [] + + for v in variables: + if v.varValue > 0: + item_id = v.name.replace('Item_', '') + item = solver_run.get_item(item_id) + # add item to list and then remove from master item list + form_items.append(item) + + return form_items diff --git a/app/helpers/solver_helper.py b/app/helpers/solver_helper.py new file mode 100644 index 0000000..b9f7fb0 --- /dev/null +++ b/app/helpers/solver_helper.py @@ -0,0 +1,23 @@ +from pulp import lpSum + +def build_constraints(solver_run, problem, items): + total_form_items = solver_run.total_form_items + constraints = solver_run.constraints + + for constraint in constraints: + attribute = constraint.reference_attribute + min = constraint.minimum + max = constraint.maximum + + con = dict(zip([item.id for item in solver_run.items], + [item.attribute_exists(attribute) + for item in solver_run.items])) + # print([con[item.id] * items[item.id] for item in solver_items]) + problem += lpSum([con[item.id] + * items[item.id] + for item in solver_run.items]) >= round(total_form_items * (min / 100)), f'{attribute.id} - {attribute.value} - min' + problem += lpSum([con[item.id] + * items[item.id] + for item in solver_run.items]) <= round(total_form_items * (max / 100)), f'{attribute.id} - {attribute.value} - max' + + return problem \ No newline at end of file diff --git a/app/models/item.py b/app/models/item.py index 71bffcf..9cb1d8d 100644 --- a/app/models/item.py +++ b/app/models/item.py @@ -17,14 +17,14 @@ class Item(BaseModel): def irf(self, solver_run, theta): return ItemResponseFunction(solver_run.irt_model).calculate(b_param=self.b_param,theta=theta) - def get_attribute(self, id, value): + def get_attribute(self, ref_attribute): for attribute in self.attributes: - if attribute.id == id and attribute.value == value: + if attribute.id == ref_attribute.id and attribute.value == ref_attribute.value: return attribute.value return False - def attribute_exists(self, id, value): + def attribute_exists(self, ref_attribute): for attribute in self.attributes: - if attribute.id == id and attribute.value == value: + if attribute.id == ref_attribute.id and attribute.value == ref_attribute.value: return True return False \ No newline at end of file diff --git a/app/models/solver_run.py b/app/models/solver_run.py index 22f49c7..1a97c8c 100644 --- a/app/models/solver_run.py +++ b/app/models/solver_run.py @@ -17,3 +17,13 @@ class SolverRun(BaseModel): theta_cut_score: float = 0.00 advanced_options: Optional[AdvancedOptions] engine: str + + def get_item(self, item_id): + for item in self.items: + if str(item.id) == item_id: + return item + return False + + def remove_items(self, items): + self.items = [item for item in self.items if item not in items] + return True diff --git a/app/services/loft_service.py b/app/services/loft_service.py index 364a68e..dea817a 100644 --- a/app/services/loft_service.py +++ b/app/services/loft_service.py @@ -2,7 +2,7 @@ import os, json, random, io, logging from pulp import * -from helpers import aws_helper, tar_helper, csv_helper, service_helper, irt_helper +from helpers import aws_helper, tar_helper, csv_helper, service_helper, solver_helper from models.solver_run import SolverRun from models.solution import Solution @@ -54,8 +54,6 @@ class LoftService(Base): # 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 @@ -65,16 +63,8 @@ class LoftService(Base): problem += lpSum([items[item.id] for item in self.solver_run.items]) == self.solver_run.total_form_items, 'Total form items' - for constraint in self.solver_run.constraints: - con = dict(zip([item.id for item in self.solver_run.items], - [item.attribute_exists(constraint.reference_attribute.id, constraint.reference_attribute.value) - for item in self.solver_run.items])) - problem += lpSum([con[item.id] - * items[item.id] - for item in self.solver_run.items]) >= round(self.solver_run.total_form_items * (constraint.minimum / 100)), f'{constraint.reference_attribute.id} - {constraint.reference_attribute.value} - min' - problem += lpSum([con[item.id] - * items[item.id] - for item in self.solver_run.items]) <= round(self.solver_run.total_form_items * (constraint.maximum / 100)), f'{constraint.reference_attribute.id} - {constraint.reference_attribute.value} - max' + # generic constraints + problem = solver_helper.build_constraints(self.solver_run, problem, items) # multi-objective functions and constraints for target in self.solver_run.objective_function.tif_targets: @@ -96,18 +86,9 @@ class LoftService(Base): if LpStatus[problem.status] == 'Optimized': # 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) - + form_items = service_helper.solution_items(problem.variables(), self.solver_run) + # remove items + self.solver_run.remove_items(form_items) # add form to solution solution.forms.append(Form.create(form_items, self.solver_run)) From 88ef6b2e032ae0b4e8828d0ddbe9ee24d5d9ec20 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Mon, 22 Nov 2021 17:25:32 +0000 Subject: [PATCH 12/23] return results whether optimized or not, add status to returned csv --- app/helpers/service_helper.py | 5 +++-- app/models/form.py | 6 ++++-- app/services/loft_service.py | 13 ++++++------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index 4f7f23c..ea9eb5e 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -34,7 +34,8 @@ def solution_to_file(buffer, total_form_items, forms): # write header row for first row utilizing the total items all forms will have # fill the rows with the targets and cut score then the items - header = [] + header = ['status'] + for result in forms[0].tif_results: header += [f'tif @ {round(result.theta, 2)}'] @@ -46,7 +47,7 @@ def solution_to_file(buffer, total_form_items, forms): # add each form as row to processed csv for form in forms: - row = [] + row = [form.status] for result in form.tif_results: row += [f'value - {result.value}\nresult - {round(result.result, 2)}'] diff --git a/app/models/form.py b/app/models/form.py index cae4073..72863ae 100644 --- a/app/models/form.py +++ b/app/models/form.py @@ -13,12 +13,14 @@ class Form(BaseModel): cut_score: float tif_results: List[Target] tcc_results: List[Target] + status: str = 'Not Optimized' @classmethod - def create(cls, items, solver_run): + def create(cls, items, solver_run, status): return cls( items=items, cut_score=TestResponseFunction(solver_run.irt_model).calculate(items, theta=solver_run.theta_cut_score), tif_results=irt_helper.generate_tif_results(items, solver_run), - tcc_results=irt_helper.generate_tcc_results(items, solver_run) + tcc_results=irt_helper.generate_tcc_results(items, solver_run), + status=status ) diff --git a/app/services/loft_service.py b/app/services/loft_service.py index dea817a..de0f363 100644 --- a/app/services/loft_service.py +++ b/app/services/loft_service.py @@ -84,13 +84,12 @@ class LoftService(Base): # solve problem problem.sequentialSolve(problem_objection_functions) - if LpStatus[problem.status] == 'Optimized': - # add return items and create as a form - form_items = service_helper.solution_items(problem.variables(), self.solver_run) - # remove items - self.solver_run.remove_items(form_items) - # add form to solution - solution.forms.append(Form.create(form_items, self.solver_run)) + # add return items and create as a form + form_items = service_helper.solution_items(problem.variables(), self.solver_run) + # remove items + self.solver_run.remove_items(form_items) + # add form to solution + solution.forms.append(Form.create(form_items, self.solver_run, LpStatus[problem.status])) # successfull form, increment f += 1 From da177481033c06771acb6b5098485d0d27283db4 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Mon, 22 Nov 2021 17:37:57 +0000 Subject: [PATCH 13/23] add total forms default to 1, for two reasons. true LOFT will be requesting a single form, and also we do not have the ability to set form count in ui yet --- app/models/solver_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/solver_run.py b/app/models/solver_run.py index 1a97c8c..e8c8543 100644 --- a/app/models/solver_run.py +++ b/app/models/solver_run.py @@ -13,7 +13,7 @@ class SolverRun(BaseModel): irt_model: IRTModel objective_function: ObjectiveFunction total_form_items: int - total_forms: int + total_forms: int = 1 theta_cut_score: float = 0.00 advanced_options: Optional[AdvancedOptions] engine: str From 3b320a67a5926d544858a4d069d18ca2d1804773 Mon Sep 17 00:00:00 2001 From: brmnjsh Date: Tue, 23 Nov 2021 13:11:05 -0500 Subject: [PATCH 14/23] Update app/helpers/service_helper.py Co-authored-by: Owen Helmstetter <84334068+owenhtr@users.noreply.github.com> --- app/helpers/service_helper.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index ea9eb5e..0fb95f8 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -49,10 +49,7 @@ def solution_to_file(buffer, total_form_items, forms): for form in forms: row = [form.status] - for result in form.tif_results: - row += [f'value - {result.value}\nresult - {round(result.result, 2)}'] - - for result in form.tcc_results: + for result in form.tif_results + form.tcc_results: row += [f'value - {result.value}\nresult - {round(result.result, 2)}'] # provide generated items and cut score From 19f6f358565526409a2efe5d465faf27546d7c87 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Wed, 24 Nov 2021 16:24:14 +0000 Subject: [PATCH 15/23] fixes for clarity --- app/helpers/solver_helper.py | 5 ++--- app/lib/irt/item_information_function.py | 3 +++ app/lib/irt/models/three_parameter_logistic.py | 4 ++++ app/lib/irt/test_information_function.py | 4 ++++ app/lib/irt/test_response_function.py | 4 ++++ app/services/loft_service.py | 2 +- 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/helpers/solver_helper.py b/app/helpers/solver_helper.py index b9f7fb0..f8a321e 100644 --- a/app/helpers/solver_helper.py +++ b/app/helpers/solver_helper.py @@ -9,10 +9,9 @@ def build_constraints(solver_run, problem, items): min = constraint.minimum max = constraint.maximum - con = dict(zip([item.id for item in solver_run.items], + con = dict(zip([item.id for item in solver_run.items], [item.attribute_exists(attribute) for item in solver_run.items])) - # print([con[item.id] * items[item.id] for item in solver_items]) problem += lpSum([con[item.id] * items[item.id] for item in solver_run.items]) >= round(total_form_items * (min / 100)), f'{attribute.id} - {attribute.value} - min' @@ -20,4 +19,4 @@ def build_constraints(solver_run, problem, items): * items[item.id] for item in solver_run.items]) <= round(total_form_items * (max / 100)), f'{attribute.id} - {attribute.value} - max' - return problem \ No newline at end of file + return problem diff --git a/app/lib/irt/item_information_function.py b/app/lib/irt/item_information_function.py index f38512e..ddb3cbb 100644 --- a/app/lib/irt/item_information_function.py +++ b/app/lib/irt/item_information_function.py @@ -4,6 +4,9 @@ class ItemInformationFunction(): def __init__(self, irt_model): self.model_data = irt_model + # determines the amount of information for a given question at a given theta (ability level) + # further detailed on page 161, equation 4 here: + # https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5978482/pdf/10.1177_0146621615613308.pdf def calculate(self, **kwargs): if self.model_data.model == '3PL': p = ThreeParameterLogistic(self.model_data, kwargs).result() diff --git a/app/lib/irt/models/three_parameter_logistic.py b/app/lib/irt/models/three_parameter_logistic.py index d440c86..755331e 100644 --- a/app/lib/irt/models/three_parameter_logistic.py +++ b/app/lib/irt/models/three_parameter_logistic.py @@ -6,6 +6,10 @@ class ThreeParameterLogistic: self.e = 2.71828 self.theta = kwargs['theta'] + # contains the primary 3pl function, determining the probably of an inidividual + # that an individual at a certain theta would get a particular question correct + # detailed further on page 161, equation 1 here: + # https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5978482/pdf/10.1177_0146621615613308.pdf def result(self): a = self.model_params.a_param c = self.model_params.c_param diff --git a/app/lib/irt/test_information_function.py b/app/lib/irt/test_information_function.py index 00fcac3..b91f9b5 100644 --- a/app/lib/irt/test_information_function.py +++ b/app/lib/irt/test_information_function.py @@ -5,6 +5,10 @@ class TestInformationFunction(): self.irt_model = irt_model self.iif = ItemInformationFunction(irt_model) + # determins the amount of information + # at a certain theta (ability level) of the sum of a question set correct + # detailed further on page 166, equation 4 here: + # https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5978482/pdf/10.1177_0146621615613308.pdf def calculate(self, items, **kwargs): sum = 0 diff --git a/app/lib/irt/test_response_function.py b/app/lib/irt/test_response_function.py index f40bca9..d06aa83 100644 --- a/app/lib/irt/test_response_function.py +++ b/app/lib/irt/test_response_function.py @@ -6,6 +6,10 @@ class TestResponseFunction(): self.irt_model = irt_model self.irf = ItemResponseFunction(irt_model) + # determins the probably of an inidividual + # at a certain theta (ability level) would get a sum of questions correct + # detailed further on page 166, equation 3 here: + # https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5978482/pdf/10.1177_0146621615613308.pdf def calculate(self, items, **kwargs): sum = 0 diff --git a/app/services/loft_service.py b/app/services/loft_service.py index de0f363..af800a2 100644 --- a/app/services/loft_service.py +++ b/app/services/loft_service.py @@ -1,6 +1,6 @@ import os, json, random, io, logging -from pulp import * +from pulp import LpProblem, LpVariable, LpMinimize, LpStatus, lpSum from helpers import aws_helper, tar_helper, csv_helper, service_helper, solver_helper From 5a35d47391b574d1c7103e2660cd6c363a9c4a2b Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Tue, 30 Nov 2021 19:24:08 +0000 Subject: [PATCH 16/23] convert b param to float from string --- app/helpers/service_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index 0fb95f8..17ac4a3 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -17,7 +17,7 @@ def items_csv_to_dict(items_csv_reader): if key == 0: item[col] = row[key] elif col == 'b_param': - item[col] = row[key] + item[col] = float(row[key]) elif key > 1: item['attributes'].append({ 'id': col, From 3e708c01374b96ed4607d6b550be8bbbfa94a7ae Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Tue, 30 Nov 2021 19:55:30 +0000 Subject: [PATCH 17/23] last item is b param --- app/helpers/service_helper.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index 17ac4a3..1178982 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -12,18 +12,22 @@ def items_csv_to_dict(items_csv_reader): headers = row else: item = { 'attributes': [] } + count = 0 for key, col in enumerate(headers): if key == 0: item[col] = row[key] elif col == 'b_param': - item[col] = float(row[key]) + item[col] = row[key] elif key > 1: item['attributes'].append({ 'id': col, 'value': row[key], 'type': 'metadata' }) + elif count == (1 - len(headers.split(','))): + item['b_param'] = float(row[key]) + count += 1 items.append(item) From e605f787edde9eebc62695c03eca72ea23ecfb77 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Tue, 30 Nov 2021 20:00:59 +0000 Subject: [PATCH 18/23] simplify, also its a list --- app/helpers/service_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index 1178982..f046498 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -15,6 +15,7 @@ def items_csv_to_dict(items_csv_reader): count = 0 for key, col in enumerate(headers): + count += 1 if key == 0: item[col] = row[key] elif col == 'b_param': @@ -25,9 +26,8 @@ def items_csv_to_dict(items_csv_reader): 'value': row[key], 'type': 'metadata' }) - elif count == (1 - len(headers.split(','))): + elif count == len(headers): item['b_param'] = float(row[key]) - count += 1 items.append(item) From 13448fb73a2c2e98a948bee16b1219b8494e1409 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Tue, 30 Nov 2021 20:20:32 +0000 Subject: [PATCH 19/23] remove float --- app/helpers/service_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index f046498..05206b1 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -27,7 +27,7 @@ def items_csv_to_dict(items_csv_reader): 'type': 'metadata' }) elif count == len(headers): - item['b_param'] = float(row[key]) + item['b_param'] = row[key] items.append(item) From 1649f5cee13c01f2dc5d86203eca06fe915ec7e1 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Tue, 30 Nov 2021 20:35:23 +0000 Subject: [PATCH 20/23] better way to b param --- app/helpers/service_helper.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index 05206b1..47e72b6 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -12,22 +12,19 @@ def items_csv_to_dict(items_csv_reader): headers = row else: item = { 'attributes': [] } - count = 0 for key, col in enumerate(headers): - count += 1 if key == 0: item[col] = row[key] - elif col == 'b_param': - item[col] = row[key] + # b param + elif key == (1 - len(headers)): + item['b_param'] = row[key] elif key > 1: item['attributes'].append({ 'id': col, 'value': row[key], 'type': 'metadata' }) - elif count == len(headers): - item['b_param'] = row[key] items.append(item) From ebccd9522a699970b77a69e0a97480bf1229d8c7 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Tue, 30 Nov 2021 20:41:08 +0000 Subject: [PATCH 21/23] a test --- app/helpers/service_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index 47e72b6..f2b521f 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -25,7 +25,7 @@ def items_csv_to_dict(items_csv_reader): 'value': row[key], 'type': 'metadata' }) - + print(row(1 - len(headers))) items.append(item) return items From b2c3f0df792c6186371e6af5862d596c0f602d59 Mon Sep 17 00:00:00 2001 From: Josh Burman Date: Tue, 30 Nov 2021 21:12:08 +0000 Subject: [PATCH 22/23] add b param to irt model --- app/helpers/service_helper.py | 6 +++--- app/models/constraint.py | 1 - app/models/irt_model.py | 3 ++- app/services/loft_service.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/helpers/service_helper.py b/app/helpers/service_helper.py index f2b521f..399882a 100644 --- a/app/helpers/service_helper.py +++ b/app/helpers/service_helper.py @@ -2,7 +2,7 @@ import csv import io import re -def items_csv_to_dict(items_csv_reader): +def items_csv_to_dict(items_csv_reader, irt_model): items = [] headers = [] @@ -16,7 +16,7 @@ def items_csv_to_dict(items_csv_reader): for key, col in enumerate(headers): if key == 0: item[col] = row[key] - # b param + # b param - tmep fix! use irt model b param for proper reference elif key == (1 - len(headers)): item['b_param'] = row[key] elif key > 1: @@ -25,7 +25,7 @@ def items_csv_to_dict(items_csv_reader): 'value': row[key], 'type': 'metadata' }) - print(row(1 - len(headers))) + items.append(item) return items diff --git a/app/models/constraint.py b/app/models/constraint.py index 2c84a60..28a3939 100644 --- a/app/models/constraint.py +++ b/app/models/constraint.py @@ -1,5 +1,4 @@ from pydantic import BaseModel -from typing import Optional from models.attribute import Attribute diff --git a/app/models/irt_model.py b/app/models/irt_model.py index 4751ba9..2596686 100644 --- a/app/models/irt_model.py +++ b/app/models/irt_model.py @@ -1,7 +1,8 @@ from pydantic import BaseModel +from typing import Dict class IRTModel(BaseModel): a_param: float - b_param: float + b_param: Dict = {"schema_bson_id": str, "field_bson_id": str} c_param: float model: str diff --git a/app/services/loft_service.py b/app/services/loft_service.py index af800a2..07bfe2a 100644 --- a/app/services/loft_service.py +++ b/app/services/loft_service.py @@ -33,7 +33,7 @@ class LoftService(Base): items_csv_reader = csv_helper.file_stream_reader(items_csv) # add items to attributes dict - attributes['items'] = service_helper.items_csv_to_dict(items_csv_reader) + attributes['items'] = service_helper.items_csv_to_dict(items_csv_reader, attributes['irt_model']) logging.info('Processed Attributes...') return attributes From c36b4cdc1803614cdbadd667a6e15725804d3123 Mon Sep 17 00:00:00 2001 From: Jared Numrab Date: Sat, 4 Dec 2021 03:19:17 -0500 Subject: [PATCH 23/23] 1.0, using coinor-cbc instead of building from binary --- .docker-compose/Dockerfile | 13 +++++-------- Dockerfile | 13 +++++-------- app/main.py | 2 +- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.docker-compose/Dockerfile b/.docker-compose/Dockerfile index 319a6ea..703424a 100644 --- a/.docker-compose/Dockerfile +++ b/.docker-compose/Dockerfile @@ -1,18 +1,15 @@ FROM python:3.9.6 -RUN mkdir /app -WORKDIR /app - +RUN apt-get update +RUN apt-get -y install coinor-cbc RUN python -m pip install pulp -RUN svn checkout https://projects.coin-or.org/svn/Cbc/releases/2.9.8 Cbc-2.9.8 -RUN cd Cbc-2.9.8 && \ - ./configure && \ - make && \ - make install RUN python -m pip install pydantic RUN python -m pip install pySqsListener RUN python -m pip install daemonize +RUN mkdir /app +WORKDIR /app + # Bundle app source COPY . /app diff --git a/Dockerfile b/Dockerfile index 319a6ea..703424a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,15 @@ FROM python:3.9.6 -RUN mkdir /app -WORKDIR /app - +RUN apt-get update +RUN apt-get -y install coinor-cbc RUN python -m pip install pulp -RUN svn checkout https://projects.coin-or.org/svn/Cbc/releases/2.9.8 Cbc-2.9.8 -RUN cd Cbc-2.9.8 && \ - ./configure && \ - make && \ - make install RUN python -m pip install pydantic RUN python -m pip install pySqsListener RUN python -m pip install daemonize +RUN mkdir /app +WORKDIR /app + # Bundle app source COPY . /app diff --git a/app/main.py b/app/main.py index ef2cd1f..2c601bc 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.6.0)...') + logging.info('Starting Solver Service (v1.0.0)...') listener = ServiceListener( 'measure-development-solver-ingest', region_name=os.environ['AWS_REGION'],