refactor of model targets and constraints, addition of new constraint types and constraint construction process

This commit is contained in:
Joshua Burman 2023-11-12 18:32:48 -05:00
parent 07af0ac0ac
commit f1fa519f31
16 changed files with 140 additions and 86 deletions

View File

@ -1,14 +1,15 @@
from lib.irt.test_response_function import TestResponseFunction from lib.irt.test_response_function import TestResponseFunction
from lib.irt.test_information_function import TestInformationFunction from lib.irt.test_information_function import TestInformationFunction
from models.target import Target from models.targets.tif_target import TifTarget
from models.targets.tcc_target import TccTarget
def generate_tif_results(items, solver_run): def generate_tif_results(items, solver_run):
targets = [] targets = []
for target in solver_run.objective_function.tif_targets: for target in solver_run.objective_function.tif_targets:
tif = TestInformationFunction(solver_run.irt_model).calculate(items, theta=target.theta) tif = TestInformationFunction(solver_run.irt_model).calculate(items, theta=target.theta)
targets.append(Target(theta=target.theta, value=target.value, result=tif)) targets.append(TifTarget(theta=target.theta, value=target.value, result=tif))
return targets return targets
@ -17,6 +18,6 @@ def generate_tcc_results(items, solver_run):
for target in solver_run.objective_function.tcc_targets: for target in solver_run.objective_function.tcc_targets:
tcc = TestResponseFunction(solver_run.irt_model).calculate(items, theta=target.theta) tcc = TestResponseFunction(solver_run.irt_model).calculate(items, theta=target.theta)
targets.append(Target(theta=target.theta, value=target.value, result=tcc)) targets.append(TccTarget(theta=target.theta, value=target.value, result=tcc))
return targets return targets

View File

@ -9,4 +9,5 @@ class AdvancedOptions(BaseModel):
brand_bound_tolerance: Optional[float] = None brand_bound_tolerance: Optional[float] = None
max_forms: Optional[int] = None max_forms: Optional[int] = None
precision: Optional[float] = None precision: Optional[float] = None
ensure_form_uniqueness: bool = True
extra_param_range: Optional[List[Dict]] = None extra_param_range: Optional[List[Dict]] = None

View File

@ -1,7 +1,7 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional from typing import Optional, Union
class Attribute(BaseModel): class Attribute(BaseModel):
value: Optional[str] value: Optional[Union[str,int]]
type: Optional[str] type: Optional[str]
id: str id: str

View File

@ -1,9 +1,9 @@
from random import randint from random import randint
from models.constraint import * from models.constraints.generic_constraint import *
class BundleConstraint(Constraint): class BundleConstraint(GenericConstraint):
def build(self, problem_handler: Problem, _) -> None: def build(self, problem_handler: Problem, **kwargs) -> None:
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

View File

@ -0,0 +1,34 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from models.constraints.generic_constraint import *
if TYPE_CHECKING:
from models.solver_run import SolverRun
class FormUniquenessConstraint(GenericConstraint):
@classmethod
def create(cls: Type[_T], total_items: int) -> _T:
return cls(
minimum=0,
maximum=0,
reference_attribute=Attribute(
value=total_items,
type='form_uniqueness',
id='form_uniqueness'
)
)
def build(self, problem_handler: Problem, **kwargs) -> None:
logging.info('Form Uniqueness Constraint Generating...')
# form uniqueness constraint
problem_handler.problem += lpSum(
[
kwargs['solution'].items_exist_in_forms(bundle.items) * problem_handler.solver_bundles_var[bundle.id]
for bundle in problem_handler.bundles
] + [
kwargs['solution'].items_exist_in_forms([item]) * problem_handler.solver_items_var[item.id]
for item in problem_handler.items
]
) <= self.reference_attribute.value, f'Ensuring uniqueness for form'

View File

@ -2,13 +2,16 @@ import logging
from pulp import lpSum from pulp import lpSum
from pydantic import BaseModel from pydantic import BaseModel
from typing import TypeVar, Type
from helpers.common_helper import * from helpers.common_helper import *
from models.attribute import Attribute from models.attribute import Attribute
from models.problem import Problem from models.problem import Problem
class Constraint(BaseModel): _T = TypeVar("_T")
class GenericConstraint(BaseModel):
reference_attribute: Attribute reference_attribute: Attribute
minimum: float minimum: float
maximum: float maximum: float

View File

@ -1,13 +1,13 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from models.constraint import * from models.constraints.generic_constraint import *
if TYPE_CHECKING: if TYPE_CHECKING:
from models.solver_run import SolverRun from models.solver_run import SolverRun
class MetadataConstraint(Constraint): class MetadataConstraint(GenericConstraint):
def build(self, problem_handler: Problem, solver_run: SolverRun) -> None: def build(self, problem_handler: Problem, **kwargs) -> None:
logging.info('Metadata Constraint Generating...') logging.info('Metadata Constraint Generating...')
problem_handler.problem += lpSum( problem_handler.problem += lpSum(
@ -17,7 +17,7 @@ class MetadataConstraint(Constraint):
[ [
item.attribute_exists(self.reference_attribute).real * problem_handler.solver_items_var[item.id] for item in problem_handler.items item.attribute_exists(self.reference_attribute).real * problem_handler.solver_items_var[item.id] for item in problem_handler.items
] ]
) >= round(solver_run.total_form_items * (self.minimum / 100)), f'{self.reference_attribute.id} - {self.reference_attribute.value} - min' ) >= round(kwargs['solver_run'].total_form_items * (self.minimum / 100)), f'{self.reference_attribute.id} - {self.reference_attribute.value} - min'
problem_handler.problem += lpSum( problem_handler.problem += lpSum(
[ [
@ -26,4 +26,4 @@ class MetadataConstraint(Constraint):
[ [
item.attribute_exists(self.reference_attribute).real * problem_handler.solver_items_var[item.id] for item in problem_handler.items item.attribute_exists(self.reference_attribute).real * problem_handler.solver_items_var[item.id] for item in problem_handler.items
] ]
) <= round(solver_run.total_form_items * (self.maximum / 100)), f'{self.reference_attribute.id} - {self.reference_attribute.value} - max' ) <= round(kwargs['solver_run'].total_form_items * (self.maximum / 100)), f'{self.reference_attribute.id} - {self.reference_attribute.value} - max'

View File

@ -0,0 +1,33 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from models.constraints.generic_constraint import *
if TYPE_CHECKING:
from models.solver_run import SolverRun
class TotalFormItemsConstraint(GenericConstraint):
@classmethod
def create(cls: Type[_T], total_items: int) -> _T:
return cls(
minimum=0,
maximum=0,
reference_attribute=Attribute(
value=total_items,
type='form_uniqueness',
id='form_uniqueness'
)
)
def build(self, problem_handler: Problem, **kwargs) -> None:
logging.info('Total Form Items Constraint Generating...')
problem_handler.problem += lpSum(
[
bundle.count * problem_handler.solver_bundles_var[bundle.id]
for bundle in problem_handler.bundles
] + [
1 * problem_handler.solver_items_var[item.id]
for item in problem_handler.items
]
) == self.reference_attribute.value, f'Total bundle form items for form'

View File

@ -7,7 +7,8 @@ from typing import List, TypeVar, Type
from helpers import irt_helper from helpers import irt_helper
from models.item import Item from models.item import Item
from models.target import Target from models.targets.tif_target import TifTarget
from models.targets.tcc_target import TccTarget
from lib.irt.test_response_function import TestResponseFunction from lib.irt.test_response_function import TestResponseFunction
@ -19,8 +20,8 @@ _T = TypeVar("_T")
class Form(BaseModel): class Form(BaseModel):
items: List[Item] items: List[Item]
cut_score: float cut_score: float
tif_results: List[Target] tif_results: List[TifTarget]
tcc_results: List[Target] tcc_results: List[TccTarget]
status: str = 'Not Optimized' status: str = 'Not Optimized'
solver_variables: List[str] solver_variables: List[str]

View File

@ -2,10 +2,11 @@ from __future__ import annotations
from pydantic import BaseModel from pydantic import BaseModel
from typing import Dict, List, AnyStr from typing import Dict, List, AnyStr
from pulp import lpSum
from models.tif_target import TifTarget from models.targets.tif_target import TifTarget
from models.tcc_target import TccTarget from models.targets.tcc_target import TccTarget
from models.problem import Problem
class ObjectiveFunction(BaseModel): class ObjectiveFunction(BaseModel):
# minimizing tif/tcc target value is only option currently # minimizing tif/tcc target value is only option currently
@ -17,6 +18,15 @@ class ObjectiveFunction(BaseModel):
objective: AnyStr = "minimize" objective: AnyStr = "minimize"
weight: Dict = {'tif': 1, 'tcc': 1} 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
])
def increment_targets_drift(self, def increment_targets_drift(self,
limit: float or bool, limit: float or bool,
all: bool = False, all: bool = False,

View File

@ -38,53 +38,23 @@ class Problem(BaseModel):
upBound=1, upBound=1,
cat='Binary') 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: def solve(self) -> LpProblem:
self.problem.solve() self.problem.solve()
return self.problem return self.problem
def generate(self, solution: Solution, solver_run: SolverRun) -> None: def generate(self, solution: Solution, solver_run: SolverRun) -> None:
# Objective Function
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) -> None:
logging.info('Creating Constraints...')
try: try:
for constraint in solver_run.constraints: # creating problem objective function
constraint.build(self, solver_run) solver_run.objective_function.for_problem(self)
logging.info('Creating Constraints...')
# generic constraints
for constraint in solver_run.constraints:
constraint.build(self, solver_run=solver_run, solution=solution)
# irt target constraints
for target in solver_run.objective_function.all_targets(): for target in solver_run.objective_function.all_targets():
target.build_constraint(self, solver_run) target.constraints(self, solver_run)
logging.info('Constraints Created...') logging.info('Constraints Created...')
except ValueError as error: except ValueError as error:

View File

@ -1,23 +1,32 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from pulp import lpSum
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Literal, Optional, Union from typing import List, Literal, Optional, Union
import logging import logging
import random
from models.item import Item from models.item import Item
from models.constraint import Constraint from models.constraints.generic_constraint import GenericConstraint
from models.metadata_constraint import MetadataConstraint from models.constraints.metadata_constraint import MetadataConstraint
from models.bundle_constraint import BundleConstraint from models.constraints.bundle_constraint import BundleConstraint
from models.constraints.form_uniqueness_constraint import FormUniquenessConstraint
from models.constraints.total_form_items_constraint import TotalFormItemsConstraint
from models.irt_model import IRTModel from models.irt_model import IRTModel
from models.bundle import Bundle from models.bundle import Bundle
from models.objective_function import ObjectiveFunction from models.objective_function import ObjectiveFunction
from models.advanced_options import AdvancedOptions from models.advanced_options import AdvancedOptions
if TYPE_CHECKING:
from models.solution import Solution
from models.problem import Problem
class SolverRun(BaseModel): class SolverRun(BaseModel):
items: List[Item] = [] items: List[Item] = []
bundles: List[Bundle] = [] bundles: List[Bundle] = []
bundle_first_ordering: bool = True bundle_first_ordering: bool = True
constraints: List[Union[Constraint, MetadataConstraint, BundleConstraint]] constraints: List[Union[GenericConstraint, MetadataConstraint, BundleConstraint, FormUniquenessConstraint, TotalFormItemsConstraint]]
irt_model: IRTModel irt_model: IRTModel
objective_function: ObjectiveFunction objective_function: ObjectiveFunction
total_form_items: int total_form_items: int
@ -32,8 +41,16 @@ class SolverRun(BaseModel):
# this is all a compensator for dynamically creating objects # this is all a compensator for dynamically creating objects
# ideally we'd change the payload to determine what type it is # ideally we'd change the payload to determine what type it is
constraints: [Constraint|MetadataConstraint|BundleConstraint] = [] constraints: [GenericConstraint|MetadataConstraint|BundleConstraint|FormUniquenessConstraint|TotalFormItemsConstraint] = []
# total form items
constraints.append(TotalFormItemsConstraint.create(self.total_form_items))
# ensure form uniqueness
if self.advanced_options.ensure_form_uniqueness:
constraints.append(FormUniquenessConstraint.create(self.total_form_items - 1))
# repackage to create appropriate constraint types
for constraint in self.constraints: for constraint in self.constraints:
if constraint.reference_attribute.type == 'metadata': if constraint.reference_attribute.type == 'metadata':
constraints.append(MetadataConstraint(reference_attribute=constraint.reference_attribute, minimum=constraint.minimum, maximum=constraint.maximum)) constraints.append(MetadataConstraint(reference_attribute=constraint.reference_attribute, minimum=constraint.minimum, maximum=constraint.maximum))
@ -130,17 +147,3 @@ class SolverRun(BaseModel):
else: else:
return self.items return self.items
def select_items_by_percent(self, percent: int) -> List[Item]:
items = self.unbundled_items()
total_items = len(items)
selected_items_amount = round(total_items - (total_items *
(percent / 100)))
return random.sample(items, selected_items_amount)
def select_bundles_by_percent(self, percent: int) -> List[Bundle]:
total_bundles = len(self.bundles)
selected_bundles_amount = round(total_bundles - (total_bundles *
(percent / 100)))
return random.sample(self.bundles, selected_bundles_amount)

View File

@ -1,13 +1,13 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from models.target import * from models.targets.target import *
if TYPE_CHECKING: if TYPE_CHECKING:
from models.problem import Problem from models.problem import Problem
class TccTarget(Target): class TccTarget(Target):
def build_constraint(self, problem_handler: Problem, solver_run: SolverRun): def constraints(self, problem_handler: Problem, solver_run: SolverRun):
problem_handler.problem += lpSum([ problem_handler.problem += lpSum([
bundle.trf(solver_run.irt_model, self.theta) bundle.trf(solver_run.irt_model, self.theta)
* problem_handler.solver_bundles_var[bundle.id] * problem_handler.solver_bundles_var[bundle.id]

View File

@ -1,13 +1,13 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from models.target import * from models.targets.target import *
if TYPE_CHECKING: if TYPE_CHECKING:
from models.problem import Problem from models.problem import Problem
class TifTarget(Target): class TifTarget(Target):
def build_constraint(self, problem_handler: Problem, solver_run: SolverRun): def constraints(self, problem_handler: Problem, solver_run: SolverRun):
problem_handler.problem += lpSum([ problem_handler.problem += lpSum([
bundle.tif(solver_run.irt_model, self.theta) bundle.tif(solver_run.irt_model, self.theta)
* problem_handler.solver_bundles_var[bundle.id] * problem_handler.solver_bundles_var[bundle.id]

View File

@ -10,7 +10,7 @@ from models.solver_run import SolverRun
from models.solution import Solution from models.solution import Solution
from models.problem import Problem from models.problem import Problem
from models.form import Form from models.form import Form
from models.target import Target from models.targets.target import Target
from services.base import Base from services.base import Base
@ -82,8 +82,6 @@ class FormGenerationService(Base):
# create problem # create problem
problem_handler = Problem(items = self.solver_run.unbundled_items(), bundles = self.solver_run.bundles, problem = LpProblem('ata-form-generate', LpMinimize)) 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_handler.generate(solution, self.solver_run)
problem_handler.generate_constraints(self.solver_run, current_drift)
problem = problem_handler.solve() problem = problem_handler.solve()
if LpStatus[problem.status] == 'Infeasible': if LpStatus[problem.status] == 'Infeasible':