Skip to content

Instantly share code, notes, and snippets.

@ZenithClown
Last active April 3, 2024 07:28
Show Gist options
  • Save ZenithClown/277ddc84fa10239b4a24a366813761a5 to your computer and use it in GitHub Desktop.
Save ZenithClown/277ddc84fa10239b4a24a366813761a5 to your computer and use it in GitHub Desktop.
A set of utility functions related to linear programming using pulp and/or scipy library
# -*- encoding: utf-8 -*-
"""
An Approach to Simplify Linear Programming Statement Defination
The module aims to modularize the defination of a `LP` problem by
maintaining a general defination and attributes to simplify
programming approach that involves `n` variables which has to be
defined dynamically.
@author: Debmalya Pramanik
@version: v0.0.1
"""
import pulp as p # https://coin-or.github.io/pulp/
class LpObject(object):
"""
An Objective Function Defination for Solving LP using PuLP
PuLP is a modeler written in python for solving linear
programming. The class file aims to simplify by defining "large"
number of variables, and provide attributes for easy access.
:type name: str
:param name: Name of the LP Problem, optional, defaults to
`NoName` as in `p.LpProblem()` signature. This name can be
used for generating output `.lp` file.
:type sense: str or int
:param sense: Objective of the LP, which is either "minimization"
(-1) or "maximization" (1). Defaults to "maximize". Check
`__set_sense__` for more information.
"""
def __init__(self, n : int, cost : list, name : str = "NoName", sense : str = "maximize", **kwargs) -> None:
self.n = n # number of variables
self.name = name
self.sense = self.__set_sense__(sense)
# ! define cost function, and assign fixed cost
self.cost = cost
self.fixed_cost = kwargs.get("fixed_cost", 0)
# ! All the variable attributes are prefixed as `var_`
var_names = kwargs.get("var_names", [f"X{idx + 1}" for idx in range(n)])
# lb : lower bound, ub : upper bound, cat : category of the variable
var_lb = kwargs.get("var_lb", [None] * n)
var_ub = kwargs.get("var_ub", [None] * n)
var_cat = kwargs.get("var_cat", ["Continuous"] * n)
assert n == len(var_names) == len(var_lb) == len(var_ub) == len(var_cat)
# ? variables returned as dictionary, thus enabling easy calling
# like: LpObject(*).variables["name"]
self.variables = self.__set_variables__(var_names, var_lb, var_ub, var_cat)
# ! attribute defination to create an lp problem:
self.problem = p.LpProblem(self.name, sense = self.sense)
self.problem += p.lpSum(c * var for c, var in zip(self.cost, self.variables.values())) + self.fixed_cost
def __set_sense__(self, sense : str) -> int:
sense = sense.lower()
sense_ = {
-1 : p.LpMinimize,
"minimize" : p.LpMinimize,
"minimization" : p.LpMinimize,
1 : p.LpMaximize,
"maximize" : p.LpMaximize,
"maximization" : p.LpMaximize
}
assert sense in sense_.keys(), f"Sense (= {sense}) is not yet defined."
return sense_[sense]
def __set_variables__(self, names : list, lowBound : list, upBound : list, cat : list) -> dict:
return {
name : p.LpVariable(name, lowBound = lb, upBound = ub, cat = cat)
for name, lb, ub, cat in zip(names, lowBound, upBound, cat)
}
def __repr__(self) -> str:
return f"LP({id(self)}, {self.name}, <n = {self.n}, vars = {self.variables.keys()}>)"
def __str__(self) -> str:
return f"PuLP Problem: {self.name} of {self.n} Variables."

Optimization Problems

Zenith Clown REPO:ADMIN CODE:DOCUMENTATION
utility function related to code and/or function optimizations


Linear Programming is defined as a method of achieving the best outcome using a mathematical model whose relationships are represented by linear relationships. In contrast, to Non-Linear Programming a basic assumption of linear programming is that the relationships between the decision variables, constraints and the objective functions are linear - meaning they can be represented by a straight line. [1] [2] [3]

The variables ($x_1, x_2, ..., x_n$) of both the programming are either integer, or continuos, if all the variables are integer then it is known as Integer Programming, however, most of the real world problems are "Mixed Integer Programming" abbreviated as MILP. The utility functions for linear programming is available under linprog.py and the non-linear programming defination is currently in development.

Getting Started

The code is publicly available at optim by ZenithClown. The module requires external libraries pulp and scipy, to initialize:

# install pip libraries:
pip install pulp # linear programming using pulp
pip install scipy # scientific computing library

# clone the code like:
git clone https://gist.github.com/ZenithClown/277ddc84fa10239b4a24a366813761a5.git optim
export PYTHONPATH="${PYTHONPATH}:optim"

Linear Programming

Currently, a objective function is defined for PuLP, that aims to simplify large variables problem defination using a combination of scipy.optimize.linprog and pulp module approach. TODO: Documentation!!

import pulp as p
import linprog as lp

n = 3 # no. of variables

defination = lp.LpObject(n, cost = [10, 15, 25], sense = "minimize", var_lb = [0] * n)
print(repr(defination)) # development representation
>> LP(2402254029488, lp-problem, <n = 3, vars = dict_keys(['X1', 'X2', 'X3'])>)
print(f"Defined Objective Function: {defination.problem.objective}")
>> Defined Objective Function: 10*X1 + 15*X2 + 25*X3

# add constraints
defination.problem += defination.variables["X1"] + defination.variables["X2"] + defination.variables["X3"] >= 1_000
defination.problem += defination.variables["X1"] - 2 * defination.variables["X2"] >= 0
defination.problem += defination.variables["X3"] >= 340

status = defination.problem.solve()
print(p.LpStatus[status])
>> Optimal

print("X1 =", p.value(defination.variables["X1"])) 
print("X2 =", p.value(defination.variables["X2"]))
print("X3 =", p.value(defination.variables["X3"]))
print("Objective = ", p.value(defination.problem.objective))
>> X1 = 660.0
>> X2 = 0.0
>> X3 = 340.0
>> Objective =  15100.0
# -*- encoding: utf-8 -*-
"""
A List of Useful Decorators/Wrappers for Python Functions
A python decorator allows a developer to change the behavior of a
python function without modifying the function itself. They can be
used simply by calling the decorator like:
```python
@decorator
def function(*args, **kwargs):
pass
```
Check the [Python Glossary](https://docs.python.org/3.0/glossary.html)
for more information, and you can check the [central repository]
(https://wiki.python.org/moin/PythonDecoratorLibrary) for more details.
@author: Debmalya Pramanik
@version: v0.0.1
"""
import time
import functools
import tracemalloc
def profile(func : callable) -> callable:
"""
A Simple Memory & Timer Profiling Decorator for a Python Function
The decorator uses the in-built modules like `time`, `functools`,
and `tracemalloc` to provide a sime decorator to return the
memory usage (current and peak) and execution time of the
function by wrapping the function using a decoratory. Example:
```python
from wrappers import profile # this function
@profile
def make_list(n : int) -> list:
return list(range(n))
arr = make_list(10_00_000) # execute the function normally
>> Executed with @profile[`make_list`]
>> >> Memory Usage: <current - 36.141124 MB, peak - 36.141260 MB>
>> >> Time Elapsed: 0.55 secs.
```
This decorator is inspired from the in-built python function
[profilers](https://docs.python.org/3/library/profile.html), but
this is written as a decorator instead using OOPs approach.
"""
@functools.wraps(func)
def _wrapper(*args, **kwargs) -> object:
tracemalloc.start() # https://docs.python.org/3/library/tracemalloc.html
# https://docs.python.org/3/library/time.html#time.perf_counter
start_ = time.perf_counter()
retval = func(*args, **kwargs) # ? execute original function
cur_, peak_ = tracemalloc.get_traced_memory()
end_ = time.perf_counter()
print(f"Executed with @profile[`{func.__name__}`]")
print(f" >> Memory Usage: <current - {cur_ / 1e6:,.6f} MB, peak - {peak_ / 1e6:,.6f} MB>")
print(f" >> Time Elapsed: {end_ - start_ :,.2f} secs.")
tracemalloc.stop()
return retval
return _wrapper
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment