# -*- coding: utf-8 -*-
"""
biosppy.utils
-------------
This module provides several frequently used functions and hacks.
:copyright: (c) 2015-2018 by Instituto de Telecomunicacoes
:license: BSD 3-clause, see LICENSE for more details.
"""
# Imports
# compat
from __future__ import absolute_import, division, print_function
from six.moves import map, range, zip
import six
from typing import Collection
# built-in
import collections
import copy
import keyword
import os
import re
from numpy import ndarray
# 3rd party
import numpy as np
[docs]def normpath(path):
"""Normalize a path.
Parameters
----------
path : str
The path to normalize.
Returns
-------
npath : str
The normalized path.
"""
if "~" in path:
out = os.path.abspath(os.path.expanduser(path))
else:
out = os.path.abspath(path)
return out
[docs]def fileparts(path):
"""split a file path into its directory, name, and extension.
Parameters
----------
path : str
Input file path.
Returns
-------
dirname : str
File directory.
fname : str
File name.
ext : str
File extension.
Notes
-----
* Removes the dot ('.') from the extension.
"""
dirname, fname = os.path.split(path)
fname, ext = os.path.splitext(fname)
ext = ext.replace(".", "")
return dirname, fname, ext
[docs]def fullfile(*args):
"""Join one or more file path components, assuming the last is
the extension.
Parameters
----------
``*args`` : list, optional
Components to concatenate.
Returns
-------
fpath : str
The concatenated file path.
"""
nb = len(args)
if nb == 0:
return ""
elif nb == 1:
return args[0]
elif nb == 2:
return os.path.join(*args)
fpath = os.path.join(*args[:-1]) + "." + args[-1]
return fpath
[docs]def walktree(top=None, spec=None):
"""Iterator to recursively descend a directory and return all files
matching the spec.
Parameters
----------
top : str, optional
Starting directory; if None, defaults to the current working directoty.
spec : str, optional
Regular expression to match the desired files;
if None, matches all files; typical patterns:
* `r'\.txt$'` - matches files with '.txt' extension;
* `r'^File_'` - matches files starting with 'File\_'
* `r'^File_.+\.txt$'` - matches files starting with 'File\_' and ending with the '.txt' extension.
Yields
------
fpath : str
Absolute file path.
Notes
-----
* Partial matches are also selected.
See Also
--------
* https://docs.python.org/3/library/re.html
* https://regex101.com/
"""
if top is None:
top = os.getcwd()
if spec is None:
spec = r".*?"
prog = re.compile(spec)
for root, _, files in os.walk(top):
for name in files:
if prog.search(name):
fname = os.path.join(root, name)
yield fname
[docs]def remainderAllocator(votes, k, reverse=True, check=False):
"""Allocate k seats proportionally using the Remainder Method.
Also known as Hare-Niemeyer Method. Uses the Hare quota.
Parameters
----------
votes : list
Number of votes for each class/party/cardinal.
k : int
Total number o seats to allocate.
reverse : bool, optional
If True, allocates remaining seats largest quota first.
check : bool, optional
If True, limits the number of seats to the total number of votes.
Returns
-------
seats : list
Number of seats for each class/party/cardinal.
"""
# check total number of votes
tot = np.sum(votes)
if check and k > tot:
k = tot
# frequencies
length = len(votes)
freqs = np.array(votes, dtype="float") / tot
# assign items
aux = k * freqs
seats = aux.astype("int")
# leftovers
nb = k - seats.sum()
if nb > 0:
if reverse:
ind = np.argsort(aux - seats)[::-1]
else:
ind = np.argsort(aux - seats)
for i in range(nb):
seats[ind[i % length]] += 1
return seats.tolist()
[docs]def highestAveragesAllocator(votes, k, divisor="dHondt", check=False):
"""Allocate k seats proportionally using the Highest Averages Method.
Parameters
----------
votes : list
Number of votes for each class/party/cardinal.
k : int
Total number o seats to allocate.
divisor : str, optional
Divisor method; one of 'dHondt', 'Huntington-Hill', 'Sainte-Lague',
'Imperiali', or 'Danish'.
check : bool, optional
If True, limits the number of seats to the total number of votes.
Returns
-------
seats : list
Number of seats for each class/party/cardinal.
"""
# check total number of cardinals
tot = np.sum(votes)
if check and k > tot:
k = tot
# select divisor
if divisor == "dHondt":
fcn = lambda i: float(i)
elif divisor == "Huntington-Hill":
fcn = lambda i: np.sqrt(i * (i + 1.0))
elif divisor == "Sainte-Lague":
fcn = lambda i: i - 0.5
elif divisor == "Imperiali":
fcn = lambda i: float(i + 1)
elif divisor == "Danish":
fcn = lambda i: 3.0 * (i - 1.0) + 1.0
else:
raise ValueError("Unknown divisor method.")
# compute coefficients
tab = []
length = len(votes)
D = [fcn(i) for i in range(1, k + 1)]
for i in range(length):
for j in range(k):
tab.append((i, votes[i] / D[j]))
# sort
tab.sort(key=lambda item: item[1], reverse=True)
tab = tab[:k]
tab = np.array([item[0] for item in tab], dtype="int")
seats = np.zeros(length, dtype="int")
for i in range(length):
seats[i] = np.sum(tab == i)
return seats.tolist()
[docs]def random_fraction(indx, fraction, sort=True):
"""Select a random fraction of an input list of elements.
Parameters
----------
indx : list, array
Elements to partition.
fraction : int, float
Fraction to select.
sort : bool, optional
If True, output lists will be sorted.
Returns
-------
use : list, array
Selected elements.
unuse : list, array
Remaining elements.
"""
# number of elements to use
fraction = float(fraction)
nb = int(fraction * len(indx))
# copy because shuffle works in place
aux = copy.deepcopy(indx)
# shuffle
np.random.shuffle(aux)
# select
use = aux[:nb]
unuse = aux[nb:]
# sort
if sort:
use.sort()
unuse.sort()
return use, unuse
[docs]class ReturnTuple(tuple):
"""A named tuple to use as a hybrid tuple-dict return object.
Parameters
----------
values : iterable
Return values.
names : iterable, optional
Names for return values.
Raises
------
ValueError
If the number of values differs from the number of names.
ValueError
If any of the items in names:
* contain non-alphanumeric characters;
* are Python keywords;
* start with a number;
* are duplicates.
"""
def __new__(cls, values, names=None):
return tuple.__new__(cls, tuple(values))
def __init__(self, values, names=None):
nargs = len(values)
if names is None:
# create names
names = ["_%d" % i for i in range(nargs)]
else:
# check length
if len(names) != nargs:
raise ValueError("Number of names and values mismatch.")
# convert to str
names = list(map(str, names))
# check for keywords, alphanumeric, digits, repeats
seen = set()
for name in names:
if not all(c.isalnum() or (c == "_") for c in name):
raise ValueError(
"Names can only contain alphanumeric \
characters and underscores: %r."
% name
)
if keyword.iskeyword(name):
raise ValueError("Names cannot be a keyword: %r." % name)
if name[0].isdigit():
raise ValueError("Names cannot start with a number: %r." % name)
if name in seen:
raise ValueError("Encountered duplicate name: %r." % name)
seen.add(name)
self._names = names
[docs] def as_dict(self):
"""Convert to an ordered dictionary.
Returns
-------
out : OrderedDict
An OrderedDict representing the return values.
"""
return collections.OrderedDict(zip(self._names, self))
__dict__ = property(as_dict)
def __getitem__(self, key):
"""Get item as an index or keyword.
Returns
-------
out : object
The object corresponding to the key, if it exists.
Raises
------
KeyError
If the key is a string and it does not exist in the mapping.
IndexError
If the key is an int and it is out of range.
"""
if isinstance(key, six.string_types):
if key not in self._names:
raise KeyError("Unknown key: %r." % key)
key = self._names.index(key)
return super(ReturnTuple, self).__getitem__(key)
def __repr__(self):
"""Return representation string."""
tpl = "%s=%r"
rp = ", ".join(tpl % item for item in zip(self._names, self))
return "ReturnTuple(%s)" % rp
def __getnewargs__(self):
"""Return self as a plain tuple; used for copy and pickle."""
return tuple(self)
[docs] def keys(self):
"""Return the value names.
Returns
-------
out : list
The keys in the mapping.
"""
return list(self._names)
[docs] def append(self, new_values: (int, float, complex, str, Collection, ndarray), new_keys=None):
"""
Returns a new ReturnTuple with the new values and keys appended to the original object.
Parameters
----------
new_values : int, float, list, tuple, dict, array
Values to append. If given a dict and new_keys is None, the correspondent keys and values will be used.
new_keys : str, list, tuple, optional
Keys to append.
Returns
-------
object : ReturnTuple
A ReturnTuple with the values and keys appended.
"""
# initialize tuples
values, keys = (), ()
# check input
if isinstance(new_values, ReturnTuple):
raise ValueError("new_values is a ReturnTuple object. Use the join method instead.")
# add values and keys from dict if new_keys is empty
if new_keys is None:
if isinstance(new_values, dict):
keys = tuple(self.keys()) + tuple(new_values.keys())
values = self + tuple(new_values.values())
else:
raise ValueError('new_keys is empty. Please provide new_keys or use a dict.')
else:
# add a single value-key pair
if isinstance(new_keys, str):
keys = tuple(self.keys()) + tuple([new_keys])
values = self + tuple([new_values])
# add multiple value-key pairs
else:
# check length
if len(new_keys) != len(new_values):
raise ValueError("new_keys and new_values don't have the same length.")
temp_keys, temp_values = (), ()
for key, value in zip(new_keys, new_values):
temp_keys += tuple([key])
temp_values += tuple([value])
keys = tuple(self.keys()) + temp_keys
values = self + temp_values
return ReturnTuple(values, keys)
[docs] def join(self, new_tuple):
"""
Returns a ReturnTuple with the new ReturnTuple appended.
Parameters
----------
new_tuple : ReturnTuple
ReturnTuple to be joined.
Returns
-------
object : ReturnTuple
The joined ReturnTuple.
"""
if not isinstance(new_tuple, ReturnTuple):
raise TypeError('new_tuple must be a ReturnTuple object.')
values = self + new_tuple
keys = tuple(self.keys()) + tuple(new_tuple.keys())
return ReturnTuple(values, keys)
[docs] def delete(self, key):
"""
Returns a ReturnTuple without the specified key.
Parameters
----------
key : str
ReturnTuple key to be deleted.
Returns
-------
object : ReturnTuple
The ReturnTuple with the key removed.
"""
values = tuple()
keys = tuple()
if not isinstance(key, str):
raise TypeError('key must be a string.')
if key not in self.keys():
raise ValueError(f'{key} key not in the ReturnTuple.')
for k in self.keys():
if k != key:
values += (self.__getitem__(k), )
keys += (k, )
return ReturnTuple(values, keys)