diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a7d0fc7b7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "esbonio.sphinx.confDir": "" +} \ No newline at end of file diff --git a/demo/demo_model.py b/demo/demo_model.py index 854755cf9..204076882 100644 --- a/demo/demo_model.py +++ b/demo/demo_model.py @@ -29,9 +29,9 @@ # Working example for MM1 model. # ----------------------------------------------- -from simopt.models.mm1queue import MM1Queue -fixed_factors = {"lambda": 3.0, "mu": 8.0} -mymodel = MM1Queue(fixed_factors) +from simopt.models.cntnv import CntNV +fixed_factors = {} +mymodel = CntNV(fixed_factors) # ----------------------------------------------- # The rest of this script requires no changes. diff --git a/demo/demo_problem.py b/demo/demo_problem.py index 57145a36d..f72df7dbd 100644 --- a/demo/demo_problem.py +++ b/demo/demo_problem.py @@ -43,9 +43,9 @@ # Working example for CntNVMaxProfit problem. # ----------------------------------------------- from simopt.models.cntnv import CntNVMaxProfit -fixed_factors = {"initial_solution": (2,), "budget": 500} +fixed_factors = {} myproblem = CntNVMaxProfit(fixed_factors=fixed_factors) -x = (3,) +x = (5, 5, 5, 5) mysolution = Solution(x, myproblem) # ----------------------------------------------- diff --git a/demo/demo_problem_solver.py b/demo/demo_problem_solver.py index dc44f4ae5..fe2504a34 100644 --- a/demo/demo_problem_solver.py +++ b/demo/demo_problem_solver.py @@ -36,10 +36,10 @@ print(f"Results will be stored as {file_name_path}.") # Initialize an instance of the experiment class. -myexperiment = ProblemSolver(solver_name, problem_name) +myexperiment = ProblemSolver(solver_name, problem_name, solver_fixed_factors={"sample_size": 10}) # Run a fixed number of macroreplications of the solver on the problem. -myexperiment.run(n_macroreps=20) +myexperiment.run(n_macroreps=5) # If the solver runs have already been performed, uncomment the # following pair of lines (and uncommmen the myexperiment.run(...) @@ -48,9 +48,9 @@ print("Post-processing results.") # Run a fixed number of postreplications at all recommended solutions. -myexperiment.post_replicate(n_postreps=200) +myexperiment.post_replicate(n_postreps=20) # Find an optimal solution x* for normalization. -post_normalize([myexperiment], n_postreps_init_opt=200) +post_normalize([myexperiment], n_postreps_init_opt=20) # Log results. myexperiment.log_experiment_results() diff --git a/docs/cntnv.rst b/docs/cntnv.rst index 8fb8f3931..ef2d293f5 100644 --- a/docs/cntnv.rst +++ b/docs/cntnv.rst @@ -5,45 +5,158 @@ Model: Continuous Newsvendor Problem (CNTNV) Description: ------------ -A vendor orders a fixed quantity of liquid at the beginning of a day to be -sold to customers throughout the day. The vendor pays a per-unit order cost -:math:`c` for the initial inventory and sells it the product to customers at a per-unit price -:math:`s`. At the end of the day, any unsold liquid can be salvaged at a per-unit price, :math:`w`. +A vendor orders a fixed quantity of materials at the beginning of a day. A per-unit-material +cost :math:`c_{m}` is assessed for each type of material. As the demand is observed, the vendor +optionally orders recourse material at a premium price :math:`c_{r}` and processes all materials +into a variety of finished goods in a manner that maximizes profit. The finished goods are sold +to customers at price :math:`p_{s}`. At the end of the day, any unprocessed materials can be +salvaged at price :math:`p_{v}`. Sources of Randomness: ---------------------- -Each day's random demand for liquid product follows Burr Type XII distribution and is denoted by :math:`D`. -The parameters of the Burr Type XII distribution are :math:`α` and :math:`β` so that its cumulative -distribution function is given by :math:`F(x) = 1 - (1+x^α)^{-β}` where :math:`x, α,` and -:math:`β` are all positive. +Each day's random demand for discrete product quantities follows Poisson distribution and +is denoted by :math:`D`. The parameters of the Poisson distribution is :math:`λ`. Its +cumulative distribution function is given by :math:`F(x; λ) = \sum_{k=0}^{x} \frac{e^{-λ}λ^k}{k!}` +where :math:`x` and :math:`λ` are positive. Model Factors: -------------- -* Cost (:math:`c`): The price at which the newsvendor purchases one unit volume of liquid. +* num_material (:math:`n_{m}`): The number of material types. - * Default: 5 + * Default: 4 -* Price (:math:`s`): The price at which the newsvendor sells one unit volume of liquid. +* num_product (:math:`n_{p}`): The number of product types. - * Default: 9 + * Default: 3 -* Salvage Price (:math:`w`): The price at which any unsold liquid is sold for salvage. +* mat_to_prod (:math:`A`): :math:`n_{p}` by :math:`n_{m}` matrix, mapping :math:`n_{m}` materials to :math:`n_{p}` products. - * Default: 1 + * Default: [[1, 2, 1, 3], [1, 1, 3, 1], [2, 0, 4, 1]] -* Alpha (:math:`α`): Parameter for the demand distribution. +* material_cost (:math:`c_{m}`): Purchasing cost per material unit. :math:`n_{m}` sized array. - * Default: 2 + * Default: [1, 1, 1, 1] -* Beta (:math:`β`): Parameter for the demand distribution. +* recourse_cost (:math:`c_{r}`): Recourse purchasing cost per material unit. :math:`n_{m}` sized array. + + * Default: [2, 2, 2, 2] + +* process_cost (:math:`c_{p}`): Processing cost per product unit. :math:`n_{p}` sized array. + + * Default: [0.1, 0.1, 0.1] + +* order_cost (:math:`c_{o}`): Fixed one-time ordering cost. * Default: 20 -* Quantity of Liquid (:math:`x`): Amount (volume) of liquid ordered at the beginning of the day. +* purchase_yield (:math:`Y`): Yield rates for initially purchased materials. :math:`n_{m}` sized array. + + * Default: [0.9, 0.9, 0.9, 0.9] + +* total_budget (:math:`b`): Total budget for all materials, processing, ordering and recourse purchases. + + * Default: 600 + +* sales_price (:math:`p_{s}`): Sales price per product unit. :math:`n_{p}` sized array. + + * Default: [12, 12, 12] + +* salvage_price (:math:`p_{v}`): Salvage price per material unit. :math:`n_{m}` sized array. + + * Default: [0.6, 0.6, 0.6, 0.6] + +* order_quantity (:math:`x`): Initial order quantity per material. :math:`n_{m}` sized array. + + * Default: [20, 20, 20, 20] + +* poi_mean (:math:`λ`): Mean parameter for demand's poisson distribution. :math:`n_{p}` sized array. + + * Default: [15, 15, 15] + +Model variations: +---------------------- + +This newsvendor model is adaptable to various cases of the traditional model. Adjustment of +model factors is necessary if simulating a particular case. + +1) Retail vendor (traditonal): + + The newsvendor purchases products and sells them directly to the customers. There + is no production involved. The decision variable in this problem is order quantity + of *products*. This variation is subject to below factor settings and intepretations: + + * :math:`n_{m}` = :math:`n_{p}` (there can be multiple products) + * :math:`A` = identity matrix of size :math:`n_{p}` + * :math:`c_{m}` = cost of each product type + * :math:`c_{r}` = recourse cost of each product type + * :math:`p_{v}` = salvage price of each product type + * :math:`Y` = yield rates of each product type - * Default: 0.5 +2) Factory vendor: + + Initially, the newsvendor purchases materials instead of products. After demand is + observed, materials are processed into products. This is done using an integer program + with an objective of maximizing profit. Additionally, in this variation, materials + can be salvaged but products can not be salvaged. The decision variable in this problem + is order quantity of *materials*. This variation is subject to below factor settings + and intepretations: + + * :math:`n_{m}` :math:`⊥` :math:`n_{p}`. Independence between the number of materials and the number of products + * :math:`A` = Resource requirements to produce each product. :math:`n_{p}` by :math:`n_{m}` matrix. + +3) Multi-product: + + Either in a factory vendor or retail vendor setting, there can exist multiple products + to be procured/produced and sold to customers. Previous constraints from retail or factory + models still hold. This variation is subject to below factor settings and intepretations: + + * :math:`n_{p}` > 1 + * len(:math:`c_{p}`) > 1 + * len(:math:`p_{s}`) > 1 + +4) Single-product: + + Similarly, a factory vendor or a retail vendor can sell one product only. Note that a single- + product retail vendor presents the same senario as the traditional newsvendor problem. A single- + product factory vendor can have multiple materials or one single material, in which case it will + behave as the traditional newsvendor problem. This variation is subject to below factor settings + and intepretations: + + * :math:`n_{p}` = 1 + * :math:`c_{p}` still exists as an array. len(:math:`c_{p}`) = 1. + * :math:`p_{s}` still exists as an array. len(:math:`p_{s}`) = 1. + * :math:`λ` stil exists as an array. len(:math:`λ`) = 1. + + In other words, the factors' datatype should not alter, regardless of the model being the + multi-product/single-product or retail/factory. + +5) Recourse: + + The recourse case does not change the nature of newsvendor's operations. It is an additional + feature can co-exist with all previous variations of the model. Recourse refers to the newsvendor's + decision to procure materials/products after demand is observed. Economically speaking, a vendor + would chooses to do so only if the return is worth the premium prices paid for the recourse. + + Recourse is disabled by default. In other words, its default input is None. + + In the retail newsvendor setting, the recourse refers to product recourse. + + In the factory newsvendor setting, the recourse refers to material recourse. + + In either case, len(:math:`c_{r}`) = :math:`n_{m}`. + + +6) Random yield: + + This case refers to indepedent yields of procurement. Such applies to material in the factory + newsvendor and products in the retail newsvendor. The randomness of yields comes from a binomial + distribution using a random number generator. Yield rates (in array form) are inputed as a model + factor. This variation is subject to below factor setting: + + * len(:math:`Y`) = :math:`n_{m}` + * 0 < :math:`Y_{i}` <= 1, for all :math:`Y_{i}` in :math:`Y`. Responses: ---------- @@ -57,6 +170,13 @@ Evan L. Porteus. Stochastic inventory theory. In D. P. Heyman and M. J. Sobel, e Stochastic Models, volume 2 of Handbooks in Operations Research and Management Science, chapter 12, pages 605–652. Elsevier, New York, 1990. +Gallego, G., & Moon, I. (1993). The distribution free newsboy problem: Review and extensions. +The Journal of the Operational Research Society, 44(8), 825. +https://doi.org/10.2307/2583894 + +Ding, X., Puterman, M. L., & Bisi, A. (2002). The censored Newsvendor and the optimal +acquisition of Information. Operations Research, 50(3), 517–527. +https://doi.org/10.1287/opre.50.3.517.7752 Optimization Problem: Maximize Profit ===================================== @@ -64,7 +184,7 @@ Optimization Problem: Maximize Profit Decision Variables: ------------------- -* Quantity of Liquid (:math:`x`): Amount (volume) of liquid ordered at the beginning of the day. +* order_quantity (:math:`x`): Amount of raw material to be purchased at the beginning of the day. :math:`n_{m}` sized array. Objectives: ----------- @@ -74,14 +194,16 @@ Maximizes the vendor's expected profit. Constraints: ------------ -Quantity of Liquid must be non-negative: :math:`x > 0` +order_quantity must be an array of non-negative integers + +* :math:`x_{i}` >= 0, for all :math:`x_{i}` in :math:`x`. Problem Factors: ---------------- * Budget: Max # of replications for a solver to take. - * Default: 1000 + * Default: 3000 Fixed Model Factors: -------------------- @@ -91,21 +213,21 @@ Fixed Model Factors: Starting Solution: ------------------ -* :math:`x = 0` +* [40, 40, 100, 60] Random Solutions: ----------------- -If random solutions are needed, generate :math:`x` from an Exponential distribution with mean 1. +If random solutions are needed, generate :math:`x` from continous_random_vector_from_simplex +function in mrg32k3a. Optimal Solution: ----------------- -Global minimum at :math:`x^* = (1/((1-r)^{1/β})-1)^{1/α}`. -For the default factors, the optimal solution is :math:`x^*` = 0.1878. +* [82, 60, 144, 115] Optimal Objective Function Value: --------------------------------- -For the default factors, the maximum expected profit is 0.4635. +For the default factors, the maximum expected profit is 343.19 \ No newline at end of file diff --git a/docs/simopt.models.rst b/docs/simopt.models.rst index 8fd61c4ae..9c6c89937 100644 --- a/docs/simopt.models.rst +++ b/docs/simopt.models.rst @@ -52,6 +52,14 @@ simopt.models.dynamnews module :undoc-members: :show-inheritance: +simopt.models.example module +---------------------------- + +.. automodule:: simopt.models.example + :members: + :undoc-members: + :show-inheritance: + simopt.models.facilitysizing module ----------------------------------- diff --git a/experiments/logs/RNDSRCH_on_CNTNEWS-1_experiment_results.txt b/experiments/logs/RNDSRCH_on_CNTNEWS-1_experiment_results.txt index 599d32a6f..f788edb8b 100644 --- a/experiments/logs/RNDSRCH_on_CNTNEWS-1_experiment_results.txt +++ b/experiments/logs/RNDSRCH_on_CNTNEWS-1_experiment_results.txt @@ -3,196 +3,83 @@ Problem: CNTNEWS-1 Solver: RNDSRCH Model Factors: - purchase_price: 5.0 - sales_price: 9.0 - salvage_price: 1.0 - Burr_c: 2.0 - Burr_k: 20.0 + num_material: 4 + num_product: 3 + mat_to_prod: [[1, 2, 1, 3], [1, 1, 3, 1], [2, 0, 4, 1]] + material_cost: [1.0, 1.0, 1.0, 1.0] + recourse_cost: [2.0, 2.0, 2.0, 2.0] + process_cost: [0.1, 0.1, 0.1] + order_cost: 20 + purchase_yield: [0.9, 0.9, 0.9, 0.9] + total_budget: 600 + sales_price: [12.0, 12.0, 12.0] + salvage_price: [0.6, 0.6, 0.6, 0.6] + poi_mean: [15.0, 15.0, 15.0] Problem Factors: - initial_solution: (0,) - budget: 1000 + initial_solution: [40, 40, 100, 60] + budget: 3000 Solver Factors: - crn_across_solns: True sample_size: 10 + crn_across_solns: True -20 macroreplications were run. -200 postreplications were run at each recommended solution. +5 macroreplications were run. +20 postreplications were run at each recommended solution. -The initial solution is (0,). Its estimated objective is 0.0. -The proxy optimal solution is (0.1878,). Its estimated objective is 0.5103. -200 postreplications were taken at x0 and x_star. +The initial solution is (40, 40, 100, 60). Its estimated objective is 276.9883. +The proxy optimal solution is (82, 60, 144, 115). Its estimated objective is 343.1883. +20 postreplications were taken at x0 and x_star. Macroreplication Results: Macroreplication 1: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 50 Recommended Solution: (0.3221,) Estimated Objective: 0.2437 - Budget: 110 Recommended Solution: (0.1352,) Estimated Objective: 0.4186 - Budget: 810 Recommended Solution: (0.176,) Estimated Objective: 0.448 - Budget: 990 Recommended Solution: (0.2143,) Estimated Objective: 0.4307 - Budget: 1000 Recommended Solution: (0.2143,) Estimated Objective: 0.4307 - The time taken to complete this macroreplication was 0.07 s. + Budget: 0 Recommended Solution: (40, 40, 100, 60) Estimated Objective: 272.9633 + Budget: 40 Recommended Solution: (75, 183, 128, 75) Estimated Objective: 281.4267 + Budget: 340 Recommended Solution: (65, 55, 132, 73) Estimated Objective: 329.1833 + Budget: 3000 Recommended Solution: (65, 55, 132, 73) Estimated Objective: 329.1833 + The time taken to complete this macroreplication was 144.63 s. Macroreplication 2: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.0336,) Estimated Objective: 0.1338 - Budget: 30 Recommended Solution: (0.2044,) Estimated Objective: 0.5023 - Budget: 820 Recommended Solution: (0.1986,) Estimated Objective: 0.505 - Budget: 1000 Recommended Solution: (0.1986,) Estimated Objective: 0.505 - The time taken to complete this macroreplication was 0.04 s. + Budget: 0 Recommended Solution: (40, 40, 100, 60) Estimated Objective: 272.6417 + Budget: 20 Recommended Solution: (89, 47, 325, 74) Estimated Objective: 270.91 + Budget: 160 Recommended Solution: (95, 74, 234, 34) Estimated Objective: 274.925 + Budget: 180 Recommended Solution: (82, 74, 269, 112) Estimated Objective: 293.725 + Budget: 320 Recommended Solution: (181, 77, 153, 110) Estimated Objective: 298.05 + Budget: 430 Recommended Solution: (73, 80, 192, 122) Estimated Objective: 321.12 + Budget: 820 Recommended Solution: (68, 76, 162, 129) Estimated Objective: 330.675 + Budget: 3000 Recommended Solution: (68, 76, 162, 129) Estimated Objective: 330.675 + The time taken to complete this macroreplication was 141.34 s. Macroreplication 3: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.0689,) Estimated Objective: 0.2616 - Budget: 130 Recommended Solution: (0.2153,) Estimated Objective: 0.4271 - Budget: 1000 Recommended Solution: (0.2153,) Estimated Objective: 0.4271 - The time taken to complete this macroreplication was 0.04 s. + Budget: 0 Recommended Solution: (40, 40, 100, 60) Estimated Objective: 273.795 + Budget: 110 Recommended Solution: (122, 90, 74, 110) Estimated Objective: 289.3683 + Budget: 120 Recommended Solution: (71, 214, 160, 109) Estimated Objective: 299.9483 + Budget: 260 Recommended Solution: (55, 101, 107, 147) Estimated Objective: 286.8017 + Budget: 340 Recommended Solution: (75, 77, 178, 187) Estimated Objective: 316.7083 + Budget: 580 Recommended Solution: (37, 74, 155, 142) Estimated Objective: 318.6333 + Budget: 670 Recommended Solution: (69, 63, 255, 98) Estimated Objective: 325.8683 + Budget: 930 Recommended Solution: (51, 114, 129, 111) Estimated Objective: 317.3517 + Budget: 1130 Recommended Solution: (98, 28, 134, 111) Estimated Objective: 337.4483 + Budget: 1470 Recommended Solution: (63, 57, 138, 164) Estimated Objective: 333.0883 + Budget: 3000 Recommended Solution: (63, 57, 138, 164) Estimated Objective: 333.0883 + The time taken to complete this macroreplication was 142.0 s. Macroreplication 4: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 60 Recommended Solution: (0.1423,) Estimated Objective: 0.4584 - Budget: 80 Recommended Solution: (0.1985,) Estimated Objective: 0.4999 - Budget: 830 Recommended Solution: (0.1998,) Estimated Objective: 0.4997 - Budget: 1000 Recommended Solution: (0.1998,) Estimated Objective: 0.4997 - The time taken to complete this macroreplication was 0.06 s. + Budget: 0 Recommended Solution: (40, 40, 100, 60) Estimated Objective: 274.22 + Budget: 20 Recommended Solution: (8, 40, 209, 128) Estimated Objective: 257.9117 + Budget: 30 Recommended Solution: (71, 74, 168, 148) Estimated Objective: 305.9567 + Budget: 1240 Recommended Solution: (101, 73, 145, 74) Estimated Objective: 318.2717 + Budget: 3000 Recommended Solution: (101, 73, 145, 74) Estimated Objective: 318.2717 + The time taken to complete this macroreplication was 145.11 s. Macroreplication 5: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.0962,) Estimated Objective: 0.328 - Budget: 170 Recommended Solution: (0.1647,) Estimated Objective: 0.414 - Budget: 1000 Recommended Solution: (0.1647,) Estimated Objective: 0.414 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 6: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.3515,) Estimated Objective: 0.1751 - Budget: 100 Recommended Solution: (0.2854,) Estimated Objective: 0.3672 - Budget: 120 Recommended Solution: (0.088,) Estimated Objective: 0.3236 - Budget: 180 Recommended Solution: (0.1055,) Estimated Objective: 0.374 - Budget: 320 Recommended Solution: (0.1554,) Estimated Objective: 0.4634 - Budget: 1000 Recommended Solution: (0.1554,) Estimated Objective: 0.4634 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 7: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 40 Recommended Solution: (0.2848,) Estimated Objective: 0.3506 - Budget: 60 Recommended Solution: (0.2603,) Estimated Objective: 0.3997 - Budget: 360 Recommended Solution: (0.2186,) Estimated Objective: 0.4617 - Budget: 640 Recommended Solution: (0.2395,) Estimated Objective: 0.4361 - Budget: 1000 Recommended Solution: (0.2395,) Estimated Objective: 0.4361 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 8: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.3076,) Estimated Objective: 0.2585 - Budget: 40 Recommended Solution: (0.126,) Estimated Objective: 0.4127 - Budget: 70 Recommended Solution: (0.1689,) Estimated Objective: 0.4576 - Budget: 290 Recommended Solution: (0.18,) Estimated Objective: 0.4562 - Budget: 1000 Recommended Solution: (0.18,) Estimated Objective: 0.4562 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 9: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.0839,) Estimated Objective: 0.299 - Budget: 30 Recommended Solution: (0.3159,) Estimated Objective: 0.2053 - Budget: 110 Recommended Solution: (0.184,) Estimated Objective: 0.4444 - Budget: 230 Recommended Solution: (0.1955,) Estimated Objective: 0.4406 - Budget: 280 Recommended Solution: (0.211,) Estimated Objective: 0.4297 - Budget: 1000 Recommended Solution: (0.211,) Estimated Objective: 0.4297 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 10: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.4531,) Estimated Objective: -0.2439 - Budget: 50 Recommended Solution: (0.0709,) Estimated Objective: 0.2583 - Budget: 100 Recommended Solution: (0.1869,) Estimated Objective: 0.4311 - Budget: 120 Recommended Solution: (0.2001,) Estimated Objective: 0.4276 - Budget: 260 Recommended Solution: (0.2638,) Estimated Objective: 0.3458 - Budget: 660 Recommended Solution: (0.2346,) Estimated Objective: 0.393 - Budget: 1000 Recommended Solution: (0.2346,) Estimated Objective: 0.393 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 11: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 80 Recommended Solution: (0.3475,) Estimated Objective: 0.2333 - Budget: 100 Recommended Solution: (0.0604,) Estimated Objective: 0.2311 - Budget: 110 Recommended Solution: (0.2576,) Estimated Objective: 0.4471 - Budget: 200 Recommended Solution: (0.1665,) Estimated Objective: 0.4813 - Budget: 1000 Recommended Solution: (0.1665,) Estimated Objective: 0.4813 - The time taken to complete this macroreplication was 0.05 s. - -Macroreplication 12: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 40 Recommended Solution: (0.3284,) Estimated Objective: 0.1714 - Budget: 50 Recommended Solution: (0.1162,) Estimated Objective: 0.377 - Budget: 110 Recommended Solution: (0.1235,) Estimated Objective: 0.3902 - Budget: 570 Recommended Solution: (0.1603,) Estimated Objective: 0.4316 - Budget: 880 Recommended Solution: (0.142,) Estimated Objective: 0.4155 - Budget: 1000 Recommended Solution: (0.142,) Estimated Objective: 0.4155 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 13: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.4153,) Estimated Objective: -0.0834 - Budget: 40 Recommended Solution: (0.1806,) Estimated Objective: 0.4732 - Budget: 180 Recommended Solution: (0.2345,) Estimated Objective: 0.4486 - Budget: 410 Recommended Solution: (0.2097,) Estimated Objective: 0.4708 - Budget: 1000 Recommended Solution: (0.2097,) Estimated Objective: 0.4708 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 14: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 90 Recommended Solution: (0.4495,) Estimated Objective: -0.1594 - Budget: 110 Recommended Solution: (0.4416,) Estimated Objective: -0.1297 - Budget: 120 Recommended Solution: (0.432,) Estimated Objective: -0.0941 - Budget: 130 Recommended Solution: (0.2455,) Estimated Objective: 0.441 - Budget: 470 Recommended Solution: (0.2396,) Estimated Objective: 0.4492 - Budget: 1000 Recommended Solution: (0.2396,) Estimated Objective: 0.4492 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 15: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 40 Recommended Solution: (0.09,) Estimated Objective: 0.3166 - Budget: 190 Recommended Solution: (0.1766,) Estimated Objective: 0.4405 - Budget: 420 Recommended Solution: (0.176,) Estimated Objective: 0.4403 - Budget: 1000 Recommended Solution: (0.176,) Estimated Objective: 0.4403 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 16: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 80 Recommended Solution: (0.2815,) Estimated Objective: 0.3508 - Budget: 160 Recommended Solution: (0.1904,) Estimated Objective: 0.4524 - Budget: 1000 Recommended Solution: (0.1904,) Estimated Objective: 0.4524 - The time taken to complete this macroreplication was 0.05 s. - -Macroreplication 17: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.2564,) Estimated Objective: 0.3554 - Budget: 30 Recommended Solution: (0.2685,) Estimated Objective: 0.3295 - Budget: 1000 Recommended Solution: (0.2685,) Estimated Objective: 0.3295 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 18: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.1617,) Estimated Objective: 0.4272 - Budget: 290 Recommended Solution: (0.1379,) Estimated Objective: 0.41 - Budget: 530 Recommended Solution: (0.1508,) Estimated Objective: 0.4218 - Budget: 1000 Recommended Solution: (0.1508,) Estimated Objective: 0.4218 - The time taken to complete this macroreplication was 0.04 s. - -Macroreplication 19: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.222,) Estimated Objective: 0.4764 - Budget: 250 Recommended Solution: (0.242,) Estimated Objective: 0.4545 - Budget: 270 Recommended Solution: (0.2557,) Estimated Objective: 0.4356 - Budget: 1000 Recommended Solution: (0.2557,) Estimated Objective: 0.4356 - The time taken to complete this macroreplication was 0.06 s. - -Macroreplication 20: - Budget: 0 Recommended Solution: (0,) Estimated Objective: 0.0 - Budget: 20 Recommended Solution: (0.0805,) Estimated Objective: 0.3047 - Budget: 40 Recommended Solution: (0.1932,) Estimated Objective: 0.4968 - Budget: 1000 Recommended Solution: (0.1932,) Estimated Objective: 0.4968 - The time taken to complete this macroreplication was 0.04 s. + Budget: 0 Recommended Solution: (40, 40, 100, 60) Estimated Objective: 274.4983 + Budget: 80 Recommended Solution: (98, 22, 130, 163) Estimated Objective: 303.25 + Budget: 100 Recommended Solution: (75, 51, 208, 142) Estimated Objective: 322.99 + Budget: 1230 Recommended Solution: (84, 73, 111, 124) Estimated Objective: 325.09 + Budget: 1850 Recommended Solution: (86, 57, 189, 71) Estimated Objective: 338.19 + Budget: 2490 Recommended Solution: (86, 64, 183, 82) Estimated Objective: 342.84 + Budget: 2730 Recommended Solution: (82, 60, 144, 115) Estimated Objective: 350.33 + Budget: 3000 Recommended Solution: (82, 60, 144, 115) Estimated Objective: 350.33 + The time taken to complete this macroreplication was 143.34 s. diff --git a/experiments/outputs/RNDSRCH_on_CNTNEWS-1.pickle b/experiments/outputs/RNDSRCH_on_CNTNEWS-1.pickle index cf7bbd114..431338f17 100644 Binary files a/experiments/outputs/RNDSRCH_on_CNTNEWS-1.pickle and b/experiments/outputs/RNDSRCH_on_CNTNEWS-1.pickle differ diff --git a/experiments/plots/SOLVER_SET_on_CNTNEWS-1_0.9_quantile_prog_curve_unnorm.png b/experiments/plots/SOLVER_SET_on_CNTNEWS-1_0.9_quantile_prog_curve_unnorm.png index 9e5990fd1..78cf3c7fd 100644 Binary files a/experiments/plots/SOLVER_SET_on_CNTNEWS-1_0.9_quantile_prog_curve_unnorm.png and b/experiments/plots/SOLVER_SET_on_CNTNEWS-1_0.9_quantile_prog_curve_unnorm.png differ diff --git a/experiments/plots/SOLVER_SET_on_CNTNEWS-1_all_prog_curves_unnorm.png b/experiments/plots/SOLVER_SET_on_CNTNEWS-1_all_prog_curves_unnorm.png index 15fd3b3d1..a1fcb872b 100644 Binary files a/experiments/plots/SOLVER_SET_on_CNTNEWS-1_all_prog_curves_unnorm.png and b/experiments/plots/SOLVER_SET_on_CNTNEWS-1_all_prog_curves_unnorm.png differ diff --git a/experiments/plots/SOLVER_SET_on_CNTNEWS-1_cdf_0.1_solve_times.png b/experiments/plots/SOLVER_SET_on_CNTNEWS-1_cdf_0.1_solve_times.png index 22f0feeed..244bebd87 100644 Binary files a/experiments/plots/SOLVER_SET_on_CNTNEWS-1_cdf_0.1_solve_times.png and b/experiments/plots/SOLVER_SET_on_CNTNEWS-1_cdf_0.1_solve_times.png differ diff --git a/experiments/plots/SOLVER_SET_on_CNTNEWS-1_mean_prog_curve_unnorm.png b/experiments/plots/SOLVER_SET_on_CNTNEWS-1_mean_prog_curve_unnorm.png index deabeb66c..bf82865c5 100644 Binary files a/experiments/plots/SOLVER_SET_on_CNTNEWS-1_mean_prog_curve_unnorm.png and b/experiments/plots/SOLVER_SET_on_CNTNEWS-1_mean_prog_curve_unnorm.png differ diff --git a/simopt/models/cntnv.py b/simopt/models/cntnv.py index 5aabc45a2..bd42ab944 100644 --- a/simopt/models/cntnv.py +++ b/simopt/models/cntnv.py @@ -2,17 +2,18 @@ Summary ------- Simulate a day's worth of sales for a newsvendor. -A detailed description of the model/problem can be found `here `_. +A detailed description of the model/problem can be found `here +`_. """ import numpy as np - +import pulp from ..base import Model, Problem class CntNV(Model): """ A model that simulates a day's worth of sales for a newsvendor - with a Burr Type XII demand distribution. Returns the profit, after + with a exponential demand distribution. Returns the profit, after accounting for order costs and salvage. Attributes @@ -42,75 +43,165 @@ class CntNV(Model): def __init__(self, fixed_factors=None): if fixed_factors is None: fixed_factors = {} - self.name = "CNTNEWS" + self.name = "CNTNV" self.n_rngs = 1 self.n_responses = 1 self.factors = fixed_factors self.specifications = { - "purchase_price": { - "description": "purchasing cost per unit", - "datatype": float, - "default": 5.0 + "num_material": { + "description": "number of material types", + "datatype": int, + "default": 4 + }, + "num_product": { + "description": "number of product types", + "datatype": int, + "default": 3 + }, + "mat_to_prod": { + "description": "PxR matrix, mapping M material to P products", + "datatype": list, + "default": [[1, 2, 1, 3], + [1, 1, 3, 1], + [2, 0, 4, 1]] + }, + "material_cost": { + "description": "purchasing cost per material unit", + "datatype": list, + "default": list(1 * np.ones(4)) + }, + "recourse_cost": { + "description": "recourse purchasing cost per material unit", + "datatype": list, + "default": list(2 * np.ones(4)) + }, + "process_cost": { + "description": "processing cost per product", + "datatype": list, + "default": list(0.1 * np.ones(3)) + }, + "order_cost": { + "description": "fixed one-time ordering cost", + "datatype": int, + "default": 20 + }, + "purchase_yield": { + "description": "yield rate of purchased materials", + "datatype": list, + "default": list(0.9 * np.ones(4)) + }, + "total_budget": { + "description": "total budget for newsvendor's operation", + "datatype": int, + "default": 600 }, "sales_price": { - "description": "sales price per unit", - "datatype": float, - "default": 9.0 + "description": "sales price per product unit", + "datatype": list, + "default": list(12 * np.ones(3)) }, "salvage_price": { - "description": "salvage cost per unit", - "datatype": float, - "default": 1.0 + "description": "salvage price per material unit", + "datatype": list, + "default": list(0.6 * np.ones(4)) }, "order_quantity": { - "description": "order quantity", - "datatype": float, # or int - "default": 0.5 + "description": "initial order quantity per material", + "datatype": list, + "default": list(20 * np.ones(4)) }, - "Burr_c": { - "description": "Burr Type XII cdf shape parameter", - "datatype": float, - "default": 2.0 - }, - "Burr_k": { - "description": "Burr Type XII cdf shape parameter", - "datatype": float, - "default": 20.0 + "poi_mean": { + "description": "parameter for poisson demand distribution", + "datatype": list, + "default": list(15 * np.ones(3)) + } } - } self.check_factor_list = { - "purchase_price": self.check_purchase_price, + "num_material": self.check_num_material, + "num_product": self.check_num_product, + "mat_to_prod": self.check_mat_to_prod, + "material_cost": self.check_material_cost, + "process_cost": self.check_process_cost, + "order_cost": self.check_order_cost, + "recourse_cost": self.check_recourse_cost, + "purchase_yield": self.check_purchase_yield, + "total_budget": self.check_total_budget, "sales_price": self.check_sales_price, "salvage_price": self.check_salvage_price, "order_quantity": self.check_order_quantity, - "Burr_c": self.check_Burr_c, - "Burr_k": self.check_Burr_k + "poi_mean": self.check_poi_mean } # Set factors of the simulation model. super().__init__(fixed_factors) - def check_purchase_price(self): - return self.factors["purchase_price"] > 0 + def check_num_material(self): + return (self.factors["num_material"] > 0) + + def check_num_product(self): + return (self.factors["num_product"] > 0) + + def check_mat_to_prod(self): + return (np.shape(self.factors["mat_to_prod"])[0] == + self.factors["num_product"]) \ + and (np.shape(self.factors["mat_to_prod"])[1] == + self.factors["num_material"]) + + def check_material_cost(self): + return all(np.array(self.factors["material_cost"]) > 0) \ + and (len(self.factors["material_cost"]) == + self.factors["num_material"]) + + def check_recourse_cost(self): + if self.factors["recourse_cost"] is not None: + return all(np.array(self.factors["recourse_cost"]) > 0) \ + and (len(self.factors["recourse_cost"]) == + self.factors["num_material"]) + else: + return True + + def check_process_cost(self): + return all(np.array(self.factors["process_cost"]) >= 0) \ + and (len(self.factors["process_cost"]) == + self.factors["num_product"]) + + def check_order_cost(self): + return (self.factors["order_cost"] >= 0) + + def check_purchase_yield(self): + return all(np.array(self.factors["purchase_yield"]) <= 1) \ + and (len(self.factors["purchase_yield"]) == + self.factors["num_material"]) + + def check_total_budget(self): + return (np.dot(self.factors["order_quantity"], self.factors["material_cost"]) + <= self.factors["total_budget"]) def check_sales_price(self): - return self.factors["sales_price"] > 0 + return all(np.array(self.factors["sales_price"]) > 0) \ + and (len(self.factors["sales_price"]) == + self.factors["num_product"]) def check_salvage_price(self): - return self.factors["salvage_price"] > 0 + return all(np.array(self.factors["salvage_price"]) >= 0) \ + and (len(self.factors["salvage_price"]) == + self.factors["num_material"]) def check_order_quantity(self): - return self.factors["order_quantity"] > 0 - - def check_Burr_c(self): - return self.factors["Burr_c"] > 0 + return all(np.array(self.factors["order_quantity"]) > 0) \ + and (len(self.factors["order_quantity"]) == + self.factors["num_material"]) \ + and (np.issubdtype(self.factors["order_quantity"].dtype, np.integer)) - def check_Burr_k(self): - return self.factors["Burr_k"] > 0 + def check_poi_mean(self): + return all(np.array(self.factors["poi_mean"]) > 0) \ + and (len(self.factors["poi_mean"]) == + self.factors["num_product"]) \ + and (np.issubdtype(self.factors["poi_mean"].dtype, np.integer)) def check_simulatable_factors(self): - return (self.factors["salvage_price"] - < self.factors["purchase_price"] - < self.factors["sales_price"]) + return all((self.factors["sales_price"] - + np.dot(self.factors["mat_to_prod"], + self.factors["material_cost"])) > 0) def replicate(self, rng_list): """ @@ -129,40 +220,89 @@ def replicate(self, rng_list): "stockout_qty" = amount by which demand exceeded supply "stockout" = was there unmet demand? (Y/N) """ + # Designate random number generator for demand variability. demand_rng = rng_list[0] - # Generate random demand according to Burr Type XII distribution. - # If U ~ Uniform(0,1) and the Burr Type XII has parameters c and k, - # X = ((1-U)**(-1/k - 1))**(1/c) has the desired distribution. - base = ((1 - demand_rng.random())**(-1 / self.factors["Burr_k"]) - 1) - exponent = (1 / self.factors["Burr_c"]) - demand = base**exponent + demand = [] + for i in range(self.factors["num_product"]): + mul, n = np.exp(-(self.factors["poi_mean"])[i]), 0 + cdf, fact, power, u = mul, 1, 1, demand_rng.random() + while u > cdf: + n += 1 + fact, power = n*fact, power*(self.factors["poi_mean"])[i] + cdf += (power/fact)*mul + demand.append(n) + + # Generate binomial r.v for material levels based on yield rates + stock_material = [sum([1 for i in range(int(n)) + if demand_rng.random() < p]) + for p, n in zip(self.factors["purchase_yield"], + self.factors["order_quantity"])] + # If recourse cost is not specified, use an array of very large numbers + # This heuristically makes it impossible to buy any recourse material + if self.factors["recourse_cost"] is None: + self.factors["recourse_cost"] = [element * 10000 for + element in self.factors["material_cost"]] + # define IP problem for production of goods to fulfill realized demand + """ + X: amount of products produced for each product type + Y: recourse quantity for each material type + Objective: maximize profit => sum(sales) - sum(process cost) - sum(recourse cost) + """ + X = pulp.LpVariable.dicts("X", range(self.factors["num_product"]), + lowBound=0, cat='Integer') + Y = pulp.LpVariable.dicts("Y", range(self.factors["num_material"]), + lowBound=0, cat='Integer') + prob = pulp.LpProblem("Integer_Program", pulp.LpMaximize) + prob += pulp.lpSum(self.factors["sales_price"][i] * X[i] + for i in range(self.factors["num_product"])) - \ + pulp.lpSum([X[i] * self.factors["process_cost"][i] + for i in range(self.factors["num_product"])]) - \ + pulp.lpSum([Y[i] * self.factors["recourse_cost"][i] + for i in range(self.factors["num_material"])]) + # Define constraints + """ + C1: sum(recourse cost) + sum(process cost) <= total budget - sum(material cost) + C2: for each material, amount used <= (initial material + recourse quantity) + """ + prob += (pulp.lpSum([Y[i]*self.factors["recourse_cost"][i] + for i in range(self.factors["num_material"])]) + + pulp.lpSum([X[i]*self.factors["process_cost"][i] + for i in range(self.factors["num_product"])])) <= \ + self.factors["total_budget"] - np.dot(self.factors["order_quantity"], + self.factors["material_cost"]) + for j in range(self.factors["num_material"]): + prob += pulp.lpSum([self.factors["mat_to_prod"][i][j] * X[i] + for i in range(self.factors["num_product"])]) <= \ + stock_material[j] + Y[j] + for i in range(self.factors["num_product"]): + prob += X[i] <= demand[i] + prob.solve(pulp.apis.PULP_CBC_CMD(msg=False)) + + # Results of IP + Finish_Goods = np.array([pulp.value(X[i]) + for i in range(self.factors["num_product"])]) + Recourse = np.array([pulp.value(Y[i]) + for i in range(self.factors["num_material"])]) + Inventory = np.array(self.factors["order_quantity"]) + Recourse - \ + np.dot(np.linalg.pinv(self.factors["mat_to_prod"]), Finish_Goods) + # Calculate profit. - order_cost = (self.factors["purchase_price"] - * self.factors["order_quantity"]) - sales_revenue = (min(demand, self.factors["order_quantity"]) - * self.factors["sales_price"]) - salvage_revenue = (max(0, self.factors["order_quantity"] - demand) - * self.factors["salvage_price"]) - profit = sales_revenue + salvage_revenue - order_cost - stockout_qty = max(demand - self.factors["order_quantity"], 0) - stockout = int(stockout_qty > 0) - # Calculate gradient of profit w.r.t. order quantity. - if demand > self.factors["order_quantity"]: - grad_profit_order_quantity = (self.factors["sales_price"] - - self.factors["purchase_price"]) - elif demand < self.factors["order_quantity"]: - grad_profit_order_quantity = (self.factors["salvage_price"] - - self.factors["purchase_price"]) - else: - grad_profit_order_quantity = np.nan + total_cost = (np.dot(self.factors["order_quantity"], self.factors["material_cost"]) + + np.dot(Recourse, self.factors["recourse_cost"]) + + np.dot(Finish_Goods, self.factors["process_cost"]) + + self.factors["order_cost"]) + sales_revenue = np.dot([min(s, d) for s, d in zip(Finish_Goods, demand)], + self.factors["sales_price"]) + salvage_revenue = np.dot(Inventory, self.factors["salvage_price"]) + profit = sales_revenue + salvage_revenue - total_cost + + stockout_qty = [max(d - s, 0) for d, s in zip(demand, Finish_Goods)] + stockout = [1 if a > 0 else 0 for a in stockout_qty] # Compose responses and gradients. responses = {"profit": profit, "stockout_qty": stockout_qty, "stockout": stockout} - gradients = {response_key: - {factor_key: np.nan for factor_key in self.specifications} - for response_key in responses - } - gradients["profit"]["order_quantity"] = grad_profit_order_quantity + gradients = {response_key: {factor_key: np.nan for factor_key in self.specifications} + for response_key in responses} return responses, gradients @@ -244,36 +384,27 @@ def __init__(self, name="CNTNEWS-1", fixed_factors=None, model_fixed_factors=Non if model_fixed_factors is None: model_fixed_factors = {} self.name = name - self.dim = 1 self.n_objectives = 1 self.n_stochastic_constraints = 0 self.minmax = (1,) - self.constraint_type = "box" - self.variable_type = "continuous" - self.lower_bounds = (0,) - self.upper_bounds = (np.inf,) - self.gradient_available = True + self.constraint_type = "deterministic" + self.variable_type = "discrete" + self.gradient_available = False self.optimal_value = None - self.optimal_solution = None # (0.1878,) # TO DO: Generalize to function of factors. - self.model_default_factors = { - "purchase_price": 5.0, - "sales_price": 9.0, - "salvage_price": 1.0, - "Burr_c": 2.0, - "Burr_k": 20.0 - } + self.optimal_solution = None + self.model_default_factors = {} self.model_decision_factors = {"order_quantity"} self.factors = fixed_factors self.specifications = { "initial_solution": { "description": "initial solution", "datatype": tuple, - "default": (0,) + "default": [40, 40, 100, 60] }, "budget": { "description": "max # of replications for a solver to take", "datatype": int, - "default": 1000 + "default": 3000 } } self.check_factor_list = { @@ -283,6 +414,9 @@ def __init__(self, name="CNTNEWS-1", fixed_factors=None, model_fixed_factors=Non super().__init__(fixed_factors, model_fixed_factors) # Instantiate model with fixed factors and overwritten defaults. self.model = CntNV(self.model_fixed_factors) + self.dim = self.model.factors["num_material"] + self.lower_bounds = (0,)*self.dim + self.upper_bounds = (np.inf,)*self.dim def vector_to_factor_dict(self, vector): """ @@ -299,7 +433,7 @@ def vector_to_factor_dict(self, vector): dictionary with factor keys and associated values """ factor_dict = { - "order_quantity": vector[0] + "order_quantity": vector[:] } return factor_dict @@ -318,7 +452,7 @@ def factor_dict_to_vector(self, factor_dict): vector : tuple vector of values associated with decision variables """ - vector = (factor_dict["order_quantity"],) + vector = tuple(factor_dict["order_quantity"]) return vector def response_dict_to_objectives(self, response_dict): @@ -414,7 +548,9 @@ def check_deterministic_constraints(self, x): satisfies : bool indicates if solution `x` satisfies the deterministic constraints. """ - return x[0] > 0 + return super().check_deterministic_constraints(x) and ( + np.dot(x, self.model.factors["material_cost"]) < + self.model.factors["total_budget"]) def get_random_solution(self, rand_sol_rng): """ @@ -430,6 +566,12 @@ def get_random_solution(self, rand_sol_rng): x : tuple vector of decision variables """ - # Generate an Exponential(rate = 1) r.v. - x = (rand_sol_rng.expovariate(1),) + # Generate an r.v. vector. + # The result is rounded down to get discrete values. + x = (rand_sol_rng.continuous_random_vector_from_simplex( + n_elements=self.model.factors["num_material"], + summation=self.model.factors["total_budget"], + exact_sum=False, + weights=self.model.factors["material_cost"])) + x = [int(np.floor(i)) for i in x] return x