{
"cells": [
{
"cell_type": "markdown",
"id": "79fd77cf-63a6-412c-b67d-bfddad4c3ae3",
"metadata": {},
"source": [
"# Production mix - Model 6\n",
"\n",
"## Situation\n",
"You own a boutique pottery business, making and selling two types of large ornamental products called Lunar Orb and Solar Disc. Given constraints on staff hours, available materials, and product sales, your objective is to maximize the total profit margin from the shop.\n",
"\n",
"## Implementation\n",
"Linear Program (LP), using a Pyomo abstract model. The data is in a dat file and loaded into the model when the solver is called.\n",
"\n",
"## Source\n",
"Replicates an Excel model described in article \"Production mix via graphical LP\" at https://www.solvermax.com/blog/production-mix."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "8c764677-f466-4837-8810-d4e243f66082",
"metadata": {},
"outputs": [],
"source": [
"# Import dependencies\n",
"\n",
"import pyomo.environ as pyo\n",
"import pandas as pd"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "da332667-88df-4489-9190-a1e47e5c8e19",
"metadata": {},
"outputs": [],
"source": [
"# Declarations\n",
"\n",
"Model = pyo.AbstractModel()\n",
"\n",
"Model.Products = pyo.Set() # Pyomo Set rather than Python set\n",
"\n",
"Model.Name = pyo.Param(within = pyo.Any)\n",
"Model.Hours = pyo.Param(within = pyo.NonNegativeReals)\n",
"Model.kg = pyo.Param(within = pyo.NonNegativeReals)\n",
"Model.SalesLimit = pyo.Param(within = pyo.NonNegativeReals)\n",
"Model.VarInitial = pyo.Param(within = pyo.NonNegativeReals)\n",
"Model.VarLBounds = pyo.Param(within = pyo.NonNegativeReals)\n",
"Model.VarUBounds = pyo.Param(within = pyo.NonNegativeReals)\n",
"Model.Engine = pyo.Param(within = pyo.Any)\n",
"Model.TimeLimit = pyo.Param(within = pyo.NonNegativeReals)\n",
"Model.People = pyo.Param(Model.Products, within = pyo.NonNegativeReals) \n",
"Model.Materials = pyo.Param(Model.Products, within = pyo.NonNegativeReals)\n",
"Model.Sales = pyo.Param(Model.Products, within = pyo.Reals)\n",
"Model.Margin = pyo.Param(Model.Products, within = pyo.Reals)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "9f764559-df5e-4887-bd5a-7fc7e9ce34ae",
"metadata": {},
"outputs": [],
"source": [
"# Define model\n",
"\n",
"Model.Production = pyo.Var(Model.Products, domain = pyo.NonNegativeReals, initialize = Model.VarInitial, bounds = (Model.VarLBounds, Model.VarUBounds))\n",
"\n",
"def rule_hours(Model):\n",
" return sum(Model.People[p] * Model.Production[p] for p in Model.Products) <= Model.Hours\n",
"Model.PeopleHours = pyo.Constraint(rule = rule_hours)\n",
"\n",
"def rule_usage(Model):\n",
" return sum(Model.Materials[p] * Model.Production[p] for p in Model.Products) <= Model.kg\n",
"Model.MaterialUsage = pyo.Constraint(rule = rule_usage)\n",
"\n",
"def rule_sales(Model):\n",
" return sum(Model.Sales[p] * Model.Production[p] for p in Model.Products) <= Model.SalesLimit\n",
"Model.SalesRelationship = pyo.Constraint(rule = rule_sales)\n",
"\n",
"def rule_Obj(Model):\n",
" return sum(Model.Margin[p] * Model.Production[p] for p in Model.Products)\n",
"Model.TotalMargin = pyo.Objective(rule = rule_Obj, sense = pyo.maximize)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "48267cfd-61a7-4b17-87f4-c5fb30fabb5c",
"metadata": {},
"outputs": [],
"source": [
"# Solve model\n",
"\n",
"Instance = Model.create_instance(\"productiondata6.dat\")\n",
"Solver = pyo.SolverFactory(pyo.value(Instance.Engine))\n",
"if pyo.value(Instance.Engine) == 'cbc':\n",
" Solver.options['seconds'] = pyo.value(Instance.TimeLimit)\n",
"elif pyo.value(Instance.Engine) == 'glpk':\n",
" Solver.options['tmlim'] = pyo.value(Instance.TimeLimit)\n",
" \n",
"Instance.dual = pyo.Suffix(direction = pyo.Suffix.IMPORT)\n",
"\n",
"Results = Solver.solve(Instance, load_solutions = False, tee = False)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "a8f5f2ca-1fc6-4d08-b6eb-45f15128b75d",
"metadata": {},
"outputs": [],
"source": [
"# Process results\n",
"\n",
"WriteSolution = False\n",
"Optimal = False\n",
"LimitStop = False\n",
"Condition = Results.solver.termination_condition\n",
"\n",
"if Condition == pyo.TerminationCondition.optimal:\n",
" Optimal = True\n",
"if Condition == pyo.TerminationCondition.maxTimeLimit or Condition == pyo.TerminationCondition.maxIterations:\n",
" LimitStop = True\n",
"if Optimal or LimitStop:\n",
" try:\n",
" WriteSolution = True\n",
" Instance.solutions.load_from(Results)\n",
" SolverData = Results.Problem._list\n",
" SolutionLB = SolverData[0].lower_bound\n",
" SolutionUB = SolverData[0].upper_bound\n",
" except:\n",
" WriteSolution = False"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "af0535d4-9d6d-4e49-99a1-153a766215bb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Boutique pottery shop - Model 6 \n",
"\n",
"Status: optimal\n",
"Solver: cbc \n",
"\n",
"Total margin = $3,076.92\n",
"\n"
]
},
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Production | \n",
"
\n",
" \n",
" \n",
" \n",
" Discs | \n",
" 6.4103 | \n",
"
\n",
" \n",
" Orbs | \n",
" 12.8205 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Production\n",
"Discs 6.4103\n",
"Orbs 12.8205"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" lSlack | \n",
" uSlack | \n",
" Dual | \n",
"
\n",
" \n",
" \n",
" \n",
" PeopleHours | \n",
" inf | \n",
" 41.6667 | \n",
" -0.0000 | \n",
"
\n",
" \n",
" MaterialUsage | \n",
" inf | \n",
" -0.0000 | \n",
" 6.1538 | \n",
"
\n",
" \n",
" SalesRelationship | \n",
" inf | \n",
" -0.0000 | \n",
" 15.3846 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" lSlack uSlack Dual\n",
"PeopleHours inf 41.6667 -0.0000\n",
"MaterialUsage inf -0.0000 6.1538\n",
"SalesRelationship inf -0.0000 15.3846"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Write output\n",
"\n",
"print(pyo.value(Instance.Name), '\\n')\n",
"print('Status:', Results.solver.termination_condition)\n",
"print('Solver:', pyo.value(Instance.Engine), '\\n')\n",
"\n",
"if LimitStop: # Indicate how close we are to a solution\n",
" print('Objective bounds')\n",
" print('----------------')\n",
" if SolutionLB is None:\n",
" print('Lower: None')\n",
" else:\n",
" print(f'Lower: {SolutionLB:9,.2f}')\n",
" if SolutionUB is None:\n",
" print('Upper: None\\n')\n",
" else:\n",
" print(f'Upper: {SolutionUB:9,.2f}\\n')\n",
"if WriteSolution:\n",
" print(f'Total margin = ${Instance.TotalMargin():,.2f}\\n')\n",
" pd.options.display.float_format = \"{:,.4f}\".format\n",
" ProductResults = pd.DataFrame()\n",
" for p in Instance.Products:\n",
" ProductResults.loc[p, 'Production'] = round(pyo.value(Instance.Production[p]), 4)\n",
" display(ProductResults)\n",
" \n",
" ConstraintStatus = pd.DataFrame(columns=['lSlack', 'uSlack', 'Dual'])\n",
" for c in Instance.component_objects(pyo.Constraint, active = True):\n",
" ConstraintStatus.loc[c.name] = [c.lslack(), c.uslack(), Instance.dual[c]]\n",
" display(ConstraintStatus)\n",
"else:\n",
" print('No solution loaded\\n')\n",
" print('Model:')\n",
" Instance.pprint()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b8b5e23a-f9a8-40af-9720-9dc37f22d39c",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}