#!/usr/bin/env python
import numpy as np
import time
import sys
try:
from . import vector
except:
import vector
def fmt_time(t):
return '%d:%02d:%02d' % (int(t/3600.), int(t/60.)%60, int(t)%60)
#def fmt_time(t):
# return '%d:%02d' % (int(t/60.), int(t)%60)
#------------------------------------------------------------------------------
# Class for printing pretty status counters
#------------------------------------------------------------------------------
[docs]class StatusCounter(object):
'''A class for printing status messages for long computations.
Parameters
----------
count : int or iterable object
The number of items *or* the items over which to iterate.
msg : string
The status message
min_time : float, optional (default: 1./30)
The minimum time before a new mssage is printed. Used to prevent
very fast updates from slowing down computations (print to stderr can
be slow!)
'''
def __init__(self, count, msg='Calculating...', min_time=1./30):
if hasattr(count, '__len__'):
self.data = count
self.count = len(count)
elif hasattr(count, '__iter__'):
self.data = list(count)
self.count = len(self.data)
else:
self.count = count
self.msg = msg
self.current = -1
self.start = time.time()
self.last_time = time.time()
self.max_len = 0
self.min_time = min_time
self.extra_msg = ''
if not hasattr(self, 'data'): self.update()
[docs] def message(self, msg):
'''Change the print message during a computation.'''
self.extra_msg = msg
[docs] def print_msg(self, msg, final=False):
'''Print the message (usually handled automatically.)'''
if len(msg) > self.max_len: self.max_len = len(msg)
msg = ('%%-%ds' % self.max_len) % msg
if final: print '\r' + msg
else: print '\r' + msg,
sys.stdout.flush()
[docs] def update(self, extra_msg=''):
'''Increment the counter manually (usually handlded automatically.)'''
self.current += 1
t = time.time()
if self.current < self.count and (t - self.last_time) < self.min_time:
return None
self.last_time = t
percent = int(100 * self.current / self.count + 0.5)
if not extra_msg: extra_msg = self.extra_msg
if extra_msg: extra_msg = ' [%s]' % extra_msg
elapsed = t - self.start
if self.current == self.count:
ft = fmt_time(elapsed)
if elapsed < 60: ft += '.%02d' % round((elapsed%1)*100)
self.print_msg(self.msg + ' done in %s. ' % ft + extra_msg, final=True)
elif self.current <= 0:
self.print_msg(self.msg + ' %2d%% ' % percent + extra_msg)
elif self.current < self.count:
est = elapsed / self.current * (self.count - self.current)
self.print_msg(self.msg + ' %2d%% (%s remaining) ' % (percent, fmt_time(est)) + extra_msg)
def __iter__(self):
return self
def next(self):
self.update()
if self.current < self.count:
if hasattr(self, 'data'): return self.data[self.current]
else: return self.current
else:
raise StopIteration
[docs]class GroupById(object):
'''Create an object which returns the values of objects with a given id.
After it is created, behaves like a dictionary object which returns
numpy arrays of values with varying length, where missing values return
the value specified in ``missing``.
Items may also be accessed with lists or arrays of ids.
* Lists of keys return lists of numpy arrays
* Arrays of keys return a concatenated array of the values
Example usage:
.. code-block:: python
from ilpm import misc, vector
#Create some random points
X = (np.random.rand(100, 3) - 0.5) * 10
#Our id is going to be numbered by radial shell, e.g. a point with radius
# r=5.2... will have id 5.
ids = vector.mag(X).astype('i')
#Make the group
X_by_r = misc.GroupById(ids, X)
#Lets see whats in there.
for ir in sorted(X_by_r.keys()):
X = X_by_r[ir]
r = vector.mag(X)
print 'key: %d, %2d values ranging from r = %.2f - %.2f' % (ir, len(r), r.min(), r.max())
#What if we wanted all particles with radii from 2-5?
ids = np.arange(2, 5)
X = X_by_r[ids]
r = vector.mag(X)
print 'keys: %s, %2d values ranging from r = %.2f - %.2f' % (repr(ids), len(r), r.min(), r.max())
Sample output::
key: 1, 2 values ranging from r = 1.45 - 1.65
key: 2, 2 values ranging from r = 2.35 - 2.96
key: 3, 17 values ranging from r = 3.00 - 3.93
key: 4, 23 values ranging from r = 4.11 - 4.99
key: 5, 32 values ranging from r = 5.03 - 5.90
key: 6, 18 values ranging from r = 6.05 - 6.97
key: 7, 6 values ranging from r = 7.13 - 7.75
keys: array([2, 3, 4]), 42 values ranging from r = 2.35 - 4.99
Parameters
----------
ids : 1D numpy array
A list of id's.
values : (len(id), ...) numpy array, optional
The value which correspond to each id. If not specified, indices
are returned instead of values (i.e. ``value = arange(len(id))``)
missing : any object, optional (default: empty numpy array)
The default value to return if there are no objects with this id.
If not specified, returns an empty numpy array with the same data
type as values.
'''
def __init__(self, ids, values=None, missing=None):
if values is None: values = np.arange(len(id))
#The [:0] may seem odd, but it keeps the extra dimensions and data
# type of values, allowing it to be concatenated with the actual
# values without raising errors.
if missing is None: missing = np.empty_like(values[:0])
ids = np.asarray(ids)
if ids.ndim != 1:
raise ValueError('ids should be 1D')
si = np.argsort(ids)
ids_s = ids[si]
vals_s = values[si]
breaks = np.where(ids_s[:-1] != ids_s[1:])[0] + 1
#start = np.zeros(len(self.points), 'i8')
#end = np.zeros(len(self.points), 'i8')
#end[ip[breaks]] = breaks+1
#start[ip[breaks+1]] = breaks+1
#end[ip[breaks[-1]+1]] = len(ip)
self.groups = {}
self.missing = missing
for s, e in zip(np.concatenate([[0], breaks]), np.concatenate([breaks, [len(ids)]])):
self.groups[ids_s[s]] = vals_s[s:e]
def __contains__(self, id):
return id in self.groups
[docs] def keys(self):
'''Return the keys which have items in them.'''
return self.groups.keys()
def __getitem__(self, ids):
if hasattr(ids, '__iter__'):
vals = map(lambda id: self.groups.get(id, self.missing), ids)
if isinstance(ids, np.ndarray):
vals = np.concatenate(vals)
return vals
elif isinstance(ids, slice):
raise TypeError('slice indexing not supported for GroupById objects')
else:
return self.groups.get(ids, self.missing)
BIT_MASK = (1<<20)-1
[docs]class NeighborList(object):
'''Generate a object for finding point neighbors quickly.
*Note: using this class is only faster than brute force for > 1000 points.
However, for very large numbers of points (> 10,000) it is quite fast.*
Parameters
----------
cutoff : float
The default radius to consider a particle a "neighbor". Although
different radii may be requested later, this sets the box width for the
cell-list, so optimially it is the average required radius.
*Note: the cutoff should be more than 2**20 ~ 1E6 times the total
separation.*
points : vector array
Points to put in the list
'''
def __init__(self, cutoff, points):
self.P0 = points.min(0)
self.P1 = points.max(0)
self.N = np.clip(np.ceil((self.P1 - self.P0)/cutoff), 1, np.inf).astype('i8')
self.k = 1./cutoff
self.default_r = cutoff
self.points = points.copy()
self.cell_list = GroupById(self._get_id(self.points), np.arange(len(self)))
def _get_id(self, X):
i = (X * self.k).astype('u8')
return (i[:, 0]&BIT_MASK) + ((i[:, 1]&BIT_MASK)<<20) + ((i[:, 2]&BIT_MASK)<<40)
def __len__(self):
return len(self.points)
[docs] def indices_within_r(self, P0, r=None):
'''Return the indices of all points within a specified radius.
Arguments
---------
P0 : vector
r : float, optional (default: cutoff specified on object creation)
'''
if r is None: r = self.default_r
ns = int(np.ceil(r * self.k))
i0, j0, k0 = (P0 * self.k).astype('u8')
#i, j, k = np.ogrid[i0-ns:i0+ns+1, j0-ns:j0+ns+1, k0-ns:k0+ns+1]
#ids = (i.astype('u8')&BIT_MASK) + ((j.astype('u8')&BIT_MASK)<<20) + ((k.astype('u8')&BIT_MASK)<<40)
i = np.arange(i0-ns, i0+ns+1, dtype='u8').reshape(1, 1, -1)
j = np.arange(j0-ns, j0+ns+1, dtype='u8').reshape(1, -1, 1)
k = np.arange(k0-ns, k0+ns+1, dtype='u8').reshape(-1, 1, 1)
ids = (i&BIT_MASK) + ((j&BIT_MASK)<<20) + ((k&BIT_MASK)<<40)
i = self.cell_list[ids.reshape(-1)]
i = i[np.where(vector.mag(self.points[i] - P0) < r)]
return i
if __name__ == '__main__':
print
print '-'*80
print ' testing GroupById'
print '-'*80
print
#Create some random points
X = (np.random.rand(100, 3) - 0.5) * 10
#Our id is going to be numbered by radial shell, e.g. a point with radius
# r=5.2... will have id 5.
ids = vector.mag(X).astype('i')
#Make the group
X_by_r = GroupById(ids, X)
#Lets see whats in there.
for ir in sorted(X_by_r.keys()):
X = X_by_r[ir]
r = vector.mag(X)
print 'key: %d, %2d values ranging from r = %.2f - %.2f' % (ir, len(r), r.min(), r.max())
#What if we wanted all particles with radii from 2-5?
ids = np.arange(2, 5)
X = X_by_r[ids]
r = vector.mag(X)
print 'keys: %s, %2d values ranging from r = %.2f - %.2f' % (repr(ids), len(r), r.min(), r.max())
print
print '-'*80
print ' testing StatusCounter'
print '-'*80
print
#These are the values over which we are going to compute stuff.
values = np.linspace(0, 1.0, 10000)
#Use the status counter as an iterator -- this automatically prints the
# status message as we loop!
for val in StatusCounter(values, 'Squaring stuff...'):
square = val**2 #Do something
time.sleep(1E-3) #This something is too fast, so lets slow it.
if val > 0.5: break
#Create it as a named object so we can change the print message
stat = StatusCounter(values, 'Squaring stuff...')
for val in stat:
square = val**2 #Do something
stat.message('%.3f -> %.3f' % (val, square)) #Print the result as we compute.
time.sleep(1E-3) #This something is too fast, so lets slow it.
print