diff --git a/app/main.py b/app/main.py index e9f0143..5d307c1 100644 --- a/app/main.py +++ b/app/main.py @@ -43,7 +43,7 @@ class ServiceListener(Consumer): logging.error(f'action of type {action} does not exist.') def main(): - logging.info('Starting IRT Service: The Enemies Within (v1.7.0)...') + logging.info('Starting IRT Service: Tokyo Drift 2: Driftocolypse (v1.8.0)...') # ToDo: Figure out a much better way of doing this. # LocalStack wants 'endpoint_url', while prod doesnt :( diff --git a/app/models/item.py b/app/models/item.py index 2c1ca66..7f786aa 100644 --- a/app/models/item.py +++ b/app/models/item.py @@ -1,6 +1,5 @@ -import logging from pydantic import BaseModel, validator -from typing import List, Optional, Tuple +from typing import List, Optional from models.attribute import Attribute diff --git a/app/models/objective_function.py b/app/models/objective_function.py index 3a43680..35f9a6c 100644 --- a/app/models/objective_function.py +++ b/app/models/objective_function.py @@ -1,13 +1,17 @@ from __future__ import annotations +from typing import TYPE_CHECKING, Any, Literal, Union -from pydantic import BaseModel -from typing import Dict, List, AnyStr -from pulp import lpSum +from pydantic import BaseModel, validator +from typing import Dict, List +from pulp import lpSum, LpMinimize, LpMaximize from models.targets.tif_target import TifTarget from models.targets.tcc_target import TccTarget from models.problem import Problem +if TYPE_CHECKING: + from models.solver_run import SolverRun + class ObjectiveFunction(BaseModel): # minimizing tif/tcc target value is only option currently # as we add more we can build this out to be more dynamic @@ -15,17 +19,45 @@ class ObjectiveFunction(BaseModel): tif_targets: List[TifTarget] tcc_targets: List[TccTarget] target_variance_percentage: int = 10 - objective: AnyStr = "minimize" + objective: Literal[1,-1] = 1 + functions: List[Literal['tcc', 'tif']] = ['tcc', 'tif'] weight: Dict = {'tif': 1, 'tcc': 1} - def for_problem(self, problem_handler: Problem) -> None: - problem_handler.problem += lpSum([ - bundle.count * problem_handler.solver_bundles_var[bundle.id] - for bundle in problem_handler.bundles - ] + [ - problem_handler.solver_items_var[item.id] - for item in problem_handler.items - ]) + @validator("objective", pre=True) + def set_objective(cls, v) -> List[int]: + if v == 'minimize': + return 1 + elif v == 'maximize': + return -1 + else: + return None + + def for_problem(self, problem_handler: Problem, solver_run: SolverRun) -> List[lpSum]: + functions = [] + + for function in self.functions: + if function == 'tcc': + functions.append(lpSum([ + bundle.trf(solver_run.irt_model, solver_run.theta_cut_score) + * problem_handler.solver_bundles_var[bundle.id] + for bundle in problem_handler.bundles + ] + [ + item.irf(solver_run.irt_model, solver_run.theta_cut_score) * + problem_handler.solver_items_var[item.id] + for item in problem_handler.items + ])) + elif function == 'tif': + problem_handler.problem += lpSum([ + bundle.tif(solver_run.irt_model, solver_run.theta_cut_score) + * problem_handler.solver_bundles_var[bundle.id] + for bundle in problem_handler.bundles + ] + [ + item.iif(solver_run.irt_model, solver_run.theta_cut_score) * + problem_handler.solver_items_var[item.id] + for item in problem_handler.items + ]) + + return functions def increment_targets_drift(self, limit: float or bool, diff --git a/app/models/problem.py b/app/models/problem.py index f08ed73..1179af4 100644 --- a/app/models/problem.py +++ b/app/models/problem.py @@ -43,7 +43,12 @@ class Problem(BaseModel): def solve(self, solver_run: SolverRun, enemy_ids: List[int] = []) -> LpProblem: logging.info('solving problem...') - self.problem.solve() + + # creating problem multi-objective functions + objective_functions = solver_run.objective_function.for_problem(self, solver_run) + self.problem.sequentialSolve(objective_functions) + + return self.problem # NOTICE: Legacy enemies implementation # leaving this in, just in case the current impl fails to function @@ -106,9 +111,6 @@ class Problem(BaseModel): def generate(self, solution: Solution, solver_run: SolverRun) -> None: try: - # creating problem objective function - solver_run.objective_function.for_problem(self) - logging.info('Creating Constraints...') # generic constraints for constraint in solver_run.constraints: diff --git a/app/models/solver_run.py b/app/models/solver_run.py index 2d486dd..1d881ae 100644 --- a/app/models/solver_run.py +++ b/app/models/solver_run.py @@ -18,10 +18,6 @@ from models.bundle import Bundle from models.objective_function import ObjectiveFunction from models.advanced_options import AdvancedOptions -if TYPE_CHECKING: - from models.solution import Solution - from models.problem import Problem - ConstraintType = TypeVar('ConstraintType', bound=GenericConstraint) class SolverRun(BaseModel): @@ -36,6 +32,8 @@ class SolverRun(BaseModel): theta_cut_score: float = 0.00 drift_style: Literal['constant', 'variable'] = 'constant' allow_enemies: bool = False + max_attempts: int + max_drift: int = 10 advanced_options: Optional[AdvancedOptions] engine: str diff --git a/app/services/form_generation_service.py b/app/services/form_generation_service.py index 2f14f2f..c1349d7 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, LpMinimize, LpStatus +from pulp import LpProblem, LpStatus from lib.application_configs import ApplicationConfigs from helpers import aws_helper, tar_helper, csv_helper, service_helper @@ -71,17 +71,22 @@ class FormGenerationService(Base): # iterate for number of forms that require creation for form_count in range(self.solver_run.total_forms): form_number = form_count + 1 + drift_increment = self.solver_run.max_drift / (self.solver_run.max_attempts - 1) current_drift = 0 # FF Tokyo Drift + current_attempt = 0 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(): + # respect max attempts + # this will likely be more built out when we add increment rate & drif limit + while current_attempt <= self.solver_run.max_attempts: drift_percent = current_drift / 100 self.solver_run.objective_function.update_targets_drift( drift_percent) # create problem - problem_handler = Problem(items = self.solver_run.unbundled_items(), bundles = self.solver_run.bundles, problem = LpProblem('ata-form-generate', LpMinimize)) + problem = LpProblem('ata-form-generate', self.solver_run.objective_function.objective) + problem_handler = Problem(items = self.solver_run.unbundled_items(), bundles = self.solver_run.bundles, problem = problem) problem_handler.generate(solution, self.solver_run) problem = problem_handler.solve(self.solver_run) @@ -90,7 +95,7 @@ class FormGenerationService(Base): f'attempt infeasible for drift of {current_drift}%') if current_drift >= Target.max_drift( - ): # this is the last attempt, so lets finalize the solution + ) or current_attempt >= self.solver_run.max_attempts: # this is the last attempt, so lets finalize the solution if ApplicationConfigs.local_dev_env: service_helper.print_problem_variables(problem) @@ -103,7 +108,8 @@ class FormGenerationService(Base): break - current_drift += Target.max_drift_increment() + current_drift += drift_increment + current_attempt += 1 else: if ApplicationConfigs.local_dev_env: service_helper.print_problem_variables(problem)