From e85b002a9f7d34bd1b90c591025472ec23009a65 Mon Sep 17 00:00:00 2001 From: Vincent Auriau Date: Sat, 28 Dec 2024 14:37:41 -0800 Subject: [PATCH] ADD: Tools tests (#208) --- choice_learn/toolbox/or_tools_opt.py | 19 ++- requirements-developer.txt | 1 + tests/unit_tests/tools/test_base.py | 199 +++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 tests/unit_tests/tools/test_base.py diff --git a/choice_learn/toolbox/or_tools_opt.py b/choice_learn/toolbox/or_tools_opt.py index d5b08fd0..93164d49 100644 --- a/choice_learn/toolbox/or_tools_opt.py +++ b/choice_learn/toolbox/or_tools_opt.py @@ -32,6 +32,9 @@ def __init__(self, utilities, itemwise_values, assortment_size, outside_option_g if not self.outside_option_given: self.utilities = np.concatenate([[np.exp(0.0)], utilities], axis=0) self.itemwise_values = np.concatenate([[0.0], itemwise_values], axis=0) + else: + self.utilities = utilities + self.itemwise_values = itemwise_values self.n_items = len(self.utilities) - 1 self.assortment_size = assortment_size @@ -460,9 +463,13 @@ def __init__( self.class_utilities = class_utilities self.itemwise_values = itemwise_values else: - # TO DO - self.class_utilities = class_utilities - self.itemwise_values = itemwise_values + # First Specified item + self.outside_utility = [class_utilities[i][0][0] for i in range(len(class_weights))] + self.outside_value = [itemwise_values[i][0] for i in range(len(class_weights))] + + self.class_utilities = [class_utilities[i][1:] for i in range(len(class_weights))] + self.itemwise_values = [itemwise_values[i][1:] for i in range(len(class_weights))] + self.n_items = len(self.itemwise_values) - 1 self.assortment_size = assortment_size self.class_weights = class_weights @@ -639,7 +646,11 @@ def add_minimal_capacity_constraint(self, itemwise_capacities, minimum_capacity) Value of the maximal capacity. """ assortment_capacity = sum( - [self.y[j] * itemwise_capacities[j - 1] for j in range(1, self.n_items + 1)] + [ + self.y[(j, k)] * itemwise_capacities[j - 1] + for j in range(1, self.n_items + 1) + for k in range(len(self.itemwise_values[j - 1])) + ] ) self.solver.Add(assortment_capacity >= minimum_capacity) diff --git a/requirements-developer.txt b/requirements-developer.txt index 9f2ad890..c8c06cf2 100644 --- a/requirements-developer.txt +++ b/requirements-developer.txt @@ -10,6 +10,7 @@ mkdocs==1.5.3 mkdocs-material==9.5.3 mkdocs-nbconvert==0.2.1 mkdocstrings-python==1.7.5 +ortools python-markdown-math bandit==1.7.5 nbstripout==0.6.1 diff --git a/tests/unit_tests/tools/test_base.py b/tests/unit_tests/tools/test_base.py new file mode 100644 index 00000000..497c86fb --- /dev/null +++ b/tests/unit_tests/tools/test_base.py @@ -0,0 +1,199 @@ +"""Testing base ChoiceModel.""" + +import numpy as np +import pytest + +from choice_learn.toolbox.assortment_optimizer import ( + LatentClassAssortmentOptimizer, + LatentClassPricingOptimizer, + MNLAssortmentOptimizer, +) + +solvers = ["or-tools"] + + +def test_mnl_assort_instantiate(): + """Test instantiation with both solvers.""" + for solv in solvers: + opt = MNLAssortmentOptimizer( + solver=solv, + utilities=np.array([1.0, 2.0, 3.0]), + itemwise_values=np.array([0.5, 0.5, 0.5]), + assortment_size=2, + ) + opt.solve() + + +def test_various_params(): + """Test specific parametrizations.""" + MNLAssortmentOptimizer( + solver="ortools", + utilities=np.array([1.0, 2.0, 3.0]), + itemwise_values=np.array([0.5, 0.5, 0.5]), + assortment_size=2, + outside_option_given=True, + ) + LatentClassAssortmentOptimizer( + solver="ortools", + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array([[1.0, 2.0, 3.0], [3.0, 2.0, 1.0]]), + itemwise_values=np.array([0.5, 0.5, 0.5]), + assortment_size=12, + outside_option_given=True, + ) + LatentClassPricingOptimizer( + solver="ortools", + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array( + [[[1.0, 1.1], [2.0, 2.1], [3.0, 3.1]], [[3.0, 3.1], [2.0, 2.1], [1.0, 1.1]]] + ), + itemwise_values=np.array([[0.5, 1.2], [0.5, 1.2], [0.5, 1.2]]), + assortment_size=12, + outside_option_given=True, + ) + + +def test_capacity_constraints(): + """Test that capacity constraints work.""" + opt = LatentClassAssortmentOptimizer( + solver="ortools", + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array([[1.0, 2.0, 3.0], [3.0, 2.0, 1.0]]), + itemwise_values=np.array([0.5, 0.5, 0.5]), + assortment_size=12, + ) + + opt.add_maximal_capacity_constraint(itemwise_capacities=[1.1, 2.2, 3.3], maximum_capacity=4.5) + opt.add_minimal_capacity_constraint(itemwise_capacities=[1.1, 2.2, 3.3], minimum_capacity=1.2) + + opt = LatentClassPricingOptimizer( + solver="ortools", + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array( + [[[1.0, 1.1], [2.0, 2.1], [3.0, 3.1]], [[3.0, 3.1], [2.0, 2.1], [1.0, 1.1]]] + ), + itemwise_values=np.array([[0.5, 1.2], [0.5, 1.2], [0.5, 1.2]]), + assortment_size=2, + ) + + opt.add_maximal_capacity_constraint(itemwise_capacities=[1.1, 2.2, 3.3], maximum_capacity=4.5) + opt.add_minimal_capacity_constraint(itemwise_capacities=[1.1, 2.2, 3.3], minimum_capacity=1.2) + + +def test_lc_assort_instantiate(): + """Test instantiation with both solvers.""" + for solv in solvers: + opt = LatentClassAssortmentOptimizer( + solver=solv, + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array([[1.0, 2.0, 3.0], [3.0, 2.0, 1.0]]), + itemwise_values=np.array([0.5, 0.5, 0.5]), + assortment_size=2, + ) + opt.solve() + + +def test_lc_pricing_instantiate(): + """Test instantiation with both solvers.""" + for solv in solvers: + opt = LatentClassPricingOptimizer( + solver=solv, + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array( + [[[1.0, 1.1], [2.0, 2.1], [3.0, 3.1]], [[3.0, 3.1], [2.0, 2.1], [1.0, 1.1]]] + ), + itemwise_values=np.array([[0.5, 1.2], [0.5, 1.2], [0.5, 1.2]]), + assortment_size=2, + ) + opt.solve() + + +def test_wrong_solver(): + """Test error raised when specifying wrong solver.""" + solver = "rotools" + with pytest.raises(ValueError): + MNLAssortmentOptimizer( + solver=solver, + utilities=np.array([1.0, 2.0, 3.0]), + itemwise_values=np.array([0.5, 0.5, 0.5]), + assortment_size=2, + ) + with pytest.raises(ValueError): + LatentClassAssortmentOptimizer( + solver=solver, + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array([[1.0, 2.0, 3.0], [3.0, 2.0, 1.0]]), + itemwise_values=np.array([0.5, 0.5, 0.5]), + assortment_size=2, + ) + with pytest.raises(ValueError): + LatentClassPricingOptimizer( + solver=solver, + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array( + [[[1.0, 1.1], [2.0, 2.1], [3.0, 3.1]], [[3.0, 3.1], [2.0, 2.1], [1.0, 1.1]]] + ), + itemwise_values=np.array([[0.5, 1.2], [0.5, 1.2], [0.5, 1.2]]), + assortment_size=2, + ) + + +def test_raised_errors(): + """Test diverse parametrization that should raise errors.""" + with pytest.raises(ValueError): + MNLAssortmentOptimizer( + solver="ortools", + utilities=np.array([1.0, 2.0, 3.0, 4.0]), + itemwise_values=np.array([0.5, 0.5, 0.5]), + assortment_size=2, + ) + with pytest.raises(ValueError): + LatentClassAssortmentOptimizer( + solver="ortools", + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array([[1.0, 2.0], [3.0, 2.0]]), + itemwise_values=np.array([0.5, 0.5, 0.5]), + assortment_size=2, + ) + + with pytest.raises(ValueError): + LatentClassAssortmentOptimizer( + solver="ortools", + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array([[1.0, 2.0, 3.0], [3.0, 2.0, 1.0], [4.0, 4.0, 4.0]]), + itemwise_values=np.array([0.5, 0.5, 0.5]), + assortment_size=2, + ) + + with pytest.raises(ValueError): + LatentClassPricingOptimizer( + solver="ortools", + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array( + [[[1.0, 1.1], [2.0, 2.1], [3.0, 3.1]], [[3.0, 3.1], [2.0, 2.1], [1.0, 1.1]]] + ), + itemwise_values=np.array([[0.5, 1.2, 2.4], [0.5, 1.2, 2.4], [0.5, 1.2, 2.4]]), + assortment_size=2, + ) + + with pytest.raises(ValueError): + LatentClassPricingOptimizer( + solver="ortools", + class_weights=np.array([0.2, 0.7, 0.1]), + class_utilities=np.array( + [[[1.0, 1.1], [2.0, 2.1], [3.0, 3.1]], [[3.0, 3.1], [2.0, 2.1], [1.0, 1.1]]] + ), + itemwise_values=np.array([[0.5, 1.2], [0.5, 1.2], [0.5, 1.2]]), + assortment_size=2, + ) + + with pytest.raises(ValueError): + LatentClassPricingOptimizer( + solver="ortools", + class_weights=np.array([0.2, 0.8]), + class_utilities=np.array( + [[[1.0, 1.1], [2.0, 2.1], [3.0, 3.1]], [[3.0, 3.1], [2.0, 2.1], [1.0, 1.1]]] + ), + itemwise_values=np.array([[0.5, 1.2], [0.5, 1.2], [0.5, 1.2], [0.5, 1.2]]), + assortment_size=2, + )