Skip to content

Commit

Permalink
Merge pull request #22 from benmaier/dev
Browse files Browse the repository at this point in the history
remove numpy dependency
  • Loading branch information
benmaier authored Jul 3, 2021
2 parents 1757ef7 + eb7b727 commit 3e32bb4
Show file tree
Hide file tree
Showing 8 changed files with 359 additions and 129 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@
.idea

.DS_Store

.coverage
2 changes: 2 additions & 0 deletions binpacking/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from binpacking.utilities import load_csv, save_csvs, print_binsizes
from binpacking.to_constant_bin_number import to_constant_bin_number, csv_to_constant_bin_number
from binpacking.to_constant_volume import to_constant_volume, csv_to_constant_volume

__version__ = '1.5.0'
42 changes: 41 additions & 1 deletion binpacking/tests/constant_bin_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,44 @@ def test_key_func():
for bin_ in bins:
for item in bin_:
assert 'x' in item
assert 'y' in item
assert 'y' in item

def test_bounds_and_tuples():
c = [ ('a', 10, 'foo'), ('b', 10, 'log'), ('c', 11), ('d', 1, 'bar'), ('e', 2, 'bommel'), ('f',7,'floggo') ]
N_bin = 4

bins = to_constant_bin_number(c,N_bin,weight_pos=1,upper_bound=11)
bins = [ sorted(_bin, key=lambda x:x[0]) for _bin in bins ]
assert bins == [
[('a', 10, 'foo')],
[('b', 10, 'log')],
[('f', 7, 'floggo')],
[
('d', 1, 'bar'),
('e', 2, 'bommel'),
]
]

bins = to_constant_bin_number(c,N_bin,weight_pos=1,lower_bound=1)
bins = [ sorted(_bin, key=lambda x:x[0]) for _bin in bins ]
assert bins == [
[('c', 11,)],
[('a', 10, 'foo')],
[('b', 10, 'log')],
[
('e', 2, 'bommel'),
('f', 7, 'floggo'),
],
]

bins = to_constant_bin_number(c,N_bin,weight_pos=1,lower_bound=1,upper_bound=11)
bins = [ sorted(_bin, key=lambda x:x[0]) for _bin in bins ]
assert bins == [
[('a', 10, 'foo')],
[('b', 10, 'log')],
[('f', 7, 'floggo')],
[('e', 2, 'bommel')],
]

if __name__=="__main__":
test_bounds_and_tuples()
44 changes: 44 additions & 0 deletions binpacking/tests/constant_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,47 @@ def test_no_fit():
values = [42, 24]
bins = to_constant_volume(values, 20)
assert bins == [[42], [24]]


def test_bounds_and_tuples():
c = [ ('a', 10, 'foo'), ('b', 10, 'log'), ('c', 11), ('d', 1, 'bar'), ('e', 2, 'bommel'), ('f',7,'floggo') ]
V_max = 11

bins = to_constant_volume(c,V_max,weight_pos=1,upper_bound=11)
bins = [ sorted(_bin, key=lambda x:x[0]) for _bin in bins ]
assert bins == [
[('a', 10, 'foo'), ('d', 1, 'bar')],
[('b', 10, 'log')],
[
('e', 2, 'bommel'),
('f', 7, 'floggo'),
],
]

bins = to_constant_volume(c,V_max,weight_pos=1,lower_bound=1)
bins = [ sorted(_bin, key=lambda x:x[0]) for _bin in bins ]
assert bins == [
[('c', 11,)],
[('a', 10, 'foo')],
[('b', 10, 'log')],
[
('e', 2, 'bommel'),
('f', 7, 'floggo'),
],
]

bins = to_constant_volume(c,V_max,weight_pos=1,lower_bound=1,upper_bound=11)
bins = [ sorted(_bin, key=lambda x:x[0]) for _bin in bins ]
assert bins == [
[('a', 10, 'foo')],
[('b', 10, 'log')],
[
('e', 2, 'bommel'),
('f', 7, 'floggo'),
],
]



if __name__=="__main__":
test_bounds_and_tuples()
185 changes: 119 additions & 66 deletions binpacking/to_constant_bin_number.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,102 @@
from __future__ import print_function
from builtins import range

from binpacking.utilities import load_csv, save_csvs, print_binsizes

import numpy as np

def csv_to_constant_bin_number(filepath,weight_column,N_bin,has_header=False,delim=',',quotechar='"',lower_bound=None,upper_bound=None):

data, weight_column, header = load_csv(filepath,weight_column,has_header=has_header,delim=delim,quotechar=quotechar)

bins = to_constant_bin_number(data,N_bin,weight_pos=weight_column,lower_bound=lower_bound,upper_bound=upper_bound)
print_binsizes(bins,weight_column)

save_csvs(bins,filepath,header,delim=delim,quotechar=quotechar)


def to_constant_bin_number(d,N_bin,weight_pos=None,key=None,lower_bound=None,upper_bound=None):
'''
from binpacking.utilities import (
load_csv,
save_csvs,
print_binsizes,
get,
argmin,
revargsort,
)

def csv_to_constant_bin_number(filepath,
weight_column,
N_bin,
has_header=False,
delim=',',
quotechar='"',
lower_bound=None,
upper_bound=None,
):
"""
Load a csv file, binpack the rows according to one of the columns
to a constant number of bins.
Write a new csv file for each bin, containing
the corresponding rows.
"""

data, weight_column, header = load_csv(filepath,
weight_column,
has_header=has_header,
delim=delim,
quotechar=quotechar,
)

bins = to_constant_bin_number(data,
N_bin,
weight_pos=weight_column,
lower_bound=lower_bound,
upper_bound=upper_bound,
)
print_binsizes(bins, weight_column)

save_csvs(bins,
filepath,
header,
delim=delim,
quotechar=quotechar,
)


def to_constant_bin_number(d,
N_bin,
weight_pos=None,
key=None,
lower_bound=None,
upper_bound=None,
):
"""
Distributes a list of weights, a dictionary of weights or a list of tuples containing weights
to a fixed number of bins while trying to keep the weight distribution constant.
INPUT:
--- d: list containing weights,
OR dictionary where each (key,value)-pair carries the weight as value,
OR list of tuples where one entry in the tuple is the weight. The position of
this weight has to be given in optional variable weight_pos
optional:
~~~ weight_pos: int -- if d is a list of tuples, this integer number gives the position of the weight in a tuple
~~~ key: function -- if d is a list, this key functions grabs the weight for an item
~~~ lower_bound: weights under this bound are not considered
~~~ upper_bound: weights exceeding this bound are not considered
'''

#define functions for the applying the bounds
if lower_bound is not None and upper_bound is not None and lower_bound<upper_bound:
get_valid_weight_ndcs = lambda a: np.nonzero(np.logical_and(a>lower_bound,a<upper_bound))[0]
elif lower_bound is not None:
get_valid_weight_ndcs = lambda a: np.nonzero(a>lower_bound)[0]
elif upper_bound is not None:
get_valid_weight_ndcs = lambda a: np.nonzero(a<upper_bound)[0]
elif lower_bound is None and upper_bound is None:
get_valid_weight_ndcs = lambda a: np.arange(len(a),dtype=int)
elif lower_bound>=upper_bound:
raise Exception("lower_bound is greater or equal to upper_bound")

Parameters
==========
d : iterable
list containing weights,
OR dictionary where each (key,value)-pair carries the weight as value,
OR list of tuples where one entry in the tuple is the weight. The position of
this weight has to be given in optional variable weight_pos
N_bin : int
Number of bins to distribute items to.
weight_pos : int, default = None
if d is a list of tuples, this integer number gives the position of the weight in a tuple
key : function, default = None
if d is a list, this key functions grabs the weight for an item
lower_bound : float, default = None
weights under this bound are not considered
upper_bound : float, default = None
weights exceeding this bound are not considered
Returns
=======
bins : list
A list of length ``N_bin``. Each entry is a list of items or
a dict of items, depending on the type of ``d``.
"""

isdict = isinstance(d,dict)

if isinstance(d, list) and hasattr(d[0], '__len__'):
if not hasattr(d,'__len__'):
raise TypeError("d must be iterable")

if not isdict and hasattr(d[0], '__len__'):
if weight_pos is not None:
key = lambda x: x[weight_pos]
if key is None:
raise ValueError("Must provide weight_pos or key for tuple list")
if isinstance(d, list) and key:

if not isdict and key:
new_dict = {i: val for i, val in enumerate(d)}
d = {i: key(val) for i, val in enumerate(d)}
isdict = True
Expand All @@ -64,45 +108,57 @@ def to_constant_bin_number(d,N_bin,weight_pos=None,key=None,lower_bound=None,upp

#get keys and values (weights)
keys_vals = d.items()
keys = np.array([ k for k,v in keys_vals ])
vals = np.array([ v for k,v in keys_vals ])
keys = [ k for k, v in keys_vals ]
vals = [ v for k, v in keys_vals ]

#sort weights decreasingly
ndcs = np.argsort(vals)[::-1]
ndcs = revargsort(vals)

weights = vals[ndcs]
keys = keys[ndcs]
weights = get(vals, ndcs)
keys = get(keys, ndcs)

bins = [ {} for i in range(N_bin) ]
else:
weights = np.sort(np.array(d))[::-1]
weights = sorted(d,key=lambda x: -x)
bins = [ [] for i in range(N_bin) ]

#find the valid indices
valid_ndcs = get_valid_weight_ndcs(weights)
weights = weights[valid_ndcs]
if lower_bound is not None and upper_bound is not None and lower_bound<upper_bound:
valid_ndcs = filter(lambda i: lower_bound < weights[i] < upper_bound,range(len(weights)))
elif lower_bound is not None:
valid_ndcs = filter(lambda i: lower_bound < weights[i],range(len(weights)))
elif upper_bound is not None:
valid_ndcs = filter(lambda i: weights[i] < upper_bound,range(len(weights)))
elif lower_bound is None and upper_bound is None:
valid_ndcs = range(len(weights))
elif lower_bound>=upper_bound:
raise Exception("lower_bound is greater or equal to upper_bound")

valid_ndcs = list(valid_ndcs)

weights = get(weights, valid_ndcs)

if isdict:
keys = keys[valid_ndcs]
keys = get(keys, valid_ndcs)

#the total volume is the sum of all weights
V_total = weights.sum()
V_total = sum(weights)

#the first estimate of the maximum bin volume is
#the total volume divided to all bins
V_bin_max = V_total / float(N_bin)

#prepare array containing the current weight of the bins
weight_sum = np.zeros(N_bin)
weight_sum = [0. for n in range(N_bin) ]

#iterate through the weight list, starting with heaviest
for item,weight in enumerate(weights):
for item, weight in enumerate(weights):

if isdict:
key = keys[item]

#put next value in bin with lowest weight sum
b = np.argmin(weight_sum)
b = argmin(weight_sum)

#calculate new weight of this bin
new_weight_sum = weight_sum[b] + weight
Expand All @@ -127,7 +183,7 @@ def to_constant_bin_number(d,N_bin,weight_pos=None,key=None,lower_bound=None,upp
else:
#if not, increase the max volume by the sum of
#the rest of the bins per bin
V_bin_max += np.sum(weights[item:]) / float(N_bin)
V_bin_max += sum(weights[item:]) / float(N_bin)

if not is_tuple_list:
return bins
Expand All @@ -138,13 +194,10 @@ def to_constant_bin_number(d,N_bin,weight_pos=None,key=None,lower_bound=None,upp
for key in bins[b]:
new_bins[b].append(new_dict[key])
return new_bins





if __name__=="__main__":
import pylab as pl
import numpy as np

a = np.random.power(0.01,size=1000)
N_bin = 9
Expand All @@ -158,15 +211,15 @@ def to_constant_bin_number(d,N_bin,weight_pos=None,key=None,lower_bound=None,upp
#plot distribution
pl.plot(np.arange(N_bin),[np.sum(b) for b in bins])
pl.ylim([0,max([np.sum(b) for b in bins])+0.1])

b = { 'a': 10, 'b': 10, 'c':11, 'd':1, 'e': 2,'f':7 }
bins = to_constant_bin_number(b,4)
print("===== dict\n",b,"\n",bins)

lower_bound = None
upper_bound = None

b = ( ('a', 10), ('b', 10), ('c',11), ('d',1), ('e', 2),('f',7) )
b = [ ('a', 10), ('b', 10), ('c',11), ('d',1), ('e', 2),('f',7,'foo') ]
bins = to_constant_bin_number(b,4,weight_pos=1,lower_bound=lower_bound,upper_bound=upper_bound)
print("===== list of tuples\n",b,"\n",bins)

Expand Down
Loading

0 comments on commit 3e32bb4

Please sign in to comment.