pax_global_header00006660000000000000000000000064137277120210014514gustar00rootroot0000000000000052 comment=786a78b2da9d8df94ac373cdf11eef93ac723a8c numpy-groupies-0.9.13/000077500000000000000000000000001372771202100146115ustar00rootroot00000000000000numpy-groupies-0.9.13/.gitattributes000066400000000000000000000000531372771202100175020ustar00rootroot00000000000000numpy_groupies/_versioneer.py export-subst numpy-groupies-0.9.13/.gitignore000066400000000000000000000005441372771202100166040ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Dev stuff .mr.developer.cfg .idea .project .pydevproject .settings/ .cache/ __pycache__/ .eggs/ *~ *.ini numpy-groupies-0.9.13/LICENSE.txt000066400000000000000000000024371372771202100164420ustar00rootroot00000000000000Copyright (c) 2016, numpy-groupies developers All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. numpy-groupies-0.9.13/MANIFEST.in000066400000000000000000000002241372771202100163450ustar00rootroot00000000000000include README.md include LICENSE.txt graft numpy_groupies recursive-exclude * *.py[co] include versioneer.py include numpy_groupies/_versioneer.py numpy-groupies-0.9.13/README.md000066400000000000000000000362241372771202100160770ustar00rootroot00000000000000# numpy-groupies This package consists of a small library of optimised tools for doing things that can roughly be considered "group-indexing operations". The most prominent tool is `aggregate`, which is descibed in detail further down the page. ## Installation If you have `pip`, then simply: ``` pip install numpy_groupies ``` Note that `numpy_groupies` doesn't have any compulsory dependencies (even `numpy` is optional) so you should be able to install it fairly easily even without a package manager. If you just want one particular implementation of `aggregate` (e.g. `aggregate_numpy.py`), you can download that one file, and copy-paste the contents of `utils.py` into the top of that file (replacing the `from .utils import (...)` line). ## aggregate ![aggregate_diagram](/diagrams/aggregate.png) ```python import numpy as np import numpy_groupies as npg group_idx = np.array([ 3, 0, 0, 1, 0, 3, 5, 5, 0, 4]) a = np.array([13.2, 3.5, 3.5,-8.2, 3.0,13.4,99.2,-7.1, 0.0,53.7]) npg.aggregate(group_idx, a, func='sum', fill_value=0) # >>> array([10.0, -8.2, 0.0, 26.6, 53.7, 92.1]) ``` `aggregate` takes an array of values, and an array giving the group number for each of those values. It then returns the sum (or mean, or std, or any, ...etc.) of the values in each group. You have probably come across this idea before - see [Matlab's `accumarray` function](http://uk.mathworks.com/help/matlab/ref/accumarray.html?refresh=true), or [`pandas` groupby concept](http://pandas.pydata.org/pandas-docs/dev/groupby.html), or [MapReduce paradigm](http://en.wikipedia.org/wiki/MapReduce), or simply the [basic histogram](https://en.wikipedia.org/wiki/Histogram). A couple of implemented functions do not reduce the data, instead it calculates values cumulatively while iterating over the data or permutates them. The output size matches the input size. ```python group_idx = np.array([4, 3, 3, 4, 4, 1, 1, 1, 7, 8, 7, 4, 3, 3, 1, 1]) a = np.array([3, 4, 1, 3, 9, 9, 6, 7, 7, 0, 8, 2, 1, 8, 9, 8]) npg.aggregate(group_idx, a, func='cumsum') # >>> array([3, 4, 5, 6,15, 9,15,22, 7, 0,15,17, 6,14,31,39]) ``` ### Inputs The function accepts various different combinations of inputs, producing various different shapes of output. We give a brief description of the general meaning of the inputs and then go over the different combinations in more detail: * `group_idx` - array of non-negative integers to be used as the "labels" with which to group the values in `a`. * `a` - array of values to be aggregated. * `func='sum'` - the function to use for aggregation. See the section below for nore details. * `size=None` - the shape of the output array. If `None`, the maximum value in `group_idx` will set the size of the output. * `fill_value=0` - value to use for output groups that do not appear anywhere in the `group_idx` input array. * `order='C'` - for multidimensional output, this controls the layout in memory, can be `'F'` for fortran-style. * `dtype=None` - the`dtype` of the output. `None` means choose a sensible type for the given `a`, `func`, and `fill_value`. * `axis=None` - explained below. * `ddof=0` - passed through into calculations of variance and standard deviation (see section on functions). ![aggregate_dims_diagram](/diagrams/aggregate_dims.png) * Form 1 is the simplest, taking `group_idx` and `a` of matching 1D lengths, and producing a 1D output. * Form 2 is similar to Form 1, but takes a scalar `a`, which is broadcast out to the length of `group_idx`. Note that this is generally not that useful. * Form 3 is more complicated. `group_idx` is the same length as the `a.shape[axis]`. The groups are broadcast out along the other axis/axes of `a`, thus the output is of shape `n_groups x a.shape[0] x ... x a.shape[axis-1] x a.shape[axis+1] x ... a.shape[-1]`, i.e. the output has two or more dimensions. * Form 4 also produces output with two or more dimensions, but for very different reasons to Form 3. Here `a` is 1D and `group_idx` is exactly `2D`, whereas in Form 3 `a` is `ND`, `group_idx` is `1D`, and we provide a value for `axis`. The length of `a` must match `group_idx.shape[1]`, the value of `group_idx.shape[0]` determines the number of dimensions in the ouput, i.e. `group_idx[:,99]` gives the `(x,y,z)` group indices for the `a[99]`. * Form 5 is the same as Form 4 but with scalar `a`. As with Form 2, this is rarely that helpful. **Note on performance.** The `order` of the output is unlikely to affect performance of `aggregate` (although it may affect your downstream usage of that output), however the order of multidimensional `a` or `group_idx` can affect performance: in Form 4 it is best if columns are contiguous in memory within `group_idx`, i.e. `group_idx[:, 99]` corresponds to a contiguous chunk of memory; in Form 3 it's best if all the data in `a` for `group_idx[i]` is contiguous, e.g. if `axis=1` then we want `a[:, 55]` to be contiguous. ### Available functions By default, `aggregate` assumes you want to sum the values within each group, however you can specify another function using the `func` kwarg. This `func` can be any custom callable, however you will likely want one of the following optimized functions. Note that not all functions might be provided by all implementations. * `'sum'` - sum of items within each group (see example above). * `'prod'` - product of items within each group * `'mean'` - mean of items within each group * `'var'`- variance of items within each group. Use `ddof` kwarg for degrees of freedom. The divisor used in calculations is `N - ddof`, where `N` represents the number of elements. By default `ddof` is zero. * `'std'` - standard deviation of items within each group. Use `ddof` kwarg for degrees of freedom (see `var` above). * `'min'` - minimum value of items within each group. * `'max'` - maximum value of items within each group. * `'first'` - first item in `a` from each group. * `'last'` - last item in `a` from each group. * `'argmax'` - the index in `a` of the maximum value in each group. * `'argmin'` - the index in `a` of the minimum value in each group. The above functions also have a `nan`-form, which skip the `nan` values instead of propagating them to the result of the calculation: * `'nansum'`, `'nanprod'`, `'nanmean'`, `'nanvar'`, `'nanstd'`, `'nanmin'`, `'nanmax'`, `'nanfirst'`, `'nanlast'`, ``nanargmax``, ``nanargmin`` The following functions are slightly different in that they always return boolean values. Their treatment of nans is also different from above: * `'all'` - `True` if all items within a group are truethy. Note that `np.all(nan)` is `True`, i.e. `nan` is actually truethy. * `'any'` - `True` if any items within a group are truethy. * `'allnan'` - `True` if all items within a group are `nan`. * `'anynan'` - `True` if any items within a gorup are `nan`. The following functions don't reduce the data, but instead produce an output matching the size of the input: * `cumsum` - cumulative sum of items within each group. * `cumprod` - cumulative product of items within each group. (numba only) * `cummin` - cumulative minimum of items within each group. (numba only) * `cummax` - cumulative maximum of items within each group. (numba only) * `'sort'` - sort the items within each group in ascending order, use reverse=True to invert the order. Finally, there are three functions which don't reduce each group to a single value, instead they return the full set of items within the group: * `'array'` - simply returns the grouped items, using the same order as appeared in `a`. (numpy only) ### Examples Compute sums of consecutive integers, and then compute products of those consecutive integers. ```python group_idx = np.arange(5).repeat(3) # group_idx: array([0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]) a = np.arange(group_idx.size) # a: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]) x = aggregate(group_idx, a) # sum is default # x: array([ 3, 12, 21, 30, 39]) x = aggregate(group_idx, a, 'prod') # x: array([ 0, 60, 336, 990, 2184]) ``` Get variance ignoring nans, setting all-nan groups to `nan`. ```python x = aggregate(group_idx, a, func='nanvar', fill_value=nan) ``` Count the number of elements in each group. Note that this is equivalent to doing `np.bincount(group_idx)`, indeed that is how the numpy implementation does it. ```python x = aggregate(group_idx, 1) ``` Sum 1000 values into a three-dimensional cube of size 15x15x15. Note that in this example all three dimensions have the same size, but that doesn't have to be the case. ```python group_idx = np.random.randint(0, 15, size=(3, 1000)) a = np.random.random(group_idx.shape[1]) x = aggregate(group_idx, a, func="sum", size=(15,15,15), order="F") # x.shape: (15, 15, 15) # np.isfortran(x): True ``` Use a custom function to generate some strings. ```python group_idx = array([1, 0, 1, 4, 1]) a = array([12.0, 3.2, -15, 88, 12.9]) x = aggregate(group_idx, a, func=lambda g: ' or maybe '.join(str(gg) for gg in g), fill_value='') # x: ['3.2', '12.0 or maybe -15.0 or maybe 12.9', '', '', '88.0'] ``` Use the `axis` arg in order to do a sum-aggregation on three rows simultaneously. ```python a = array([[99, 2, 11, 14, 20], [33, 76, 12, 100, 71], [67, 10, -8, 1, 9]]) group_idx = array([[3, 3, 7, 0, 0]]) x = aggregate(group_idx, a, axis=1) # x : [[ 34, 0, 0, 101, 0, 0, 0, 11], # [171, 0, 0, 109, 0, 0, 0, 12], # [ 10, 0, 0, 77, 0, 0, 0, -8]] ``` ### Multiple implementations There are multiple implementations of `aggregate` provided. If you use `from numpy_groupies import aggregate`, the best available implementation will automatically be selected. Otherwise you can pick a specific version directly like `from numpy_groupies import aggregate_nb as aggregate` or by importing aggregate from the implementing module `from numpy_groupies.aggregate_weave import aggregate`. Currently the following implementations exist: * **numpy** - This is the default implementation. It uses plain `numpy`, mainly relying on `np.bincount` and basic indexing magic. It comes without other dependencies except `numpy` and shows reasonable performance for the occasional usage. * **numba** - This is the most performant implementation in average, based on jit compilation provided by numba and LLVM. * **weave** - `weave` compiles C-code on demand at runtime, producing binaries that get executed from within python. The performance of this implementation is comparable to the numba implementation. * **pure python** - This implementation has no dependencies and uses only the standard library. It's horribly slow and should only be used, if there is no numpy available. * **numpy ufunc** - *Only for benchmarking.* This impelmentation uses the `.at` method of numpy's `ufunc`s (e.g. `add.at`), which would appear to be designed for perfoming excactly the same calculation that `aggregate` executes, however the numpy implementation is rather incomplete and slow (as of `v1.14.0`). A [numpy issue](https://github.com/numpy/numpy/issues/5922) has been created to address this issue. * **pandas** - *Only for reference.* The pandas' `groupby` concept is the same as the task performed by `aggregate`. However, `pandas` is not actually faster than the default `numpy` implementation. Also, note that there may be room for improvement in the way that `pandas` is utilized here. Most notably, when computing multiple aggregations of the same data (e.g. `'min'` and `'max'`) pandas could potentially be used more efficiently. All implementations have the same calling syntax and produce the same outputs, to within some floating-point error. However some implementations only support a subset of the valid inputs and will sometimes throw `NotImplementedError`. ### Benchmarks Scripts for testing and benchmarking are included in this repository. For benchmarking, run `python -m numpy_groupies.benchmarks.generic` from the root of this repository. Below we are using `500,000` indices uniformly picked from `[0, 1000)`. The values of `a` are uniformly picked from the interval `[0,1)`, with anything less than `0.2` then set to 0 (in order to serve as falsy values in boolean operations). For `nan-` operations another 20% of the values are set to nan, leaving the remainder on the interval `[0.2,0.8)`. The benchmarking results are given in ms for an i7-7560U running at 2.40GHz: ```text function ufunc numpy numba weave ----------------------------------------------------------------- sum 28.763 1.477 0.917 1.167 prod 29.165 29.162 0.919 1.170 amin 33.020 33.134 0.979 1.181 amax 33.150 33.156 1.049 1.216 len 28.594 1.260 0.755 1.023 all 33.493 3.883 0.995 1.214 any 33.308 6.776 1.003 1.216 anynan 28.938 2.472 0.930 1.182 allnan 29.391 5.929 0.931 1.201 mean ---- 2.100 0.972 1.216 std ---- 6.600 1.127 1.370 var ---- 6.684 1.109 1.388 first ---- 2.140 1.067 1.188 last ---- 1.545 0.818 1.086 argmax ---- 33.860 1.016 ---- argmin ---- 36.690 0.981 ---- nansum ---- 4.944 1.722 1.342 nanprod ---- 27.286 1.726 1.369 nanmin ---- 30.238 1.895 1.359 nanmax ---- 30.337 1.939 1.446 nanlen ---- 4.820 1.707 1.312 nanall ---- 9.148 1.786 1.380 nanany ---- 10.157 1.830 1.392 nanmean ---- 5.775 1.758 1.440 nanvar ---- 10.090 1.922 1.651 nanstd ---- 10.308 1.884 1.664 nanfirst ---- 5.481 1.945 1.295 nanlast ---- 4.992 1.735 1.199 cumsum ---- 144.807 1.455 ---- cumprod ---- ---- 1.371 ---- cummax ---- ---- 1.441 ---- cummin ---- ---- 1.340 ---- arbitrary ---- 237.252 79.717 ---- sort ---- 261.951 ---- ---- Linux(x86_64), Python 2.7.12, Numpy 1.16.0, Numba 0.42.0, Weave 0.17.0 ``` ## Development This project was started by @ml31415 and the `numba` and `weave` implementations are by him. The pure python and `numpy` implementations were written by @d1manson. The authors hope that `numpy`'s `ufunc.at` methods will eventually be fast enough that hand-optimisation of individual functions will become unneccessary. However even if that does happen, there may still be a role for the `aggregate` function as a light-weight wrapper around those functions. Maybe at some point a version of `aggregate` will make its way into `numpy` itself (or at least `scipy`). numpy-groupies-0.9.13/diagrams/000077500000000000000000000000001372771202100164005ustar00rootroot00000000000000numpy-groupies-0.9.13/diagrams/aggregate.png000066400000000000000000000220561372771202100210410ustar00rootroot00000000000000PNG  IHDR sRGBgAMA a pHYsod#IDATx^oLg۾خ6ͩʞ +6Rϗna 4YusK,F!%IJiWHA4HX+ѴI): EOtU)tO{g챱L~4J晙 晼h4J?(!!BC @7N7ou먧GZ + "xg(;Y&Xyo`FHGaQHڐ! QU!NdsVO?si']+sVsV݇V:ֈ9Ĝ|gM@"q'RX^(ͨ٪F1υ4J;DI.rlM!<{iAPclnJ]‹O;,O"{~>hcd#& РY "A+ىڥM "moYi)'DNMuQ.]{<* Iۇ$@`*P+%ו)4T`+sK+$], P´䬧)%g,Ӿn؋}c^ 4FJyH6. $S/?%\RI5omvȋG9H0Rv镧FT5Ԙɝ w&([֬W#by=~z]~wȡVleбֻlݮ|&>>dw~ ?J1SwM/aP/ K[RT$rBHMb!XT量"byåDG4J?b4aeSֱֻ= )ӈkXlR[}&f͉#$w( u%VT-E!%).CEW ZL:кU.^vѕrl,wRS~&m< {zF5طK/Ih^IHCFi_YHjl8 vG~*v]VJt^2uDsiijj^oQ /`o eO`yl.\/L'vo/%ћ>q9Gnuk(L.y{mOֽ B"5H;QI @ӼMS}.LRvf6}E`C9ec߻Sjh mh4zx&B!O xܳ/P1uV%1֞{3G<50k$o ӾWV_ 4o`8"/2kXؚ]&\JZgy}6m BQo;nGռڡ:;}dr$ܘT6 Qt?NSźRVT)xQF{Y`&B,:-$J5m'0z"lYxO]J!4N|R aӎHӣ}b YkoŊ*2V5gnʞU&dy;{2{6;G9r4ia1b62y2ioΟ4U^-vVvOTTVtbɿZ a`#:DkL$rɁT*(4Jڏln>on2Ҋ n;R0Ut)~/biRUY3$6:ؿӝO{fOkXZ]젫#DL;ogӛs颲9j%UI{HcXRzuۘXko8_uU;D߰bޙkϿ!IFGMba9tg 7x+]yExkAS+]6fGc0)YObX ;6/캺GjT0VYӱ^G~ꙶ>>VWyFZӱԱֻ}alrP#a:[}-h4*f9u*P#oaڹ6IƈoaR ã?@AEw?UYQYwZZ[#,bsV5 < mHI}_I2J 帠eAd@z- A/˖=ezٲ l B[rQʟf2mHzKgSz V+n!}3,Q1.OG⤴9;ܣ;ni851)&_M՗rn9$)OI=VYb6w;c_5}QΦNܜw~-$L?*U TQ'cu6$ȪObQ=c!.kMʌNœm{n3]l\V͞5srFH 0({ߢ!* ]H_ J%z~> W#N.^0TDR0L?^wҧq8l.LDxb1%)Xio'Bj[l3O\мH8'{{#g70K/4ϵa]DaK*U3>tO$sNœy gy=ǹ[D1-eI6OQ[ @lnsyHSɞ k)?Ds&4NTn (_҅,i=-r'J=k#O;lJS- TO4=BT/myùvRzv}s!dZԃ Q~998RISZ0KB;|oWgԀrilDH%ds;ؖP6XtHu"};}].k4N|ө~[E9ݲS/Y rd ?VٞV I턻w^CRҐ걝WjɰiGQejK]6~ҝAk_頞 jl65Kʰ[2ҖQ~*=}ISWlIMW] {gb13ߣ}.q׿-]RoHNBR:4lX]4w*;"Ϳy1ibIu>ULWn(-{;ݻ=vYSzљ]O=VW_ImVNayv/9޼f2eEvC!ĝ~l#%c9>S~v5sk.k􏜡6(lV2l_emG>{m|(Q!靄uꮯmuDY?]¸).'*;oKڥ<'I) w9^|g|u}!&׻KIWu vCMq4VǂOd_G; ޘ£T~NK`خx۞ɃMӣ"q<^ܿ'yF=rx4ךހD+M؀ĥw.۩ &*(0Nh׎%7 1Ԇd=3*R&nw6SĄ;4Fago#1؋xHu渗M5sVn%֍'m у=>)pR9Myea+WhwSZ+myy-OS;?2Qdf/ns|sWu2GFWT)#' {ו/ػl[%Ln>>r`2n{qjZgU|$g}eW%< ۜJ~ԍxwW.3ӚkϷզ"yReJ HӶ,R BoyYQi=e&vE8ӲQ4o7/ϳS]bT\G^͐f`sjJjDzƉ[b:kFt]fӓ)CHƌwxcy<GG>ugVk˹k6޺keRwħ, ޱKRŪS[Y 2..7IH(*Y$( W땺|ɠE[w,;VEU6IrtbO?H378-Dn@JƷEEQ1CU}9#dџ~ ӢOX)1sVsV݇V:ֈ9Ĝ|g2`Ʒ(S C!!ByT{)#!l OG:]o/DH^ÃQX`m=ACNoI{eN9 IHieڒ2}{GB'Yk4 f+H&x$.ޱf%f5-us HJIX!pR#>UBrsj#kf4mJ.w 9YQF7!BZlvqQ.,Re;LO]bx$i[iY=_mZx"]m%&ʰ]FmD0k+ę#ng{ufrrҡf" ǾvY @"$=DHz<'5)de(?sb OX)ǭt7/Ŝ7'tޮC C^#0Q:5ŲR+e 5cZ':ʖ|u5r&,ى)S7=FQ?iK(֨^'H^rI $zYXr!5:YBcnQdXhQ\P,=Mv\|mzYQVaqM>g+Uj- ?k멯]So H~aT]3*X89@]Op'Zë́?EiމG47vcAi-:^+Ja2GZx¿Tc$rQT$r(~B964S[ UUAyU2&Q7F8D23Bz83%YgJh歟1ӾC'n字]t\\Lz2`U.i[*>Ŭ{qi-kj~ f)t6xY.1PG=g;|OV:a8~ZJps'5 b!ʬ{mK~퉪F{L4z-X*k]]͎n9wyzE<=#ICz͵$BAσoJ[(͕vt -th{GJ4|B#'JVL.gD?^N"Jqdn߿8IVL<#7u0@'Z|ҼdНws;EhvZ &Ëg3k:^Xv9/uQ 6XA4u_${2U쐚䇸xq^@/aBT/.oӾWV_ 4o`8wz2aˡ+{Fwٔ%.ȞԘ ي E[O,:[8p'jeLhuR9ͅUn-mPzRrRI%!ŋxE3Dj@\Q/s:&D,"mHGMg 0JjuӨة(i5IKxJx VW 4SP;9mSTMjɰiGQ>ʈ}_?JNH?拻w^CLA['ύ0pdҫd{ߣhuiCW_Mp'Zi>:I;JS <}}oBNd{ڌ/tP5zhgsSC3hFH67^ ;&.P;/-FaJ(~eEMiljXM@bqv(\l.6zj`w{ֳF3z~6*8BFO%ǛL /r3PpN+5-,ZKS}7h;ȴmnf2ptӫ<(kKTS<%7хӳ@js֎mH%׻ JV Hro]<,g_=}9 HwEƩЦ9,h)iGrt5Rs>xj}ã_pѽIM1`"yVz &*(04h׎L6 7yjqs}o:ãVX" }fLq%[^6\t#ƒѫ+-CyL&9Q& -⛭ˮ{ѱ&OCo]IV+KvmHQ^z9N%O4yGIgg4Eϒ)a*I7ԼW%/)z76rdzS1R(GN/&/* ?4IJxBo|3DD9|1R@R+I;hd$*/Xv 'cCM@EQ1 91g'VzG|RY~b wJ*3(@mHz!! @mHz!!BC &>9IENDB`numpy-groupies-0.9.13/diagrams/aggregate_dims.png000066400000000000000000001301351372771202100220530ustar00rootroot00000000000000PNG  IHDR6W sBIT|d pHYsaa?itEXtSoftwarewww.inkscape.org< IDATxy\Uϯӝ}%+{@ҕDP@H#.(<8288:>.(:&%$! ! ~V]]}+]շWU[{=ǀvg662#"""T`Vdv$FaTDTgF`ko'"""R!0^ R ㉈Hq w,fv,%.]EHt:-""""""hTDĤ"ZDDDDDD$|H_`t""dNC*EDDD %Hy -""""""z+`BiT_,H: ) }YV}8v/ǘ 6἞t:""{TDq"p<:#1Dx ({-"""Zc8buZn=Ig6UT-–3K~*Vm1q[q\UqB"PdER-@vh. q⼜;oA9mcӁa9GGc vh9z-@t}3Tl %;Q`cN'`fʙIjM"\hlXވ+Vڿ<ԃ""Gztttf!W vvA4+p~83P ÜYoG0F(8Oliyx8wJJegzS.~`y~$'1FG7&|TWqC0̸/3WǴ?\q֖,?ILOф ES ]U \L[Kߵp-_t^DD$rYkw6^WfD7!{m-cXٛ߇b80V""2bRU|{h=ۭD9@]%Wu$h22ℛvw/}R]7Ku݆kw![sxj%""2^(z ً.ZeWee/ȥZk႟s~hc;Nv.nB:{Ruu7|<{w TWCE}w g΍E³BO3F!y}J=!摄2G[0cP#;{KfM6', ql sʑsft80RAѣLs({=%g[}D`aa&, ,²: \'0\̈́W4t = 7? \DN(Fr<"}p<c7º,  p:>eOx!yHtS !B%8 p`0aS;_l#|1y0}s u֛"HXj$. xeg+r52qDup{l }:^B$4`lqjt| a inusZ턞b<nm_c8繒(""hg éCV ÑO 亂+mx2j~Hpu5hBk?hqa %v.""4;/c"<\ li͙H#bPwdD{btbxgU/E֧ …}ow;oi|ycxEE_zP0l}# -!%.""_n4` &\wќ$aBV"=aFE݇qK(&7uZ[i5Gw9B؎a躜^ߞXDf aXRBO88p{8ڿ.]M ZHFnr1voe]7п]3q0磄n0gx|{gNmBx)殲s/-pT^5 |KyMq.3Lo{~3r.WΊ{P)GY3V܃r1^DwUtEtr)'=ὝQQ "pg sq#vUVV=}q?by;.""H&JSJmEDwbHS"Z>ŤSRI' """"""_IEHL*EDDDDDDbR-""""""hTDĤ"ZDDDDDD$ޕ6N'HYlK: ) ѕ. ~t"""R*EDDDa {jn 9l L˕+\}0q;xG̐h~$C{/f6 }{Db+gտ]ogfwSb"""""""1'Z}'Ǯ=X~uQ5-Gr嫸叭叭卛/vÙ=qO%""">,m fQ`%FgG Ny(I{S]|qŮbt1~HEtvpo6.-EDDDDDLt< |xp*p>p0eڨM,; >ѹaq^NMDDDDD"'I ѦLGDDDDD'K  dP'%"""""RTDaica c9`pPXr""""""HEtv' FgKvG8""""""JEt6O6:;NIDDDDDibwSѐ^EDDDDDDPݗ-Z1I5iش $DG5:ƿR^&$s|VZ ưƉ:w1q[qrjs2,Ϲ#Iɥj G \|8S1_൥HM6w7ܼr.W[؊[؊[޸bW1ޱs04g*w_793; ixgK:  ,ά 3WVf1b_L&+ʥ ӱ'i4nmlx8b7$gƮbVK f%M߭3q{o#;6SO:D6DKaƠZcPm*F+}%&Vq߸)V+?$DD*HsUDU&W"""""""1't""Q]a&H"""""""1IEHL*EDDDDDDbR-""""""hTDĤuEDDDqpMyTj"ZDDDt""dpw 4[DDDDDD$&"""""""1i8Hk }nᵣXq~![qq;ƙpLC""""%7a1v4e_6 M'*Wl-orVVh~"h ):?& n +θgN:?9D΅8` 20wQ7Yh""#*EDDDJ糃ѧ WU+N`^> \\z""S-"""R" r h۝6E/OK -)!DXq p4`p|k|bIHI)IYg """"%0ۘ~ 8e0 58DDT]aԟ M:7Мt""!3yoٖPR""Rr*+ONB*d`cIsMHYi8Hil~8n-"""Rw9\\=Rpx'sH?>h6/MӼ¤sxw>ds '""%>3fqP5+Ku[_`hݨѬ8-i27Nb37.pmEDw2:dR`$ܳ *ؙt""3* #u P"uk3E;nEyv=G1q[q7Mq㊈ d yPNњ}qvR c8秋)\q[q[qchD1^R؊[޸匭叭{;3ǝێ4} ,SrMDDDDDDz_EDϦqõ5o[H'.x/p@fO ~> {f1|wn4?go.➧gRlS4̰kpo]G=0xG ~! w*v"u~? m ^36-}T.tbt 0|c# OoKzxhUGΗaQ*o`m@Sv_*؟t""ۿUDg9~ǯ1R vy3 87ecq'pA\ L[ٶ,]>T6:}*L,] iR!ävYDOߒ0O> Hv]-V`l.%'8}mwnDD3 fwD+3Mޜt>"""RzZD>I 6Xfr9O@ǧdH;o6-^_D_2qS yߝLOc)6\DDD *I~S_w5T8ܗ~=Unﴈp<`^ff̌<!"""ltG]J>ֹ\YDӵ);w\ {q."DZwotdpdIHPq|mo9qnd|LˀpSMT,3 R̆D{&``P v{[xh{SZ3ͬS\ lpL0^w=E9OTص[pݝhssi$ }avWlmÇ7uw ZArHŽ!L& gQ1{pTc[lo6 2ǀqGkڀs=f<0f2) ̞s3F`cw*EDDT Lr+͝>}B5lL>cGqnP3g嶭c+O/ꍉ ;6¤[ٟz =2`1pwvTv48Pyr!Qf`5Q hf*` yszEDDDsSK?nT=GX03m/gHXy~ Zo) >`s7cd~VGwSXya7dhmH31R3 /%(DDJ_3݄#Z}i~3DRx<4̶iB1w7W`ή0"x X}f&,q7)}gEyL """"Qc!.sg&p1 >GC4joTM[: 8aokd6l_p؇>[oC.kȋ'0lS_1=m-7,2n9c+ny3c94K¤d 9v<&a^w_\E:gXl"Rbi꫁McFL: π+:l[ ǀ0OsKö]ɕU+;+CçŻh{l9e2rjr:>X BY1 D;Sp)ϝ+Pb˖p(S2dV.Ws` d4@Qɍ4l쾙T3 OH:컐0CiN 3;Ph/wf6hw4ODDMEMNֹ{+,Y0Ү68H_8iM{bsMi $F>t"M'̟r@ED]rvnoD?]ϟv,fc4!5T;y̬P@C;R&E#By%9cG4$-w20Mv{_SZbaqbQI!"UTO=ff̈́ٹ;N V"ԟ +Ь3kw:dsNhݴ(lX5=tyWYuծ٠#.< j+a·5AEt幄 Ho؜to}VUyi'"}P{znlaef687ك IDAT| Gwxc:)TQm!"<|_NEDPD-c/̎ ND~Äۻr/p}# y1azf|w9yeF^:)_=ч:҈ؤSgTDH}m",aum L&6; ٵ3; >2ꭂ0zeIS?<) O(p]# y4B߀dz%o2'1윱"2@NXg Lh`rmhZ~I,4>c; -=D!F2֓ ݜND""+~;V#7rN^~wT`j`uo"1הoWn\+_5Ґw#,{5>\y{W;}o91>\"HE)Q13EkH1"疍5xgDE~&p+piWm9Öe-/Ѻi&o_wmgf }3|NJYxikFRLXжi%d?PD """".M4WUokWp2Wע/Kl(3 +f)㥩 )̸mζcMh~%#"2+E?""""2C {V_{UGMn(pͳ[l8ES-"""s3()PNJAÞ'unK{pËlu1-!|Ow4ng.&f⥩?(bbF0RDjtLDD$I;N@zj4`㫁N DYaSjײ _Zƶ~V[g ̺ỘFŨ&VG?oݟL: .F:c6vi/# W>6m;[[|}fO[p00Xoy5lBўn(p$БU̒4vȾ̈́{4hm4[DDDv`QBI= ahxvr;&Pvww,~бQx/5o秛զ7?7ҠYD *EDDD4=ϗh pk# uifkSx9=t.IH"դc'鄖bWirGDTDH5wQE ;8~7Yg[gS h]ǘ3w;>#c+/Ms~4 6i5ϡ \ض}ow\)z3pUP-4D!鄉MeeHÚ;lisk|7v(x)ch;'W3)pގpC# 8ND*h铪isi&żvf1GUtoƮgG'Ԑi;ڰhiǀTo&i !gjd~[D78ݜMu{,MdWWS;ݝixSf5/ ntjTWLÿuΚ#F8o>k" "ZDDD4Ӫ'w;S n6nN=@wRlZa`t_~ciYkN`WX-66 oi4ckOC 4}P>q7vsZUxaS_|dϓy/?WZq""*EDD/۲{?XFlNggpjy E'R?S5=_K4ÜuDE߹(zi{  k)N3GxvzF7| RQߞƌFwsރ4 ߹۽iejhd¬"O>jAUc)ؖe1-T0#>W/HS!3  NSuW߷ti6BRê^r%5S>%;"uMl2-Z崉mP w)1"""ҧy43;/Qy/L§6ߧ_Ut<=˾d} 0.:r,4k`ynƯ-g˷WiʷvY|oUD"""D@ H28\SE=o{r&@[Ŧ.ÔO3]Vw'R-"""wT53ܾc"pl/ { O6Îw8_'rQI3Օ[?Zƛ_e}So>CvwZJLD""",4k3Xhw6ؽ3tiŤ618U8 X//p/I"{G]DDD%'tX.<+kbqs=cH(~h'o uHMbIOe*E*zEDD_r+gsZjoz[kGP}Fܔ6łO/,dz1IFi"/v =wfL>v}Xn0+C{cs WpӲGz昏Y]$c9cnwǶwq*3hqzbIhqϫiOG/FP= `.dHSx9-sݿÈ뀣hY~cL !Qng=5Z֩MĘ:4Iq5խ?^bw풌Y5zPS |фh8I[5!4&Š)KӰv7hN^=&l?0xұw@էBN4fۧH퉶="~q9@'lfwS!"'ZDDDFyxKw Y}ۢn-ۻ}PNOs+fa# J8TDt`fc '!*EDDDD"f6p 0*tDR-""""̪ #"}hhfNVsOETf4pz҉H%DDDD,. h)h+ǤNJ:  7& $oI'!""RV*Ec' 'd.iD3 W`ߒtmfUI""=`""""2Pd71ia,tDDm?@"""RzN@dݗ$Mn>^dWD?+z= `fCCd 0%sC[v}ؚ 4l&*3Lq4U `l}ӇmSYY-1z."h~hW?~qw.%,8%KMGDzhV7?&f#"NEH_fp[$aI!"RApI: 7o䩈  2'-w25*8 y>5%""""q6w_t2"N`NJ:~`WtE=Xe-"""" p?t"".g*4bzl%\լL2Uq3: w{EDRH0lଋڍ wލjm0Xm!6UΛ~Kxx5N'g1؅þA$ x182zg_d?6jc QqL1$YG4EqN&Xk19kχH9ymauWta\Ww('f%8s4e,qwY#R!z6;eǘqƙe-O8C|>ٖUL֥E3`) 9&*_4h]uhFDk="8Sp̈́y0AFCEDDDR4wZbg ജ; ^B(rq6c,fFkC! c% ~<} 7azzwq E(vxgcyY֣r7l,`,ar?I? 8O邙 |xфk?"0u0P8NеsCg0^#r)9 y>P,KY˳lsx४r)1#SlE_ym ?N׏H*EOxâfخ>+{)f +3l()`THT"0q'wt^Ωp6t""""R^fV \ LN8J1F0JXu`Wr_G{=""""R"fNVK)4byky?ɳVN:  Ry:DvӄeOEDb)Y54DDDD ""L*DDDDDzJ*`1dDR-""""&RU'mtfv~fw$-""ZDDDD4w$2zW%.poI* 3R-""""EpI* 3|T"R*EDDDd ݗ$谤R-"""mak*d;۝*3$]q4U ’TwlCADzh-^t""һTDW18̧,9DDDDlρ ɦ""IP]Ye>O)9DDDDzܽLVT""""""] |J:IhZ{Eim y^D}Ƥ68aCM 69,nt֗4?+Q.""""I&]*%k#ZOGGtZKp'റ9$Zw DDSG90`T 3__^O6K̪!CEt _[ u'=m < """"E3+[%tgS`!<4GhDDDD'3;̞~ t>"?'c mSWu:  W("""R \p*+SC&CCDz@=} IDAT^}HXђ$Ii' , M᮹CH#ϯlp=T?ђ$I).~c!7Ժ=H[b]D-:hy9H/hI$ w+clG9n+cZ#Ig-Ij-E1 G-n..rW=4p,%I4l \c|QXK \W<$ sgf&7RSesr8ђ$ICЪXKxwhd'RAUE¬*M&-U hҖT8sUgaYTj-땋Ƴ$IhvC~\Sej ̑? sO0jr꫱Ld+06 |@˳Cb-I$=k,>y0;s/¨s~|@NRYDK$I@4RH+n$ "ђ$I&vy߾ziLwhe`24YDK$iDj4 JM+JM f-I'G×i+5{~ILҠg-I#G灓+ЫVe@&ih$IҰ#p 6 |@ILҐc-Ia+G~2:hyzR4DYDK$iɑ^Ғۿ"-`@4YDK$iȑ|0B[HU5yI>,%I4ȏ Y @˰R* BřS; !m1WRa<cݎsBcH q^;N~T-+iw(`'ԅk |."M \#?FZ],_hL2{zx%Y庾$ih)4F1^HuB :40'~yq~np8fjΎm1>-~`iU1!ci<`:0 !,1>{dbq`QBD<iͩUD ɑxgv|9&ݏygȟQ-`9ޖ=]I3x铌75!ڥrǀhLn%$IZ)[bB@*kH ´%1ƒFw!@ŵw6t;$iy3iI!VR )si0sT5/*>T/1.>4H+ -{^s_ \ xcDz͑U_ȟI*7gh7k75"5Io-9羇$Ij&B8 5Ƹ[H@pIqw ݮ;x!n-iˬRIӫWca6iVY_{SeM xjGE"ZR<# FI P 89;b]|$G~|-в:G@ˊZŖ$I!DmΤ1 ![|9['Ƹ2R=ʼ"M#_[.ZO\k5nhuqTBJ܇~?Һx:ݓǣsVhG|3p=4u`aq7UI&֒h˂ QcmG:*]nx|\tT㡗6IkN2to@JWw6hY# U xjK҈} |2iR%[d( !L%/6fҭCO$IAmn unmbR~.U,x(֊-3!}6i+7q"IY` iA :3Fi{ͤB'K&Q8Cq)kFྞeʞz9jQ8'G~lhY@٪/ϑGI*'dwDLf⧼ bVKb[>$-Y-“1F%(IRl9?-{P6RbaF\Q يF*<;[ɤ=ʪL84Ɛ>l+)˵m!Lf590̹Q!>c|,~t?U -Ip{nzRVG53Zr|"))'ϑ pcU9Ӏ>@K;Hz!G)/>{{wב62=4.IJa_ҽcB/Ƹ6k7FqC1M%cd$҇Ǔ>)A)1\ai)l<c\]ArLo %IJb!GCBϓܗ4`e]@˟s|T0f7e/F#JGw\༬KOc*r[#Q򯗜8EtDӸ4M?Մ㦍m}hm}T[߸g.]TBC XI=4E!{bkk"n!;? x]=N!T@΍ˮɅn1vF#N$ ;1ل9N"}(=TЇGn*caGr$I /%Iqy'fOt;ڽ~y*9{˭ [d1q{+I6[4ȄZx|B}Q߽NA6d !~$I "zdYIh1.J+:{k@ !Afc%D}:%IRYD ZntT*Ƹ9A4GTa'\G&mI4|xO$i0(ʜ֭ݠGa-I҈H$i0xg~!jV4(g*oľп$IR?XDK'ͣ1W{Ź{ QcKqI"Zp1B3p4iu҅hDnU)*toI4YDKJ+peV1&SIV/6>U0ݐ\OIʕ.\E6.IvE$i@d9MϾ*+Y|qssb*15ն$IÃsK$IT%hI$Id-I$IR,%I$IE$I$IUiuI!]<IFjĒ$ITOEٗ$I$I*tnI$Id-I$IR,%I$IR&7: IF1ƶj\Tc\$$IRզ{4: IF*sK$IT%hI$I>ђ$i^ht$ Sc ,%IZc4: Iǜ-I$IR,%I$IE$I$IU$I$J.,&I4XmUXIR]uYWE$Iv[NBFyѕ8[$I*YDK$IT%hI$I=ђ$id 4g\5YXnM;L`oKk~}D -m_3q[[wNNjBnb}@$5NH`Cc74*a!0({k"+ v"1ma񌧕ޮ\cSU!\WeFUǭgl7n=c۷?\W[ !QhI4EزIg#$ ~ђ$id4$ICE$I#U`"0 Vf`$ XHGIQNt`#=[FXz>Vokߋ_㶯kumpf"m= :lW$iI i `49kIE8sIyi_w&]`m^nvlY%SتTiF7gyݕ-^&IFVZ?SE1979uUv_{_cןP{<.d!}-z{ӲI`pETOǧԤsXiͤ{!-$wHSI^$I>o],2Hf|Qarq 5')R57ާ+#l[4furI$IҀo]_U6f.ӦnCrzrWG^ Wrl12I$Io],2wPV/> eZuGk]DR}F'=-$I$i~XMZumuX=ݙsOX-ޏ=m4:0pencO7\X'nWp}k$I$ M7 E`wR7؅+#XTxgFjU>GH Ƒ ;biD4 7gkk}qI$IR tdq\` Q`9i:rim 쫣$GI]¶ =HdoR] Xy>EaR~"[x ؿzȳ% }b5S$ITVm#UX=+"`QgJGn|+.}u%I_DV~*Ӹx|`O#vB/+mX|o˵g_2Ќ[ƭl7Ė$I@-F]V_Dڊ)we[< E}J I$IZI< mQ3[喁HH$I4buդ7:I$IS4En!-Iٛ^4 w/q۸[ƭl-! {0h#I^&Zcz‘9L6oU~d4$iG <%I$I(hI$Id-I$IR\XL$iׯ$ 5$I"OO48 IR sK$IT%hI$Id-I$IR,%I$IE$I$IU$I$Jђ$I$U}{ \=l!\㵳|]$IHڱ~'+s"ܾ|yIjh\j:wBd}a"w?̾ooD913$i$98}$E!Wm?u_$IU݋<8X,~Z,Q?{fW"/i?b "xEv.Z uk{4J`5--Dk(Db.$-I$Iꛁ̹O"_A? }4 xY!,.>ЭW"?vEt.-D]@!r}Z,a qfz6ncqǞɇ؍T{# ~?=\0KNi`,8\9{b.N>|ِ -p rb*"_<[ہIi%m_B-ȝ}h_ !Q孝ݯ/ǸC3n=c[߸cGЍcn*,x.2*IR"é{r=wsEt_(r%m |T^ K*|t*m\(q\;%m_\܇r h-I$IRC&FrCI_wc#YNW"w8B1BdHZ؇$i^L̸@ Ⱥ'I)sQ!-5[54"`/#-0VB5t|Z  El[TKc~H L\wITri-x1p%pM =-Ir {e /Ī iFl>ҏgk$IC/HL+D"SCo$I7#_^JZoKϑ>}߅HLB:O/'OWH+|>)E{Z8c^$ %= |O؁JTKQD/+- ! &xC)Df1%q&FҔg֑ժֆޛH.kҽgiXBk 4u=4_=Hw"O>q>Ҫ^G-Xf"ϗ+D_'чWҔqI4H;yC0 IjD"]lݺtHEh.pLOiUܗ=9*ܹ\0=;}bo/D6"?~OZ{\#s{Y$ #n!r LMTC1o. z/ g] "~n=7 [Kp^.pQ!2M>|&xX DZyҖY5 ~z4-)DNU$i 8+ q,\ JTSu_4UQRѾpiˁ[תcfތ+ [Fswح8H4} ҇gwBdI5yW"Y@ZVg H%IR${Vi-I^dB\`,9nO}< 8ݟKjC">X| jP$IƘq2Hg@Bd};r]Գ$IC-Qߓ ,ɞƥ%I-t щH$H>|x{r$ؠ*ծ?+D$IR 1e)@i\~/I#ޠ* 5{Xўiqxz6ncqĸAe>T5K ۱c6"W.02>z Zԭ1GB8PqIz6ncq;\[!2[I[0-1>Ї܎$ Q1xCkh@kizVK !QzlPDkpRsU*wĭgl?qޱ%IV}%I$I.$I $i[DK$IT%hI$Id-I$IR,%I$IE$I$IU$I$Jђ$I$U"Z$I*YDK$ITQN@$I#G,<ѹ hY$$U"Z$Iu#? 3|IHE$I&G~p0 >]AE4XDK$rG>t&NˑUѩHE$Ij&G>o> tF'":ђ$IW,צ}Kw8О?OqS(4Oß:돃!^1zw{η X@KCE$I%GŤűN-צk]G׆{w~#/cCx!xg/C|~4OKW[<[mCIE$IvHQs˵m]q]o>r++ ! \\KO?1v6:^ޛYȨ]ҕ"z >"p}!^^e[&}:jq|iߙ|ˤ+G=oBOmbGdk4͋Nŕ&TT+xMoޝ>u0cu.iv;Yz,08 bUajA;:jqݡ]٤!/G~6iwRlzSoRLc{1|9Ƹn0ě+04YDK$%?˵Otn͕>Lд &s!ޤ;1CxR߶]̸hO&6ZIE$Iz#?x?a`Zvm ׯT9k* !G*wдtKŧbOxrS0]*tӃ4\60ͻPf-Imȏ \*n_h{l%yǷ9|(oOW:$p0{Df_1i]ݸegcᅾ](iP$I9MH[]W/#O=oxN S׀}HxBG'χPb3sݿx: B߆7޳egMegK,%IDy]_Ȫ{5B_eξB8"齤=#p)^blm]5X֗$ 1ђ$I#Xirmbg\7xr٥Y㥤~TieزzI#?؇)F3,"V}w)#RICE$I# irS!ҺO? {TX(uK\BN[-{.ip5f-ȟAJ[ZJF,%I1^@W؅\m_hg7t^9ٺPrڟn}t[%dqٺYx9Ǔ~i/O%Z؁k% Cђ$I587f`;»k|7j/9K!eF&}P/1%fM}լYwcz'hv4YDK$ 9M[H+US]׆eaL;_h\@8B{I+[/"xf} f\'ie-!;-]54XDKA.ʆ4_= p3"2-q5vTtw}6H| :ZWn=ʹoy彴l,@;0k͵{8NǞ[s++6T?r'|X6]:oWh`MB'ӿ,9]ԁ7j1VGϑohM"Z$ Jmlz)4ߟ…c8Mw:B8W~(0("to^wNk !C:s;`ߋ4Ҝ2.nmOSO)1]Efy<tQ=ʑK*({ umdTi|W l*.p(it$i[n<&bޟ܏}veZbI#E$IJ!@̾8W^ @V#_c,;R;&v}2jJ[Eo/wV<2ջ@ˊ>],iD$I u0a/8,›!gν{۳7+i/>/ᮝ`ThNJ㸏v2]J~~sqwOnj |x-;ksw7N!$->R/4f\'.?1{U"zZWv2e5ƅ^Ue[χ6w$P{/|7VuRgϾ=n@ Ĩ7/C598q]Aj -k޶NM|5Um=kXv|\MV>z@b=­p>\B*3 [= cV].p i'Ο<vۤ ;g^ ΅0{ @祦m6cYs321J[Ib,%IҐDη)y-t^g*;9~#}jg64ޛVj1r^W;sIg-Iv]r`V$no溻*#'x `{c] }[vz^L5B;+>+8]U//_cBr'I!)Cް#ůVhp;@]/N-gv&UUA|u;ų_|#+zd 6}?G%IҐhLLCr޼5kvbhxd Iۿ 6s(KqB⫏oWqSMn%D2T\_㮍e9vm M\GMc̱i qyW똣7fƛصsI_IfBS]bKsI e-I{]9 F/!0/@'pbo_q>0\(w;@;M5Q't1 cZ#/@w}K-^C&}@·fo2bQ{wU}qsfDD dVںRc.VEZSu!T-j]&6 dLywpdL&zߜ{fr[s|'/3+..vt2"""mqa ~L2^p}{ lp[9Lgz3fUMsl$0 )w|rkGe4|9;yK3p&a{9)""""%Eݳ+kG1_ #1b/Wm\?4FzoWl1#cK.f9R8W 8sS@þN|tf6ph5VZզh~r{z .̎&;:TDd1Q4 IDwQ-""""^Ϡ`E*EDDDW3Amh]oihbfv=AE6iV`>"""݌^n7 Ͷ316߇7v^Rܮq3CDN+vB"*HG_Ǥ9v["R4{]MHOV6>1e"ݜh鉖qKSwe5TE6;^hp9kwO;3;!0عHP݋0y*Zzw__dhj"""LEtk|ݗ۸l a||sИh*d h3nQ-ңNH7Yسָ{~G<=O~s'n؝xv. 7% *EDDD`c06f+}v{b7+W  \+n:"ҙԝ[DDDD$?Tw_ ZѝhwD"iJdNnqb'#"]h::w_[dDkR-""""]#p pSdDkS-""""U_,v2"=jO/v"ҽhvnUl]عH"ZDDDDz3cf/9%ԝ[DDDDz43.v/b*n4PZDd;NXfv 1*EDDD1l6p0Ǹ1^Wf;^fv;POq eY>]Hϣ"ZDu_؉Dj fu@[ _,']8~Op/Nuz؉|{"8"~sHg #JaYÍJ7/:yO2{ [Ɛ缘+10Xr_\2Y//q6L2JV,!6aYf5vMNͰz>]`vVWʸYe |^4gf J.-Ȉ^]wy-~|.{uÊ9β%Z當5‚ ]4Ef$yuB ;b0= '`2?C=Ȕ3aG:*Jh6Nrq]mP8c0/^MEt RX,'߭9(t8.y1rh*[w"ٓr^8,r `tau3n8[vgn=n~cVӗb2X̘caY7|-bq-!GvNƄggNn:sQ5j=nwl/t}f6 8عH"zJ 36})Ċf1+~d4>*`bp﫹㞺[ql'x3szf2|lTƗnf*qWq0b }@Ӵؽc_'Ex5Lε& ɮg>mq Ct}Fw%[/~0k9l5&Vey@, ].UVֈLzv'}B,u> uΪwwewݮ2>VF18x0ʼ7-ECF h~@x,VtDE40MؿR'DPNfľ\#2=s7iarOyz &/0c|z>Lnyxz ME=NDٹrqtəLV ;j<'ja`l9:/iY`(5 3=Kan˜0Ѡ0Z8zN /VNxsIqc;CMGՇ 93E #,SZ "?nSֱ-,4XUsZ-ܸ}ND ζ} WE[i|}9oe?*x'r?'8jRDޔмYDOBDKD]6:gU} <9HxL᷵΂c* 1Y뜟V'ޙ]ep:kk8L6g =Y18>'i;wM 4óqa1) chÉRΣ X+@,)p7cQq8$hg?cbpKUD;-{<:֭y/Hu3#bpC歩w>8'bBg'ffNx}$HQ"`u:gT*|>(컘&ؑib"yߋQ"R"Z{۞'/&\`n;Wٿu^',0mʷι'lg5-\繖ۢᰕӳ=D9_J9=wCnј- Gۙ-6_-Jn:dŘZgm͢n9zHn%;"""L.۰.- ڴ%8o*;ATϸ۝[j e!ߚm`b,-"]9a}VD&lƴDmuf6;g`0WU=5I]p䈈t/3PcO Xavn!w?ݷkImef}|c1"\o0>F,Hq:JQLWπ_^MQ:c@b\dbj Zjwd LK 1c<'_IwpX;6Q~!7.=yd^WL_U$&UlOl_ѱG2rG,%ݽa!ffqeACsEWz`?5v2}m40GGߣq|vDCl2kn ly_q1G9[)Y>Ynh^QΆ3ācv2NhXQ&*T^H|8ePHqL9/үӾ +|[DwY@,${+h#}-S&|M'MH0yLMSv ?#l; fxߙ~ĭ~r"i k0> Grg~g )ft nJ}6/ޖW;v8Nݮh.7؞8#RI8[1G<&E͟+ i,Y,t8̎6]b "+sS.H~1uk 78}COU|a.;C9o57A)`8,7?0- ીa)jWBD\{` 4nV `VQT3ޠޣێWQݭy\#WvE[PkݽCl 3\ _Dze b/qR* if7*(_Ȋf*~fp)YO0}8N:# -0`o ps]~~ ~2$T1峆ĆLK?Lcnhl Xʉ}{^Ia+`ūjpn Y_k4yVS>aw}y`}: `\3I~ձ !\3xu\Ts; gudPmإW&**c ]Gh/8~Y=3_ i >ŌGHbt&A)jn|{@5G?Ŭj9~8Rn>J0(.8 έ'6/FWKt%H~,0*=k=5ߊ}ɰg8bjY-YǝhZ׷QsKB7&Hy4S.^ Zv|il Sld =@E߇I0<ƈ^=Ϸhzph )%rM,S˽Obr֏؀RҵY6Ef5s:ӛ 5R:"7XV ) l fT3et3:M >؟ 8@XT``W8<~\ 4?+=#giX`eN#a!< J3wF~:a/ L9lv1R̜DGK H)e4;ѰHGٲOccH4RG6u.MotymiS4m%ýg9v\ ;~=c8n<\3 +*^URrFa?ٰ/FL6|` |,5ULyx?=Ӱל/+ aof;u̸2~|S oz=D3#gc6p<nf)_CcQ㊷Py'U@.ńˊtYDo]^BwYm78"]YKe4H ɺfJoT\dSfu23)E98 9~Gͥ0ETиa |5؃)fl꒗`>`rU 1Ox,nk)ݿacyΥJbF 8̉ g_ C92 pFoy-eF}59 8>Vf}ڜ 9 9 &p>c' ; =V6Bntu=ŜTg!޽~sG; [6b_dm 瞕 ,;ҁf0 IJ'[)x4;Q?m9vI4@3B H;夽yo2Ly x[ ݿ:v0e34@D npp5Ƀ0d8/&h~$sʭ|z)χ6i~)s @00NK3t6- rKS{`6#92 ^JDuTDHQU߀' 2 !\K= no.7IfS{9!-*_喦*Nl7 %Ti>7}/oȑ kWK7ۊaT<5(oK6f<'xm9|q so%|&ح`fZ8vz 1xױۓt 9ZtDTDro\QvUcWXQnfúHq (]|҉y N׽=<;n|{q <^ˉbiZ$pq$G:NDٓnm.,۩40҆ض @3Nlhpo=5̮&sowZ}D˛GXnmm .ox~alY [l{,_XM037Ϟ7|)jE]p>*J(AXn9)V/p=k MIxıe'ZkpeHaVDou}d=͛>Z#hT\3|% 0=0<|Rz=a&v]wR̼yC=3#湈t.3|8Hw"ZDDDDz43OIDAT'4l"VK.KݹEDDD2SWI۝ ND6""""f6 YTcw+b"Aԝ[DDDDz 3\ {%EED:hl SKW2r.b"R *EDDD[[@YX\ \,҃*巾fMFs#ax:"y4;H̬xZѝhvY 8pҰ^ߤA筛P7vkb+na:Hgf^n]6mIJfv70!Pw63-PLl:ā;L)EDD<3;prEDҖ!y]&n!cӾaDDD.3L&;zTDf3h^ihlp!pP^c$H+D|<*EDDDE8@d9pps!!"KE6iV`>B A2|{m4m,Ep4CDDDDz p8H\^@|9HN{] 9pI#h?OQP\DpTDHOVGX!w_^Vb ^VVzCD""""3#gc6rN?#fv~'9CGc?Y`Ǐe#)C[-. EP-""" y4[koe> 3A2vЫjp5MhH^un?IQs2BEznWOvϭ aw;-9RTDro\Qvccg l-hN$>qEDD)A'L;nGb;jzpAtq*EDDDH,.Amz8/E:'3.TDH > ػ SDQ-""""=Z4`t^.~3IEH #hp5 tk*EDDDGI<6~\fU$&"=htff@_ ltENIDz=ˁ3 n.OQfg&ݙYЇwRѨNÀkftד^uk8 i&H8ak \IK3 ڼx8m3N:δ/Eh;پ|Znf]wD oj; 濝HOaf%!@voZx*ED3 }YQ3ew_#`.0`0V(ҫ(v""w[.}mUDH0u~t%ڶ̖r`'b$'=۰{!JLʒ ۋuI#W7{|(f|y#EMO/2a4M]UDHY;Fxvpx4o{c.J8;rw_"^ oEv)Їiw_ff;F Yvgk>/w큙bOdkۿF08F]P15W"St> m*7=g1ӃJsj1ś]3]ωƨ@sJ3˜;3򖻯T ީө0\ c|E٬F1֘٪q;E룓Y< `}{ wϔMc2#g-"j5c')j;d^4Y6?f5zw_m?¢)3=zl,77\=I03{j;uwr<“w5<Xh8Xط;0pB9-Gx7ewϾ[XJb9Llq2^t[qټzwpkr$HQ"mlT"zO;KEwxVcѶ Dݹ}ynj',-KxѬ3+!<>ϝd3Ixb{)>Q -""]5_MXm}[f t+/A3pt}o7G;{NHeǭl!Ǿm9w kW˱h{@thLѺf}Zٿ5:yov ǁd,"""=A|y1]Z}}QkY{U*EDҼ_C*;-YY+uDDDۙN79w{oܽR[NBztњ gCmw-_2 6[t?qkwl3g.hɮYaTD*;VΊ;-WY5u%]FWs} ""dC,cݣ]jG3E]sL8: \6U-""]._{.Y}Hh3=vfv ̈́kTґ&ˋ5 image/svg+xml group_idx a result group_idx a, axis=1 result group_idx a result group_idx a result group_idx a result form 1 form 2 form 3 form 4 form 5 note ndim(a) can be >2 and ndim(result) = ndim(a) numpy-groupies-0.9.13/diagrams/diagram.docx000066400000000000000000001162711372771202100206730ustar00rootroot00000000000000PK!)e[Content_Types].xml (Tn0?CSu^jdxdGprb"6_q1x*㗥˿ IJ{@q3aGK"ߤDPK!N _rels/.rels (JA a}7 "Hw"w̤ھ P^O֛;<aYՠ؛`GkxmPY[g Gΰino/<<1ⳆA$>"f3\ȾTI SWY ig@X6_]7~ fˉao.b*lIrj),l0%b 6iD_, |uZ^t٢yǯ;!Y,}{C/h>PK!4Gword/_rels/document.xml.rels (N0 HC;M7`Ch.+5K6IDֵGV~T7xΦl',+]mEHrRd*A(XLYIT?r#0v5ؐQA!i̸ze3Z)EٮOޡSKgSJ˽ʑvY)O(eg82~cDž'',]hSAa#Nc24!xdrgx85lL,ebSuJC#6o@NI;ݘ xFqPw?PK!{+Qbʆword/document.xml}ڸtyt6;_x7aߒ :tOIvƖT*J?J_b[XuO]FwTW3x$2A(;lp׃]Qs֝0$ԇUz\.PW-lS5Ok\0U=s}-G$( `d[)P~էI{8[p]&l:g2?ƚKˆ'];W39B;jFpx]6-._ JKełH&$3( 슠lAHF䤻SRY7 ~һHDz>)iWqеJ,[wHXP]18<܁" OVB$V]˅!.G{gt$T}urʁjB'0] )]q%8$XwQW,QQ+%)TNHO/s|rcSG6M*@ bY^.}}):v+фP)IypoJ$T}SC|hmC- ʩ' |pf Szu(~ءIn? }_\`&~˓yb]\Z9"s0 mxXH<]+z =?%,[+q;ǝwqp\JK@ JT9}RӾ85?g5y\F ׉K}G=K80+P:DN94HcMtGe]+5L i)+~]Aƞ$^1fD,̴eX3cm+SKk|^XY+ߤ~B?PkܬEڭ`S~ӝj 5x+`v(S(\03}QT^Qvԕg43Gx`FC0~у=K@6ot(8*@*t40qFU7z,yFau$Q2drw]XXpa L'x=53DɌX!/ys2 .p"zE LdՕ|~ y< 2~xM\Q<ɾeoa̳/`z2B;X@PEajXI3!0'y=u`M߇[ϘD09نif͒R:v*jJ> 'i O,Ͽ_ `TYLaw-]Rq֢05[T\L Fx"-rVKm#Iyc*_NFR&*D2.")_g-ظ0R3 iRL%Cz|6&42<24fI9G02PGs*.1!SU2T.[_>iVްtmiAzP?Be>X \#/3&ďsG#jx. w!g|5bEQHRN. QXtLSu-SXHٙZ*2pY"p"z#uw/:K*Ai<*q#Kk6߃5ePލ '~΅T>K QOuo65$$K#@,^ ʥR_(-}Zan 7J PĊ3?cRvYGk9Pi q.ZicMbHnZڌfkQMQu.U%i='wjΒFm s82ؕ]hּ4z mCvKuXMMY:Vvsl_:1JAߨ4|.ǢSy=a<:6Kc`-N˜]s][&7HY>'膥qiXRn6mi;U-U]bGfvMTcݖ|O>fz+c%+gVmNT qcefyi,iu"Մ1@S(櫭%巶b.6ɴkn E͑/Sy;"-2i"lzw,,>wơE 1{Q`"Y~PfLUi3ژ8= "vt·>Ho.oKelZ K֕P2ܦҖj7^g8U>3Yv.fKƇiX IUN^Lh2 ˒Tɘ 6W%77Bmb*t5'SfJe(fm- 6U0VZ2Y~k jd.ddV 岭fw"Pn.t6LF_rBmy{n1A9*f݆\O;|5ҝR-ܗUH<_5gzgDt;im'^;h; 5r9cn_zFb c8\%p<[sw֕M[ %Zk1rXw*9L^&r[zߖG嶮7 r1="54i4ͥکMcf%TՍwzM[}gWd++a+]эtUkK'+\pJAKvȴstQ ah5* (W7Qrm[uq$4bd%m2IOvGj/akmqh4rp\c˰2)88 cz]C"txL5m{Cva*Y-e";ɽR;Nؘv34sBYJ-a \-bEYZ']*Bn7 E=VDf4'օ 愚1%Us[R_ Wk锍M u͸I9/4<]3ry4>Lv$ui?Lj꼺#áCD 9yQ\%/ŐbΠ-)L q&LaM65;a'!+PA)$bl3\r4tՓ#*|Bcc`&E~#%AZ@7 >qIۖ<^} 8,|>BuBRiqOA=ϋ=˩ >E0ss3hC&c:^}Jކ8r@\n=LaC<橧S~ƓeDM_nnr~E4zL& ;hc5O-=Jb?E'сEb#0vM&@)lLmT+wc!øB&HqK\:#@|D,|Đ1d\̘16Ӿ5FRveG|7$b!ކx q݇A#/>e;r\r-,jV^FRD.A|ۇUnfS(V&=- أ7Ĥ3 p}P<qvlܨՠNB)ĵYXɔ4>2F\kI|2hHZu)+>.Dߪ[v55kRJE'G^2`,DSfqqY|,S*~\k9R7c#`&Zlx=0Qav|p(Ti.dӂ"c'8/E84ù292 _5k:.5\9'N̙D( z!K\G0_jdxp(v>UkW +tLpTg|azraƁ8iOI ][r|KgI4j,olaPXָW#q"CRxZ'Mctzz/ra WyͬZ:sLy*.fq g=68bzJiϔb ޣޒM;JWJB5ɚQMLZ̎4~gw> 96[}P5Ψy%AFF{. 6Щ.a:TIatk}v&m̋QW,=PJ΂1$Q1Lzc`/cJIҪF8c v'zsk15&ձZOG:0v#Fh 빊Q7; 7&Ukhq~p }NM'Ѡۜ5 'W%{`V@r#g1nl>/̒8H^oTiQR*TyhMsDȪe2iUn+h1]rArSn@)]w_Xoqdd/]֠t|iX=pԒ5&Kc Ay0ʉr] Bv vm>>TzM{.T[}\&H ؁N}Rܬ8 KakCxr@&~ G~:Bv NSt6F`bq3bqh= d^,!H+AQ.UX {K$ש}>闟J^ނxf/_65*z~8}@y uDꗊqC ƻegW,w.KWiրm+?kKě k7-`GH _F`QYGP:xk*| ZC1?4f xߋ%lʠh8 h^1c^(d]IՑ^?QP[oں?[L{xǷ_ ړ}}w|WXQ~Iza} ](޴P?=uħW0)7(XSyy+_^n__u)T//KH݀y (\_O5ӄ^o~*{P8a+E"J"3*8j@2U~z<^'UrXӈk@,,gKTXITG81|i*)\*bI*NCU n=TDoPU/~Ѩv%?0p+=|vO| Ǜ±`vA'P<&DXaD}*@4e&pMVySNx~AoD(4P] \SDPg"nw FCkZ%N d6޴k^K\:d }1Juk%|y`q8 ,X?+*VUh:VaƨV%Ak3fMv$Dkњuŝ3y͝zfS6)[hz]J!O]\9[S Uoy?%D͖y<.0=fØ6լQ$t(f fcy7ڵ:%~:X1lwƽ:\~nwhZRUvKo V-.Ԁa\fۣckY*AcR$@ۉ۶ Dz6pz-v;S36Ys H9a?bW|P#zFQS_Ę ʞtUP>VEq sV9=lf!8k AON)T5!r(2N] >#HQ>oҚۥlp I+Ӂ\E@hT֌_X "NZ"X*vb(fz&eR Ek~>= a'ЫN[iyW5Ď(*A[RFzǾoXYVL|]kle9}lVFjh1$ 4.:#-^6Рm|:# ؈4ჃlNV=V4P/INHPI3{+ mf#W(\V ߳$P:#˱;)wGK꾀T/?%x 2(FubԈU~ϬY˲|*WD;*:!%ZiN ә j/]c,:tsyၟw)ݤC;0Oj0/m5{w B}GЧ$t9 Bۋ!vތEg7 W C nKy?W;+eV8§῾mo8_(D?O0$NvaR auvOIZ~3є>nW3gE(, ^9#Y,$3e%y$_rz}I57Z+*Qh8ySduo;uDJt[N 74K3\ܞ^u3.`(.oc}d,/{LK?17h;<_' ѽ˫8%Z!W]߬*g赫 ؆4lwR( Vit.h<^AE]7^[E䉼~ybI,%/+Ε,!PhՃsm{nI+M=G=70Cuz.Gy\42<24 h,雏>s~C,xh2ιOU2T.[_VǘLpmR[M_Uw;WCfX%ڲY6G-%J(nSiKrm/3DVSǹ뇫~}f<\0jkև5)(gJE_阯\ٗ17EQTD*;겺ꮚ*1M|NasN %J +_¸\Û(8VTn!HDMUnq,[-CnccDmcc7[~!NKãuN-< ̉'6K©@2 HL(haxe 7/ zpwg+CMߋq`DSՖWڕGd(!դw: *d^ 2:.Nh) G :R.d[Necp6sJ@tw{ij;:iUY w'EgDm$`(_ LȨx~+U72f2rpCT# `6?'(a)M40wf_ A֚cy[i.Prbr1gb88 .^CXKZ)6>'31OlȻSi r9tNLߐ!6t)/[LϢ98Z`. وEuYl)(\7zCߧxN< [qˀIʗFcڎ]u:T>Y'k"MnZ,>h܀DE4@q'Z3ywxI75<{./Cw)E%O2bHԚ 2t^H*JHY\KA!f(iy'V82N;iΘQ_" ˅ 1?yg }n]i A=En=.!<@ۜk~)vb\IG~N&UGj,%B=@\76~YSF]}mbĊQ RXiEcF>UAsNߖ$_Ͳ'߈?&K<7Yhw$7geF/ Pq15 f}W1⯀zDAj9FJ$qv8LNmo–l}h 7XƧ_bV6$|p]uHcE;ȇ5Z?j C =cV`j\w3 ?#Og$jX1J=\3d#FݜoD]=3wƬh >9ylG!O@~GSCx0%pۖ\Pmjet9l_h_\f6rN>=킢ޅz]NLޝȪoaPL&u!\긣 4H_WfHNSG˼*Tc`UѸ:TKWf+,ՄřDE*CSUղ7B0Da7%ywq|<1HcbF$X,M rsZִdjq.ԑ+:ࢥTKdr 6&C PۡA5qiH8DHKp(<Wر4:;h8!wXZ ,l,L;\ulzYMxKGb|ʤQ6q.#tmʚz"3bO1WzQLxK' xq0R\j|؛RUul!uMQ0B+ϯ K=}އ :_^_e=+s\.z4SI"8QaǕ z98O6te\S0dr,z?Զ%? h~x#*\\9 8/7f 蠫CM 碗Hby] 䌾h> h.ru 8u|8A 1}Enc%КՌ ?H4bDxqQ@'SPO^[ad p!f@>M]FS-ݗ gp~HWMyVx`vm ÖUqS6 Mz` җkyz=.aYz Vnc%tX?|s?myEZhᵸ4o!ҿD?Qwy}sg^)|R'"{֕kWYl]P]3Lk @uǿ= l𽔘oθgMnd*]xCĠ`x RήGk]R?k+W%-Gv/^r,~`> ,k|v`8(65fw@7~47pnufK_a&W7O -eï`5;oૼ_54O7t͒PoJnCܐgW |1!}gK&_tjs_Zw.8KPF+A]Xὸ/؈AxMɆXwPϺ䑟fYJ~DfKV6ԯ7]LL ׿4`E+\7)w=zo$v { ov^ݶ{>7\r~@kWKq _X!V=#?bi ~ɽVΗGk>6򛲋Dqp1  +Q v1qH[c+k(jD.PWI V_*Ş;~`5 w/bo&:rLG֑\C j8Z}%Q#JƢݝ-8Q}(lϏJ4 ls$a`7/@l`0`V1ӏZ#k$nĨ p{VCG֑ud` *2e NlipcY]놸e=yͳ$0:i$g Ȧ 2қf%|@gQ)1IElSbbt3Q.w=1i1fǥlى8&gN7++8*)pJg4v=M̡_T-U k),~@ ;9(BJZ8A{&d`J.o=ԶYR&D?'_NEj닣٩Ns?OS<'.'/śN6f=HCd+g9<9TCq"R{Yp΍pp.b $FKǵ)n|_D֏,ي'caș, ]U%Oc;Q.H>K؝̚Γ\qsQX.qܨ|[au@IvUSuS%#-Wij] 1ܯӼ3{vTӊFpYW#^.j@aOS=zA9 &IO(eY iCε$DO et%mѳ|G4RèeJ@>~m<4݁oxn: @l8mo.>إb j0[(V-_٬r8BYH1_=+ âf{z M 8 ^ ;@bdq}d;':p)N3^HS4lRyBC6b٣K2 ]sIh ';ãjM.5S1fMzsEP1=*B@#pÈrM^vK/|XD3Rח:3,}&3d`$(t?bh*}Q7/h8Ѭ2W.`}|_4-5|gz²R l'Mv?zQeYbzF1>NYk jYJq6ģIB()3k:FiC<*w" p~ !{XkzհE~_{҉_ߦo@:; A|X!|Z)hltE[Y:S;rmm;g{Mp~DsIX ٠2Bͷ>XA+έ cY+5[}L~Mͺ^4+ 7-RU}'F=|'v&$P8P,H, tK'`i%8?ڣfׯ؇WS0a  ob͵7~8L ( _Oa0POQ~Jh$R?'N tj .oQCt" sMH ;z0v"EJz97M7Ɠt͘NқTL#;*RWCQY~1-G2U:F*68852= EM<S'⪜g=W$5gRIB](m!jvJ*ߐ@г~+d f&PDP=u/G}03\#?S{]͏Giۅbk~. GI@ SD4+yp49xǶuåQPas/Bٴ@u$e[7񹳀4 ow,7s/6"pN J{7ޅk#lul'V[Ɠu(|.= hZ_%Qk='͘Zdyry`={b"6u9(yJtI:6\5ǭE} 3<;O_b"2wU)V܄h:Auc36H 52tePU]#w5t Y|`~>jSA۵I;uy߹S?/. v]t>W<,m ͨ@{k]{vyŸhjځTK_aNWg 2 ] n进'yJ(}s94us~# VHZ3V*#fnۙO(KxtXF˽(N/CX/ p߇C^ % 5'ȶ@JUC 0u @ {r(q8U/[]7aNPAN%v-i^ ij.UJ˗!>[z1zaaĀ5oۼ^sVj:pAu <\|\ϥ4vϴz6zW=*PUзlm(hoRjҠ{N>;a Ot=GG?W 7խglz8nd-gM zgs#t]Mp[ur7Ǐ]^?vsqtԻDa_t3>ܔPgh<NoǶ?˧oɁGVw/.M0P?'-5?|P/f*>Ցi3s!.yM˶LB4t؟F\8>. qUN;W[9"@Ke3.N#{drOFjy=2v#2E֕)Ŷ1cSe@Gfd$hpn=2"v! -p_CEa )Ѕr @qJV< >ɶ~H>쳔K#:>Mya LR*T{zW!4w,r|YJiV!$Wq1QQ{)T(VZ(F)1H۹haeZ%Q EQN'8Z} B_y0'SD⬴ACa!'YB?4`ICiȞW[kEmӽeDSKs(&1;x(#xhZzqG+V!evXd7<}_?GBBA {3)Qq6P[9#N#;Zr@xfv}PXVRC\:f"!ESnG55b! nbR0)d'%Hv8Nk\Z Ayhm ɮfE蛠<$28I$r*!1}Qzs8t_&kjX*YˌDYW@>Pƺ .x]HljJ?N?D5*QgQiB[K7l~22Eg Q8啷Hb^S`% i*< ;ɳuz;*3lYܞ =Dz q/u?d3,Szo+S>kp?SEXbWfsު.)3n5>!!oCyrnt>2nxM~2ĀR`ڥ`\% D<װa5<`~A '岼m^YiW,u:k1ze=aJF~e#Ϟ8/lff0E ;B}b3l&[]4*]Jß<}76}mbz{ zv+>X{ɐ{ٸ^/ =OVz 5h˼-4| +:ۀMNtPݔ2ǷAˇ>4 i3l)hƭѧ7&%a4bмzQ? (M2vp?Py8Msu@HNz3؞'hG 0|EM# #^h+b$X= utj^> #$RX_@ E$`iz9\n ï_S+KAǿ/u@ksP.m[{d̆>k."n~D} 0Jxt3joǼ4́(Զ>/_멦xw C],0 8 tOch>(xR|DVaU:_S'⪜g=W$5gRIB](%VΰS-V!qbbSGqҎ V5V%i dc&.>6̂8 rW@ 4P!}<]E'h sU]rk OHIN[t2;Qek~?rer߾k3N,&]INҝILwpDb[Ůq~h0Z=jx=ciRm0q {2ԓA"fVmV]x@NpJ&$ tG+e|df!Jʱ9GB.a]1zbؒudM4>(_,A=4 ` Ѣ8 u8*;I~*=H\8LHû ɀfkz5pJ͊Pnz˲DO% >U[I&0]uzΪ /F08z<5^@X >NRP )nk4&[Cs&:Il&֨yjkħHD&f 褋4.rtۚlDFdՕ y2qu=Sջ+}K @Z%Wa8=CถWD/7(6 嗛88ͨϾ~Al`Lx/6:;Ȫ~|pڙxR2ɯ%f^-/ }>/>TGQK/_GO c? זMȨWED+ޫPA _֢`:B.ɞڙ 3g`mo5{U@kv_(Whk6--rfFi  1I |ِ51>~P6AQ!nldC$L }}wVI=2$sGJTHNY)OS޷߼噱JU5[hoxd )s\z++9%6e7?]п;}Ts_v́C|+O52l }V`lȌ~peV%HMa8QoP(Y9e@GQastct^蚃9j*enXaJ߀)'+,*۫ e,^br-өoeTuҮ#3\I}\?V8Ahp"g{j ?U:_=AawPxm߈l= `bow4rH";2݌ t.~6:"anyp'<@ z_F Ⱦp7T[ލ9B"nQc4 Eo0_E_DΊMo :'\U%]Q֧@yƳ5RAs1ⶳH4~ɃzC_/qT "^:7 Z x`q4_ wBkCJ _HJ҄jb}oFyB7ڛC!ÉܬXYۧ`[$f  "p ,x})oA.@W39]F}?7$ ݡD()Kg}{\&_k[t?yt1 A [ݳVw/hT {wgMGtűA@׵ul%4{ڸմQwiOfA(kow7iYsw?bƬ ||9=9M3T١Vgb;2x^R`Q0\qTRk |zɔb5JѦ/dW5%)~fkXw;SbJߕ:pv 5JTsJUznHW˳2|ИsbONŝU,(5[ԘJ(#RcF:RmjpږTb[ka;H%{,WӪ1p2\#4rew} DEfgl]# pwSm]$U-㠤cG~M擩n?6M3qh'7#:rMQJBsqcwP84-bAJ^~[r8[ FQ-36Ldڥ:U*ǖC q+%^;(`Dw'4KuWݡ%"DV%sXCf(ҕZw&ӕA۬LZx뻜Da&"D4ih t'ΨizJiV=p'*om>l:,b{&)? e{/f¼ _&:x^y?8xꃍ3Q=L+yMkThO괯f{uk;>߫^S=J~~ģ QPr=˥Q̱jK3G]~J~E}{GaX>? 9G>2BI?z3tY!w!*DaGsY7[#,֮W' = qĐ+wp9A-xm2/@̜p^=D۬,_ZJ Tq닧_PjB.9,/ [(~p~`}_GNW1_AE 5 }Q>} xMb [R(P>Ly?_,O_.h 9-_4'BO(*KPC;`ґ?p\mĥRFπbo=yA_9I>Ӵgw={cdɶ $@ ىQ6jn=?#8J)> :gLNے]6@$8cފ7w_9#K{ E8ܻY{~0WGz  ͎m2iC*#zzr)86gNEKSp(N S?q]҄KX郱JV,a+ͬ` Y1>,-oDs#NE}RA#F!H܀:e8<`Zc=AY\jG pnEV1Xven]=T$FQ5FȌGJ2o (bDVW#Fv&X\Ӵvǩ -̝nLk3E!;-FBQX~ Ywjּ`9Qi=kccg+giTc.Ĝ`hCJH"U8Bّ{La%$~2Kح؝m@ـ.쨴dfWnT@7jO*BUYy akY m_sC0jw+n}qJO;q CQElWJ.(#/8_Sd{Z?"?"CQ}b=|(OE!YV̊ ?i )ֳOYnQf[Kxa>*8c 6+ ?U'V3[x +(?=KBuޱA~owހӐ>s)y1 ģw ѿP~\ޒ*_2_-_Xv|}W( e_c"O]'ܗS@`qF m/ 2ۜ}F :':]|a}qB_x%$ !)O^rC$y@/yH*񄴽)޵߻UDb`}"qۋJחX^)I`nEp)liV[]1M<OP6r=zgbIguSebORD۫qu gZo~ٺlAplxpT0+[}`jzAV2Fi@qv֬5\|ʜ̭NleXdsjcs7f W+Ն7`g ȘJj|h(KD- dXiJ؇(x$( :;˹! I_TS 1?E??ZBΪmU/?~xY'y5g&΋/ɋ>GMGeD3Vq%'#q$8K)fw9:ĵ x}rxwr:\TZaG*y8IjbRc|XŻǿI u3KGnD1NIBs RuK>V.EL+M2#'fi ~V vl{u8zH *:(W☕ ~JTe\O*tHGHY}KNP*ݾ˦TѼ9/#A7qZ$*c?qUnwN%Oi4 =3N)cbJ uV4(Tn 7_?m-ٛ{UBwznʜ"Z xJZp; {/<P;,)''KQk5qpN8KGbe Sd̛\17 pa>SR! 3K4'+rzQ TTIIvt]Kc⫲K#v5+|D~O@%\w_nN[L9KqgVhn R!y+Un;*&/HrT >>\ t=.Tġ S; Z~!P9giCڧ!# B,;X=ۻ,I2UWV9$lk=Aj;{AP79|s*Y;̠[MCۿhf]o{oY=1kyVV5E8Vk+֜\80X4D)!!?*|fv u"xA@T_q64)kڬuV7 t '%;i9s9x,ڎ-45xd8?ǘd/Y|t &LILJ`& -Gt/PK!2ա% word/settings.xmlVmo6>`9,Ɏ8ElG]dPmHʊwŨ^ԠXO"; V0`^:)^m3_i36=\gZGcd6WBb^( \aƐznL"CJB9az5b4g)#Z~O*Nr'an:3) >6_AG^'F=_qBկ?JTXk(.\U%o R=sgVGaw ssk 姳9F4OƺJGPx f0j a|`˻² |,}/]"ql1$/(|I<C([rAwQeڌicv(.12 x6$.xTnƫQm$Gϗ|;[&w.k2;T7dԄǸETM8)yCK M!Js8fYM}>"uj .;D#V!7%Ip@, IY!=mf`wO?n,++ #+hPr8 ~w)w,]Pe#`@=-h_2ROK௣͎0,gh{Ahq'7$}DC]"Я$=9e)Ij^`x=7Egј ^YfyA Ju!ܵ|iWڱ8rXxWqJ)hu[yPK!ab eGword/stylesWithEffects.xml\[s~ߪ(3NReg89Oo2-<!׭Zqo8"\أCb+<,OgN9~g*I}X Lv7i] a|Db.D ?EpYڭ9 DBZ8p䃈_n39)_q {8ňëL3\r~+Gp;n8:0hoƩM[oEiOL]{qG6S_ޫea1GEC\s7K|6DU˒iipW4-1PIƉmK(bgFԒi{O?c?>0Mݕ?06aN^'9r#g}&iA =O {ρ\g XQ,$|@ֵon85ŞG PݡP0;D{#T`uZ c޸ش Kfk:OjvJ Vgq QQB$a mRyT,T3qgԭu+@5Q5ҽ8`Ӳbvdf32+=M&{cZ7 (MwJLMVouUS5}TTfHC~țyw;HM2ͩE&_5P @ܠ.({(țb*yPSG,b %,Mu~țy!oP?Mꇼ @ɻ?&`s"yAɛSL(ycr&;JcUR X*ai&``Ȱ0Mꇼ C~țyw;HM2ͩE&Ӄ*7Ș7&/'oM@1NP5TMxL r*E7~țyw;HM2ͩE&Ӄ*7Ș7/'oM@1NP5yTTGꇼ @ɛSN,2qS?M&u'vțe SM2 T$o17{p_|=uT{2ITlb*dC: ք!^-IMt-B#dI׭u`*0o@P]ۓd虾Gв7˥4h}]Y >@CP#>MU06Cπ P\j.;Hxݽ \s+ٷdjfg(5fީ ޠ3o# (V,TMCpW-f!]֝p(x| il!VHSԯ8jrLCQ(pX]7 IY90$]ךPtn:trkeu(oTr/-郯9T h,-ˠ#z2SoZOYT?o8<h؅rs*nUvug.? vCW޷2騨:iVxbmj-W)#{CYhɐ,t7Lo/ T;n/1S9 eլǒw&of)cIe 'Yr@+*U Yt"ݗ&0dfmu3&ٴez~1lq>lt6h:[4O[4 kt4B'6F݆j.j6K-S&s f)9+y!hY:%A Vlc}iN_ާx""pr9WE)hXA?35YhƲ= `7*\OKwe)Ϻ/SeF= 3+,dV9d"1ϒQХFqn}埒PK!t?9z(customXml/_rels/item1.xml.rels (1 ;ܝxxYt23iS(O+,1 ?¬S4T5(zG?)'2=l,D60& +Jd2:Yw#u]otm@aCo J6 wE0X(\|̔6(`x k PK!5]9 tDword/styles.xml\Ks8ol(S~LƮJ<=S$dBZi4H"!2=E_C2:# ,IuX싀ǯ م뤙^(bpY*C: N"nl{5E^AlY /" A%v3_D[/+}0gn.&H5ٝw3\?HXEn6-Q$&gi FGy8K+/9-Yq]xü4NwF:Opw ӟ .^Zϛ& ^rhr-Y[2w{`<<*[go1 )42su'e"AV XYlYXE,|yxJH HĄ%=&%{7<`ݰ%e~OD_ ԟ1 4϶2lAtI?8*{m@W@l'3A]glʵu]Ĵ"0xż]=btfWWއeC(j]Q i]Q h]Qsx늚[WٸQ4 %3B&7Ш#y5G֪Mdܭ2H2KDں#PeDۍr8%l?SgƒVs|5`r=6" X<ʣTV:3dr%lftN(y{ИL3)mI>, .*p)>psUlޢtQ=Zʅ (*.)Rt|p(ٿLs_ZRzͭsV"Y"Zanfuk$[g}:׾(qj=ZXC`mvJFY;5Ƶ@֤}71b,Ϛ<1 cQQb$e mb<*ZOY[V-B C|<&AG ,kZU Îskf@v%I8 I@vPnPSenz,C0̩6FY2> ,ꇼ @7&u'vțe SM)6_5P @ܠ.ͨ{(mM@vP (17 DBKS&C~țy!oPwn鏼 Xܠ9L kz@e&n8Jޘ (7;BՇT*X X8&r, n!oE7&CyAsj @M憣ɛb:yPS!Ts,kU4y0^:7 dcQ?M&CyAsj @M憣9ɛb:yPS!TM,kU4!ofg&0ly,ꇼ @ɻ?&`Ys2yAɛd -%_OzϠ@D ,&+~;#`a!<&͡] W!xo&Nn{S[!uxB؞$@} -;f B+ohzb*ƿqaQ Pw} W Q}KFf~;~Rh6ɛ :M=rpj]AhB4Bb,&ArfS- /6ebkuގX+V"Dd^q2QaxX^ {(dNÐTw] @in:trkeu)oT/-郯9T h,*ˠt2SoL-yT?o8<hZs*nUv.? vC_޷2騨:iVxbm/j?-W)#Q!U,2$'=CAM&ϜjǍ)a/;cǒw& UBRֳ&-N.&<VTyBA"LiN]Lr4wid66L/3Χ-ΦMqia-C0mhxY8et UYm2I7KQ] yFsMЧй/ "9_bpK{do2>#`*"/c~m[sSӟ|,o߃.}vf88_-Ky֭gx*k4쩠RȏY&fQ&! )Ս,]jw&C~PK!tz+U(customXml/itemProps1.xml $( j }6&.1BXkiWטDԔwaagC4 `{ H;{F/\I^ié=4}<2ɘ|פ- 9?Ӣ(SVRY ^zo@Y(`Ji9դ{hVa$~ҝW.GJ+֬ovvx[5Zn6?L@چQm|PK!'RdocProps/core.xml (QK0C{u:Bہ=9(n 6iH{nu >/ܴU:$Eh7%zY-)gZP84 n(o,<ƀ\HQnJP߂b. uch7064 <3c3 )4cAoܟ2r*&t:?{`l6i>FOK )|c9hɴktGJŚ9 _KKoC7ca'Mpmx7(ǖg5R2IMW$4}z?Sw=1 ? PK!\(customXml/item1.xml $( I /-JN-VNIM.IM .IUq pԋQR %bJ 9yVIJ%%Vzy@ (]??--39%?475DL?)3)'3?( jUч{Ǝ PK! 8word/fontTable.xmlێ0+;D_hjKԛ^T`y Y޾;d/j0c<=hiME %0ܮTuyH̬FT %=s(ϧtI|Dܒwհ9i{EۡGd\9.kgծМѦ$|idSV, c+(F$  6S(b}L"5īG#x|涭/4?Ay8v΁zՎgt`h`r\DSAr-Ӑ;3;r7l16l!M/+c?iv萿QI'iK|۳NB1s,:}L(M耥~mI0$~ P;䯌< " 7K8[8A$B]N\u X$qؒҥWK[c$]tdD!߅? J<0y9DZWcSˤۗXAӝ_PK!p|docProps/app.xml (Sn0 ?7JڤFŐba[mϚL'lIX׏vO4H[Sg-h]4jW[OŗyIR*?`w c 16=_ '\hqvUxktz+ЖX^aw\MK\< P`kE('9tԀX(0 )C(g z/.Q~Gޫ4rXhEftpU=v.d`g_dBn@ ZոJA x@FQBK5E{g?Td*oU0õdaޜ̓\0N\Nߔ!v6i襎p]׮ d w6h%9+SZ΋0Kߙ=P5Ssl\\ȣ}?dT}Rb4m֌aZ?6+ؘ׷C3LMqȌI|,R32\KI͎!"+3V7AIʾMX#~fPEQKqha%ũIs2Y2wsMj`K&cKF/b.n^8RknX:ccy\ЉCr*S;W'b:BVԴ!U S\$pE/Dۥ莉jId6h--Okh{ץAGd^]YCudX$hky;)0~Med$]`[wgh8;hQZ2&.Ak|Ej@k/'\)1^z=aԑA"ޘo VL|24rG :?g&.'?b}ߘIG=ehL;t0)m皸n y);.\l1@X%/19Vebˎ6 c Q%H퇙 6EN,Imlk v5><3{0omY;X]M 1JN.ȰHѦ옩)ԊAA) ?$;~MQ)fG&^5*UFֻ _LZJqfg57NV?S cRU>l.6G< x"_̋ttңsI["=zʔ$V|*'K:h{|;mOcE&8۷n3W$4]p'>v h}_Kdu|7ѵ~Ǝu+;}׳+{P⺴G&S7ozk }h;lwk3Lk8F$OՀ&u?62ีK'l?VcM7][3mH;fvɴkNǧ׹J*&.Dor ]"L.]i//p^쬔9Q|`Ce웎#6xG\RߴM>WNʢ{K}Ħ ިI{`w^}Ϯ3ǚ~]Js9y-]'oݒ[^9m֌K%{S_*~S A}UuE8}-xG\VC?V=վ*Rɪ| O5q# r`˛O}A\О|]@3n޼)Ǐlٲ{}da!Mkˣ&Q1[9'?wn#r~b<_oӈM=O yZd+/ٍ ./Lpo!9W@&&;cg_bTyD~hhHyy׽ǎm۶%ocUhx.]g,9 18r#u2R',-&NwT?f.cË́ 3g;Jvс7l /zEqbbQ~_N8n =NN<ԏns&618?hդe_pg2 p>{2yǪfޛZg&e6hg3k?Tyd]T0~o Kh;6d;m-2ێTavz)qi1y RVS΍>+o:1G[EJ$<<@nּдq#\zOc8p{O'HK+u;w\oNjW˷#%{64O G/p)qXt²=ζ >5hZ|ۄȸ{X|tGT>k^kyV$Py}&ώ{Qߩ~K}Xx'?22"gϞw}'H[Se(;ȼ9qS},Jo$)n59dggHs5bTqYk Ra$ y&Cjq<֒NjtCycw%TR\lm^rd|uşOFmNtS<d> n#.Y\VvM+{H?ޫVk}lǾgKr;z9uuȉie2oN0]5bTqiy-8ĥ>bקi+ͩ$^u ~yX.X ꕶ Zyod߬۷oۥVX9CtPQk,^s1{nt]j>Z?.3}cW=weW\C\k|tߧR O+1vʿ+nWEdA~EoOnһסGtkZW+m$^{<_ln}v:M:+o#6>xĥ),,9!.`>ִ㮺q|'yfδEEy$UZ{LK[Xkos-lown<7Yh/$$< B"@ Z@"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$< B"@ $$4ѮK˃ݍz2ȉD"dLK0#?EU4tnrߥ^줣EYj\\,R2XSq9JIΎQXܤ,$pV3rzc^C5XY0дaщ`8s6(w?K38NO:ץhW'-Ek|n-e:۞k݃:rv\'<%h .bJ_bs8m:JKP>A 3MMAzmXRLtxMk=7n7~&_f%'~rmyFuD"C옩)ԊAA) ?$;~MQ)fG&^5Iʂ+#ZY]7k:q[LG9Hᐔ xȴYV*f$ga<+hw]$_Ip<[QJf#@~ H>L2=oGiOu jly^Vna؊2Mf|Y:fA9LepHd%k];mz(>.AɌ$`1 YfɌGjo܎2'*T)qz Y1U+6&]9TGn&.Mp}"sEbz }hi8d*R*3x8p;[\L&ǣ4Kr}ԑCy -$< ҢD(!aJeZh@ch AZ4ٝQ"oD;sm?l.hܩ]s_,-Dݟ~ByUż~yt]mVm7%o?|gwϛ>v7~m6ɚd߮7I{ 0/.IMr?/Mhe{*ګ1W_ui\uw[LL2'|36QďGfv]0knB$8V7n~ry{u-ݭ XD(2i3nA_^_̠H\ze|'rr׵t~o-Jɝ Ly\#68z?lOmMpD~wB7lq}ZAL{nl#1eDv1*]hv͛E'ڭ+in9֓{T$ (#6q<#.u\'=*[>*:K RY/@!F>.٘]sqYק\;!WJ{I^_n皥{M`r[ONߺ%rڬJ0MT$u<{ZBep<#.[篮,r~tA~/GlcT~tttxe˖- /l_WOȦM7o{mfAT0>}s޲w$չ)={#yyd<*#f9Nލ^9-ߟf7ClxG\Wrlm?jgtaA R|ih.[n#.p}Z≗Ⱦ7cv[; %)v$rgk\Lr]g_wM䇆矗_+|1ٶm[lҭ׿կV%_xѮ4i߭n^>o֢woc[-E9=(^'KW>=lWi1ٷ>>|kcÆ o!/Wt/722R׮I mפ]5OSYo7sPi(/$88n a+xG\VrG>jBsqT\GB=yJ}zQoﻊۥeIu"U|Ibez.{IGE3χ&Id3{ZmI/ch"%jNvbQCHoo v>55~^pf{ ;m-2ێTOK~v99cמre9fyIfhC\|\]k{q^#ϋ/TZ{2f[5o#ɢ>MCσDZ=C#ϖܵ߱vt,|xoqS.S~;| fz!]:]si/H xG\dG"ϝvqDmФn7(6[uO)FKkN12 t̚mф8qIrc⦆)us﹛}XxM5A>{8mu_ qZ/':_?3L淦vQvys⺕XHRV4]cFeF|]^3_)\A\>շ^-?;^K}['g߉OqWh4Yڵ}9U_ckƱNh˼^)o׿elz #,\~Gmt Y-Rק{ : x{UJt_rbPS<].p}oIU(Ԕy}ohNnyyRD… ޲:nuW]]]sM}mp붠\$w{olܸۮ vuTy3<Ng}o߶K(G{ós8rXݢCbNϜ+1Upv8ykl.B{OqhS5wU?”{]ZgG#/$*ئc=Mu&Q:|0v> 'Ji$烿mW|3v}Fovڪ*/ܒ쪥uthM?W>+ڳEu͛7-˻~5^d;ֶ߰ȷ#Z"jU

w?vfa}|=.6"-]!]PF4)*h7Љnnܽ0ÆkPF4)*hz6ȏ2b^6%=o s3Y2*#˧fލgE.QF,g[͵h}VLql˦oG볢Oۗw k[iX\qzj%{էH4ʈM E.Vh w(#F{;k='zeK.31-k;LR2b*f>3Rt>ter9ߜfgؓ[ây9vg]6D5az%x Z~!y ;IZ'\<azjs{CgteROm?;3#E3ɱU:,ݗgz}fKdSn{ oH4K}-6{ (CO.p& tvdTX.@mwte" OJ|ctX*F.'26{n["zx*#Lmo?8Z)tF1?oH*Iu)ʈ%qnFIUF, ¿Ɩ @'hs?UMLql $~(#ǭ ;9XHjhݧ2bVN4Vu)an^4g&ʈ%a@ LR-\2bN<:#oHѥz(#f$~kDNKQ*%~kDNQFɗK!~2X-$lh (# PF4) Jh<}HCG (#CQF4D ¾eʈ#ECGvҡ(#xa2PeD㑢ʈ#E;A~G<тoz2HPeD㑢th?ʈ#hA@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@؛άc.kB1]jGFvNec62(t |b[nvK(`ZE( K~:6tiu-/v[C:թ4{||@_WC7aCG|?N5vXWjpYX]+4ÍA juK\96:M"uaC f p3VGT.j ~ lpT; r7nQmM/vTe&MST]6npR' &6کH'V- EnJUa\f]@@zh.վm`z]XmWa8ja_gY؟_& Nvfɤa>;W]m֚+'}Yj jM6V-ZsSdN9ʩ5 )-{5-Z IWR)T}vy lQ!C Fxd5ݙR{4[wؙoI!lrcH(ǐQF4y p^L+k], !;+Mvv|Wۋ:uF{E9&+Peg 5l_ۇM妝*|rۃŃM奝-#?ԐO|}Cl?m`*(Xv/]"19 :L^vJ6^[IJ }0_azYjE,ۺOi.>l~۠@d5F {U?@}/ݫݩ@נv9:5dg$LXstƝۢ,e+d$LXunYd\;LXuao}d\;Luȯ?cnM~d5NB@-KXe7&g!{|:dջq**qPY=~x7{A~x7VϹ-ƃn>f߿ON6J'偿{/u'E+xJԐz7v۶RE_7(Ǭ n]M v~7&yvd5 {<Xo^R`Yt?iw5d4>ϳ%^8:kPY=i}0gsK<;i}\婨 #45rLV◐v a˗v a'C95I\NWv..S IF {CI e VMⰈGK(&}qX{wG17ى7Sd5>t6} 5dMr )R0ʈ##vMN/;j&R01`GF^ ʬtjԐM691`cH(#@PC6(ǐQ!h<2a8a8a8aK.OiIENDB`numpy-groupies-0.9.13/diagrams/multi_cumsum.png000066400000000000000000000203511372771202100216320ustar00rootroot00000000000000PNG  IHDRzTDcsRGBgAMA a pHYsod ~IDATx^_lǝӗ@<IU '9ն*rT90[*I& DhNT]ETlNTlQHH=Ix3Rl33˝#HH;w( xy*jm5 nZWi>^fq-`LܹC6PI _52dp19UȩEיf0F8\b56{%|C %ȴGḕNkQ%A#n)J:x`E(rw w^|&ȁ Ѵ>zy!84l4 I~_a*vPqX*\ycu8Ô;KS^5_JUK_d7ygt|Q+%Qy%Q1"H* wשܨtM267_^!Mƛ>t=`mNmWgv8} 3tW9utjybu漕7|M1beYz _cT9hʌBZJu=}S^۹zOmAy1;M6ҼaKXK~,ok󸨫M^ݷX*Oҫe]zW,w(muZFkt^Ïͷ/OF$+1DZ.,˔k5#TT7>}Ֆ2~2 -JBQ(0̦s硻S&vrz?BSEzBզSkOKlMxIAzVLkGYzT(jӫV evts{koX- t0uĨ:^l#b=X e- żMf%YK'G̪jF۷nɡOJӫrgO:q8޺cUk:jxK9r+KBizѫz6i8Ō~s&j45jVׇdƢ-1\s\k2tϔ灇e-(l1WQ(ޛǖ ߠ|4o~D~Bč\#[bN&Z2\@SZoge&3ctqI?:Nk|9=|Na*6&eh[*vLbAEx՞::",%k,6x%a䔬Y2-X3,f{v? . R8Ya)-*5%  > OilY%֨Lu]]䡳>̛3GFspME=ce*M{\o-Mǟ9mb-fmhTjz̿|R|IE;EY*Bb}rρO~ٱsX0=ZYkhj1CCҞ55K1+\7u \tiOϥݩQә)fEK:'8gřM o dܕuFצze&>tC|fĂnљn73'Uw^dɩͭš.ZU^i&_/0%ۊYP'X8:1iu{kz6@E+U| Hq6mN,54;y,jyu$ Q\Ο.\;(8ģn MQmҶKznjRҶʹ Mm Eg,z}}l&''E)#.fXƇWClMPyCYhPmx\gN4med.qI>2Lrk$k͆w>G2YhGD)#$RE̼~QYp+YYqEd.kmf}dB27%`܄)27a M`e8nJ;O?K @}$VNgL7oޫ;XU;c7E< ݮ(W#"|gzRccOt 3]tC:Ag:d &H - ng[h@# v1} +'ˎ7FKjJkZU] [n.Ś?<.겟gGZ5_SbZ/Jk-_f+Dѣby2"0X@ R"A|$ux"%mRgmkj|39d-oFV˚7]M=S‰st9:8[zRGCD]e#͛+nβϲNŠ[|9WsJHDU֚KnXDQaD{ġ{TN+88rn_xE$nJ:VH]KZzjQ}ؘ*Lc߳gjVսXjV3LZPR*,˨֧M"ּFƚw7Ud[]<3Ŭ9Z?}+7>r=Jy ~_ϗb >t&{^^xw>LyE9[ ē$7_mO܌YZ?vST=5}n ϧqc@w)硡^?;7Fz\31y|qɞSQSoHP.x}|dvD!xpbAwH}8ܫr&7uRҮ҃}WOoMG FڧYkM}M Z#:WˁX=8 EI}lB;ӣќ.|9%M@=Bb7 zl+@Q9*kIG8kb{"ԴTrеty/oF>6q;tuV(Ohۮ t1PJv)|sн&:qp NՇBNڼ@6|$^Rh֨+ƹu{!H~<2>ؽ4yN$vDffEs?7ʊ>X" 0bBغXU]:Bu'^)UwXH;,BG-S8y,f)xw73aE.$cCMGۼ76ߎRǩtzbMGAmT4oJE]mκK#cW|Ydz b@'npፈ;kuW3t_Wxyڷ7\2_Uɧ󥧆`*,˚}T۩D);N[VY][Unjp(̅+osp/#v']}76N;(;Pln~|j_?ՖnU4<9BG_݉bv5͆;>;ź;x9UKcͿRxdp|B2zXX{Vڟz;;Y͠;~/͹ǚ9Tf̡p1M>nv>$jpv;s:׊abBWvC(ʹ/񼳽_G:wdg ~7.n|O Vk=G]CNd>$y) ޜcYMC2DjRwXR: )n.wnxo#Ԁ5Sgw&X-H$8_~FTt3.u['bXtTtsxC;UIȢ3&&_ jG07XۦR%ZDqnޤ*^.)?4&''EI_}%4D 2e8neZ`{(_Xa=8hAqsQ&7Aܔq7E1UT 3$b &H !nD &H !nD &H !nD &H !nD &H !nD &H !nD &H !nD &H !nD &H 9(.qwP(D.UjAܴ;FWҾ}&[`Йns6l kd&H !nD &H !nDp+UM-iRիe&HWkѡk7!& Q_ī)j BE6ZP:bA^,5A"Zh`]'k j 7!/RfVZ>V3,ΖrBiz\ծl%r^+V,5Aͮ zu:tMMeBFb&N|(ѥ 3u)Q^!.V y&`lMȩTTppA4TD *~ᦵyv2IuЍx쑱TMbrtێU//Dƶ;{m{ߩ݂" ~{>'+,e~oB{zk:pMMiw薵1L\Ƿv3FuuY9=sqezʴ갨.D(NÙ-+o-ł^s:  |(udZ2\@SЕ8=Sq",mn5O +r<5~CɐWfLN1Ca7}}HnJ>I*3G[,sqdP$2(o4.jO"ǩ-6yF[.sRtHiAܜIQW9D 2qQL{t[Y$%Xt<"J:xVR!nDͲ|9pwbafs󈨛kEedvd}ƨxI)f;6! j\ [n.2f#I27+ƻ٣YWk不~r_kcmL iw;Zn[SKvۼvQ<zI,MȒBzBTJ3LY/UdO=!)O%=QvZst9:8[zRGCD=eg?U"qjv-kKEM#&r\jﻚ E=rtVB9[kK^K6j0/Nr,Cw$l9=[kÖd@܄lپYZF۷nɡOdy.nei{l_ڪW WͪF۞ (Oݎ:Φfl DMEzepsTԹW, AiyX1ҝ,D57ZiY]"o|z:جɀCbG}LbQ* bbvLj]3mkymk׌ L^}+ ME(Ɯ[sMQͪrNŘw_Znb`DbXG,;nfݡ [vq6v4Ҽ[Xak}|l[R GEn;|Zr#l{DIy<+:r寻:ze)аiفSd }UQRԕf,,ߒk"8tӍ|F@ nFk<_4!D} Ķ@(>bQ(.v$f١έۧ]q] oeBL{ˌ[}ty/=l҃};:N=%*`uV(9eZMix3ؙzezvFkjW U-w8z5kʿ!/߇LY֒Z_' ehܺFkЖpgm~9)ҷ1ͩ6|ͻfO??)ѧJsero5Q.n|Oos51TD̍ٚli,)dӘmSVunB6cͿ1>>e<|tSZ L?W{vg"ID"!s699)J:;(A9n Ji.q+˝עKރGDIRt= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) return None, p.returncode return stdout, p.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both the project name and a version string. We will also support searching up two directory levels for an appropriately named parent directory """ rootdirs = [] for i in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} else: rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. keywords = {} try: f = open(versionfile_abs, "r") for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) if line.strip().startswith("git_date ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["date"] = mo.group(1) f.close() except EnvironmentError: pass return keywords @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if not keywords: raise NotThisMethod("no keywords at all, weird") date = keywords.get("date") if date is not None: # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because # it's been around since git-1.5.3, and it's too difficult to # discover which version we're using, or to work around using an # older one. date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d # expansion behaves like git log --decorate=short and strips out the # refs/heads/ and refs/tags/ prefixes that would let us distinguish # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". tags = set([r for r in refs if re.search(r'\d', r)]) if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: print("likely tags: %s" % ",".join(sorted(tags))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] if verbose: print("picking %s" % r) return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %s not under git control" % root) raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", "%s*" % tag_prefix], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() pieces = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out # look for -dirty suffix dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: git_describe = git_describe[:git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparseable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%s'" % describe_out) return pieces # tag full_tag = mo.group(1) if not full_tag.startswith(tag_prefix): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix)) return pieces pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) # commit: short hex revision ID pieces["short"] = mo.group(3) else: # HEX: no tags pieces["closest-tag"] = None count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" def render_pep440(pieces): """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty Exceptions: 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += plus_or_dot(pieces) rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_pre(pieces): """TAG[.post.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post.devDISTANCE """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += ".post.dev%d" % pieces["distance"] else: # exception #1 rendered = "0.post.dev%d" % pieces["distance"] return rendered def render_pep440_post(pieces): """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards (a dirty tree will appear "older" than the corresponding clean one), but you shouldn't be releasing software with -dirty anyways. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%s" % pieces["short"] else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += "+g%s" % pieces["short"] return rendered def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. Eexceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" return rendered def render_git_describe(pieces): """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render_git_describe_long(pieces): """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. The distance/hash is unconditional. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, "error": pieces["error"], "date": None} if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: raise ValueError("unknown style '%s'" % style) return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date")} def get_versions(): """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which # case we can only use expanded keywords. cfg = get_config() verbose = cfg.verbose try: return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass try: root = os.path.realpath(__file__) # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. for i in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to find root of source tree", "date": None} try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) return render(pieces, cfg.style) except NotThisMethod: pass try: if cfg.parentdir_prefix: return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) except NotThisMethod: pass return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to compute version", "date": None} numpy-groupies-0.9.13/numpy_groupies/aggregate_numba.py000066400000000000000000000346111372771202100233650ustar00rootroot00000000000000from __future__ import division import numba as nb import numpy as np from .utils import get_func, isstr, aggregate_common_doc, funcs_no_separate_nan from .utils_numpy import aliasing, input_validation, check_dtype, check_fill_value class AggregateOp(object): """ Every subclass of AggregateOp handles a different aggregation operation. There are several private class methods that need to be overwritten by the subclasses in order to implement different functionality. On object instantiation, all necessary static methods are compiled together into two jitted callables, one for scalar arguments, and one for arrays. Calling the instantiated object picks the right cached callable, does some further preprocessing and then executes the actual aggregation operation. """ forced_fill_value = None counter_fill_value = 1 counter_dtype = bool mean_fill_value = None mean_dtype = np.float64 outer = False reverse = False nans = False def __init__(self, func=None, **kwargs): if func is None: func = type(self).__name__.lower() self.func = func self.__dict__.update(kwargs) # Cache the compiled functions, so they don't have to be recompiled on every call self._jit_scalar = self.callable(self.nans, self.reverse, scalar=True) self._jit_non_scalar = self.callable(self.nans, self.reverse, scalar=False) def __call__(self, group_idx, a, size=None, fill_value=0, order='C', dtype=None, axis=None, ddof=0): iv = input_validation(group_idx, a, size=size, order=order, axis=axis, check_bounds=False) group_idx, a, flat_size, ndim_idx, size = iv # TODO: The typecheck should be done by the class itself, not by check_dtype dtype = check_dtype(dtype, self.func, a, len(group_idx)) check_fill_value(fill_value, dtype, func=self.func) input_dtype = type(a) if np.isscalar(a) else a.dtype ret, counter, mean, outer = self._initialize(flat_size, fill_value, dtype, input_dtype, group_idx.size) group_idx = np.ascontiguousarray(group_idx) if not np.isscalar(a): a = np.ascontiguousarray(a) jitfunc = self._jit_non_scalar else: jitfunc = self._jit_scalar jitfunc(group_idx, a, ret, counter, mean, outer, fill_value, ddof) self._finalize(ret, counter, fill_value) if self.outer: return outer # Deal with ndimensional indexing if ndim_idx > 1: ret = ret.reshape(size, order=order) return ret @classmethod def _initialize(cls, flat_size, fill_value, dtype, input_dtype, input_size): if cls.forced_fill_value is None: ret = np.full(flat_size, fill_value, dtype=dtype) else: ret = np.full(flat_size, cls.forced_fill_value, dtype=dtype) counter = mean = outer = None if cls.counter_fill_value is not None: counter = np.full_like(ret, cls.counter_fill_value, dtype=cls.counter_dtype) if cls.mean_fill_value is not None: dtype = cls.mean_dtype if cls.mean_dtype else input_dtype mean = np.full_like(ret, cls.mean_fill_value, dtype=dtype) if cls.outer: outer = np.full(input_size, fill_value, dtype=dtype) return ret, counter, mean, outer @classmethod def _finalize(cls, ret, counter, fill_value): if cls.forced_fill_value is not None and fill_value != cls.forced_fill_value: if cls.counter_dtype == bool: ret[counter] = fill_value else: ret[~counter.astype(bool)] = fill_value @classmethod def callable(cls, nans=False, reverse=False, scalar=False): """ Compile a jitted function doing the hard part of the job """ _valgetter = cls._valgetter_scalar if scalar else cls._valgetter valgetter = nb.njit(_valgetter) outersetter = nb.njit(cls._outersetter) _cls_inner = nb.njit(cls._inner) if nans: def _inner(ri, val, ret, counter, mean): if not np.isnan(val): _cls_inner(ri, val, ret, counter, mean) inner = nb.njit(_inner) else: inner = _cls_inner def _loop(group_idx, a, ret, counter, mean, outer, fill_value, ddof): # fill_value and ddof need to be present for being exchangeable with loop_2pass size = len(ret) rng = range(len(group_idx) - 1, -1 , -1) if reverse else range(len(group_idx)) for i in rng: ri = group_idx[i] if ri < 0: raise ValueError("negative indices not supported") if ri >= size: raise ValueError("one or more indices in group_idx are too large") val = valgetter(a, i) inner(ri, val, ret, counter, mean) outersetter(outer, i, ret[ri]) return nb.njit(_loop, nogil=True) @staticmethod def _valgetter(a, i): return a[i] @staticmethod def _valgetter_scalar(a, i): return a @staticmethod def _inner(ri, val, ret, counter, mean): raise NotImplementedError("subclasses need to overwrite _inner") @staticmethod def _outersetter(outer, i, val): pass class Aggregate2pass(AggregateOp): """Base class for everything that needs to process the data twice like mean, var and std.""" @classmethod def callable(cls, nans=False, reverse=False, scalar=False): # Careful, cls needs to be passed, so that the overwritten methods remain available in # AggregateOp.callable loop = super(Aggregate2pass, cls).callable(nans=nans, reverse=reverse, scalar=scalar) _2pass_inner = nb.njit(cls._2pass_inner) def _loop2(ret, counter, mean, fill_value, ddof): for ri in range(len(ret)): if counter[ri]: ret[ri] = _2pass_inner(ri, ret, counter, mean, ddof) else: ret[ri] = fill_value loop2 = nb.njit(_loop2) def _loop_2pass(group_idx, a, ret, counter, mean, outer, fill_value, ddof): loop(group_idx, a, ret, counter, mean, outer, fill_value, ddof) loop2(ret, counter, mean, fill_value, ddof) return nb.njit(_loop_2pass) @staticmethod def _2pass_inner(ri, ret, counter, mean, ddof): raise NotImplementedError("subclasses need to overwrite _2pass_inner") @classmethod def _finalize(cls, ret, counter, fill_value): """Copying the fill value is already done in the 2nd pass""" pass class AggregateNtoN(AggregateOp): """Base class for cumulative functions, where the output size matches the input size.""" outer = True @staticmethod def _outersetter(outer, i, val): outer[i] = val class AggregateGeneric(AggregateOp): """Base class for jitting arbitrary functions.""" counter_fill_value = None def __init__(self, func, **kwargs): self.func = func self.__dict__.update(kwargs) self._jitfunc = self.callable(self.nans) def __call__(self, group_idx, a, size=None, fill_value=0, order='C', dtype=None, axis=None, ddof=0): iv = input_validation(group_idx, a, size=size, order=order, axis=axis, check_bounds=False) group_idx, a, flat_size, ndim_idx, size = iv # TODO: The typecheck should be done by the class itself, not by check_dtype dtype = check_dtype(dtype, self.func, a, len(group_idx)) check_fill_value(fill_value, dtype, func=self.func) input_dtype = type(a) if np.isscalar(a) else a.dtype ret, _, _, _= self._initialize(flat_size, fill_value, dtype, input_dtype, group_idx.size) group_idx = np.ascontiguousarray(group_idx) sortidx = np.argsort(group_idx, kind='mergesort') self._jitfunc(sortidx, group_idx, a, ret) # Deal with ndimensional indexing if ndim_idx > 1: ret = ret.reshape(size, order=order) return ret def callable(self, nans=False): """Compile a jitted function and loop it over the sorted data.""" jitfunc = nb.njit(self.func, nogil=True) def _loop(sortidx, group_idx, a, ret): size = len(ret) group_idx_srt = group_idx[sortidx] a_srt = a[sortidx] indices = step_indices(group_idx_srt) for i in range(len(indices) - 1): start_idx, stop_idx = indices[i], indices[i + 1] ri = group_idx_srt[start_idx] if ri < 0: raise ValueError("negative indices not supported") if ri >= size: raise ValueError("one or more indices in group_idx are too large") ret[ri] = jitfunc(a_srt[start_idx:stop_idx]) return nb.njit(_loop, nogil=True) class Sum(AggregateOp): forced_fill_value = 0 @staticmethod def _inner(ri, val, ret, counter, mean): counter[ri] = 0 ret[ri] += val class Prod(AggregateOp): forced_fill_value = 1 @staticmethod def _inner(ri, val, ret, counter, mean): counter[ri] = 0 ret[ri] *= val class Len(AggregateOp): forced_fill_value = 0 @staticmethod def _inner(ri, val, ret, counter, mean): counter[ri] = 0 ret[ri] += 1 class All(AggregateOp): forced_fill_value = 1 @staticmethod def _inner(ri, val, ret, counter, mean): counter[ri] = 0 ret[ri] &= bool(val) class Any(AggregateOp): forced_fill_value = 0 @staticmethod def _inner(ri, val, ret, counter, mean): counter[ri] = 0 ret[ri] |= bool(val) class Last(AggregateOp): counter_fill_value = None @staticmethod def _inner(ri, val, ret, counter, mean): ret[ri] = val class First(Last): reverse = True class AllNan(AggregateOp): forced_fill_value = 1 @staticmethod def _inner(ri, val, ret, counter, mean): counter[ri] = 0 ret[ri] &= val == val class AnyNan(AggregateOp): forced_fill_value = 0 @staticmethod def _inner(ri, val, ret, counter, mean): counter[ri] = 0 ret[ri] |= val != val class Max(AggregateOp): @staticmethod def _inner(ri, val, ret, counter, mean): if counter[ri]: ret[ri] = val counter[ri] = 0 elif ret[ri] < val: ret[ri] = val class Min(AggregateOp): @staticmethod def _inner(ri, val, ret, counter, mean): if counter[ri]: ret[ri] = val counter[ri] = 0 elif ret[ri] > val: ret[ri] = val class ArgMax(AggregateOp): mean_fill_value = np.nan @staticmethod def _valgetter(a, i): return a[i], i @staticmethod def _inner(ri, val, ret, counter, mean): cmp_val, arg = val if counter[ri]: mean[ri] = cmp_val ret[ri] = arg counter[ri] = 0 elif mean[ri] < cmp_val: mean[ri] = cmp_val ret[ri] = arg class ArgMin(ArgMax): @staticmethod def _inner(ri, val, ret, counter, mean): cmp_val, arg = val if counter[ri]: mean[ri] = cmp_val ret[ri] = arg counter[ri] = 0 elif mean[ri] > cmp_val: mean[ri] = cmp_val ret[ri] = arg class Mean(Aggregate2pass): forced_fill_value = 0 counter_fill_value = 0 counter_dtype = int @staticmethod def _inner(ri, val, ret, counter, mean): counter[ri] += 1 ret[ri] += val @staticmethod def _2pass_inner(ri, ret, counter, mean, ddof): return ret[ri] / counter[ri] class Std(Mean): mean_fill_value = 0 @staticmethod def _inner(ri, val, ret, counter, mean): counter[ri] += 1 mean[ri] += val ret[ri] += val * val @staticmethod def _2pass_inner(ri, ret, counter, mean, ddof): mean2 = mean[ri] * mean[ri] return np.sqrt((ret[ri] - mean2 / counter[ri]) / (counter[ri] - ddof)) class Var(Std): @staticmethod def _2pass_inner(ri, ret, counter, mean, ddof): mean2 = mean[ri] * mean[ri] return (ret[ri] - mean2 / counter[ri]) / (counter[ri] - ddof) class CumSum(AggregateNtoN, Sum): pass class CumProd(AggregateNtoN, Prod): pass class CumMax(AggregateNtoN, Max): pass class CumMin(AggregateNtoN, Min): pass def get_funcs(): funcs = dict() for op in (Sum, Prod, Len, All, Any, Last, First, AllNan, AnyNan, Min, Max, ArgMin, ArgMax, Mean, Std, Var, CumSum, CumProd, CumMax, CumMin): funcname = op.__name__.lower() funcs[funcname] = op(funcname) if funcname not in funcs_no_separate_nan: funcname = 'nan' + funcname funcs[funcname] = op(funcname, nans=True) return funcs _impl_dict = get_funcs() _default_cache = {} def aggregate(group_idx, a, func='sum', size=None, fill_value=0, order='C', dtype=None, axis=None, cache=None, **kwargs): func = get_func(func, aliasing, _impl_dict) if not isstr(func): if cache in (None, False): aggregate_op = AggregateGeneric(func) else: if cache is True: cache = _default_cache aggregate_op = cache.setdefault(func, AggregateGeneric(func)) return aggregate_op(group_idx, a, size, fill_value, order, dtype, axis, **kwargs) else: func = _impl_dict[func] return func(group_idx, a, size, fill_value, order, dtype, axis, **kwargs) aggregate.__doc__ = """ This is the numba implementation of aggregate. """ + aggregate_common_doc @nb.njit(nogil=True, cache=True) def step_count(group_idx): """Return the amount of index changes within group_idx.""" cmp_pos = 0 steps = 1 if len(group_idx) < 1: return 0 for i in range(len(group_idx)): if group_idx[cmp_pos] != group_idx[i]: cmp_pos = i steps += 1 return steps @nb.njit(nogil=True, cache=True) def step_indices(group_idx): """Return the edges of areas within group_idx, which are filled with the same value.""" ilen = step_count(group_idx) + 1 indices = np.empty(ilen, np.int64) indices[0] = 0 indices[-1] = group_idx.size cmp_pos = 0 ri = 1 for i in range(len(group_idx)): if group_idx[cmp_pos] != group_idx[i]: cmp_pos = i indices[ri] = i ri += 1 return indices numpy-groupies-0.9.13/numpy_groupies/aggregate_numpy.py000066400000000000000000000270621372771202100234350ustar00rootroot00000000000000import numpy as np from .utils import check_boolean, funcs_no_separate_nan, get_func, aggregate_common_doc, isstr from .utils_numpy import (aliasing, minimum_dtype, input_validation, check_dtype, check_fill_value, minimum_dtype_scalar) def _sum(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype_scalar(fill_value, dtype, a) if np.ndim(a) == 0: ret = np.bincount(group_idx, minlength=size).astype(dtype) if a != 1: ret *= a else: if np.iscomplexobj(a): ret = np.empty(size, dtype=dtype) ret.real = np.bincount(group_idx, weights=a.real, minlength=size) ret.imag = np.bincount(group_idx, weights=a.imag, minlength=size) else: ret = np.bincount(group_idx, weights=a, minlength=size).astype(dtype) if fill_value != 0: _fill_untouched(group_idx, ret, fill_value) return ret def _prod(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype_scalar(fill_value, dtype, a) ret = np.full(size, fill_value, dtype=dtype) if fill_value != 1: ret[group_idx] = 1 # product starts from 1 np.multiply.at(ret, group_idx, a) return ret def _len(group_idx, a, size, fill_value, dtype=None): return _sum(group_idx, 1, size, fill_value, dtype=int) def _last(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype(fill_value, dtype or a.dtype) ret = np.full(size, fill_value, dtype=dtype) # repeated indexing gives last value, see: # the phrase "leaving behind the last value" on this page: # http://wiki.scipy.org/Tentative_NumPy_Tutorial ret[group_idx] = a return ret def _first(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype(fill_value, dtype or a.dtype) ret = np.full(size, fill_value, dtype=dtype) ret[group_idx[::-1]] = a[::-1] # same trick as _last, but in reverse return ret def _all(group_idx, a, size, fill_value, dtype=None): check_boolean(fill_value) ret = np.full(size, fill_value, dtype=bool) if not fill_value: ret[group_idx] = True ret[group_idx.compress(np.logical_not(a))] = False return ret def _any(group_idx, a, size, fill_value, dtype=None): check_boolean(fill_value) ret = np.full(size, fill_value, dtype=bool) if fill_value: ret[group_idx] = False ret[group_idx.compress(a)] = True return ret def _min(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype(fill_value, dtype or a.dtype) dmax = np.iinfo(a.dtype).max if issubclass(a.dtype.type, np.integer)\ else np.finfo(a.dtype).max ret = np.full(size, fill_value, dtype=dtype) if fill_value != dmax: ret[group_idx] = dmax # min starts from maximum np.minimum.at(ret, group_idx, a) return ret def _max(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype(fill_value, dtype or a.dtype) dmin = np.iinfo(a.dtype).min if issubclass(a.dtype.type, np.integer)\ else np.finfo(a.dtype).min ret = np.full(size, fill_value, dtype=dtype) if fill_value != dmin: ret[group_idx] = dmin # max starts from minimum np.maximum.at(ret, group_idx, a) return ret def _argmax(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype(fill_value, dtype or int) dmin = np.iinfo(a.dtype).min if issubclass(a.dtype.type, np.integer)\ else np.finfo(a.dtype).min group_max = _max(group_idx, a, size, dmin) is_max = a == group_max[group_idx] ret = np.full(size, fill_value, dtype=dtype) group_idx_max = group_idx[is_max] argmax, = is_max.nonzero() ret[group_idx_max[::-1]] = argmax[::-1] # reverse to ensure first value for each group wins return ret def _argmin(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype(fill_value, dtype or int) dmax = np.iinfo(a.dtype).max if issubclass(a.dtype.type, np.integer)\ else np.finfo(a.dtype).max group_min = _min(group_idx, a, size, dmax) is_min = a == group_min[group_idx] ret = np.full(size, fill_value, dtype=dtype) group_idx_min = group_idx[is_min] argmin, = is_min.nonzero() ret[group_idx_min[::-1]] = argmin[::-1] # reverse to ensure first value for each group wins return ret def _mean(group_idx, a, size, fill_value, dtype=np.dtype(np.float64)): if np.ndim(a) == 0: raise ValueError("cannot take mean with scalar a") counts = np.bincount(group_idx, minlength=size) if np.iscomplexobj(a): dtype = a.dtype # TODO: this is a bit clumsy sums = np.empty(size, dtype=dtype) sums.real = np.bincount(group_idx, weights=a.real, minlength=size) sums.imag = np.bincount(group_idx, weights=a.imag, minlength=size) else: sums = np.bincount(group_idx, weights=a, minlength=size).astype(dtype) with np.errstate(divide='ignore', invalid='ignore'): ret = sums.astype(dtype) / counts if not np.isnan(fill_value): ret[counts == 0] = fill_value return ret def _var(group_idx, a, size, fill_value, dtype=np.dtype(np.float64), sqrt=False, ddof=0): if np.ndim(a) == 0: raise ValueError("cannot take variance with scalar a") counts = np.bincount(group_idx, minlength=size) sums = np.bincount(group_idx, weights=a, minlength=size) with np.errstate(divide='ignore'): means = sums.astype(dtype) / counts ret = np.bincount(group_idx, (a - means[group_idx]) ** 2, minlength=size) / (counts - ddof) if sqrt: ret = np.sqrt(ret) # this is now std not var if not np.isnan(fill_value): ret[counts == 0] = fill_value return ret def _std(group_idx, a, size, fill_value, dtype=np.dtype(np.float64), ddof=0): return _var(group_idx, a, size, fill_value, dtype=dtype, sqrt=True, ddof=ddof) def _allnan(group_idx, a, size, fill_value, dtype=bool): return _all(group_idx, np.isnan(a), size, fill_value=fill_value, dtype=dtype) def _anynan(group_idx, a, size, fill_value, dtype=bool): return _any(group_idx, np.isnan(a), size, fill_value=fill_value, dtype=dtype) def _sort(group_idx, a, size=None, fill_value=None, dtype=None, reverse=False): sortidx = np.lexsort((-a if reverse else a, group_idx)) # Reverse sorting back to into grouped order, but preserving groupwise sorting revidx = np.argsort(np.argsort(group_idx, kind='mergesort'), kind='mergesort') return a[sortidx][revidx] def _array(group_idx, a, size, fill_value, dtype=None): """groups a into separate arrays, keeping the order intact.""" if fill_value is not None and not (np.isscalar(fill_value) or len(fill_value) == 0): raise ValueError("fill_value must be None, a scalar or an empty " "sequence") order_group_idx = np.argsort(group_idx, kind='mergesort') counts = np.bincount(group_idx, minlength=size) ret = np.split(a[order_group_idx], np.cumsum(counts)[:-1]) ret = np.asanyarray(ret) if fill_value is None or np.isscalar(fill_value): _fill_untouched(group_idx, ret, fill_value) return ret def _generic_callable(group_idx, a, size, fill_value, dtype=None, func=lambda g: g, **kwargs): """groups a by inds, and then applies foo to each group in turn, placing the results in an array.""" groups = _array(group_idx, a, size, ()) ret = np.full(size, fill_value, dtype=dtype or np.float64) for i, grp in enumerate(groups): if np.ndim(grp) == 1 and len(grp) > 0: ret[i] = func(grp) return ret def _cumsum(group_idx, a, size, fill_value=None, dtype=None): """ N to N aggregate operation of cumsum. Perform cumulative sum for each group. group_idx = np.array([4, 3, 3, 4, 4, 1, 1, 1, 7, 8, 7, 4, 3, 3, 1, 1]) a = np.array([3, 4, 1, 3, 9, 9, 6, 7, 7, 0, 8, 2, 1, 8, 9, 8]) _cumsum(group_idx, a, np.max(group_idx) + 1) >>> array([ 3, 4, 5, 6, 15, 9, 15, 22, 7, 0, 15, 17, 6, 14, 31, 39]) """ sortidx = np.argsort(group_idx, kind='mergesort') invsortidx = np.argsort(sortidx, kind='mergesort') group_idx_srt = group_idx[sortidx] a_srt = a[sortidx] a_srt_cumsum = np.cumsum(a_srt, dtype=dtype) increasing = np.arange(len(a), dtype=int) group_starts = _min(group_idx_srt, increasing, size, fill_value=0)[group_idx_srt] a_srt_cumsum += -a_srt_cumsum[group_starts] + a_srt[group_starts] return a_srt_cumsum[invsortidx] def _nancumsum(group_idx, a, size, fill_value=None, dtype=None): a_nonans = np.where(np.isnan(a), 0, a) group_idx_nonans = np.where(np.isnan(group_idx), np.nanmax(group_idx) + 1, group_idx) return _cumsum(group_idx_nonans, a_nonans, size, fill_value=fill_value, dtype=dtype) _impl_dict = dict(min=_min, max=_max, sum=_sum, prod=_prod, last=_last, first=_first, all=_all, any=_any, mean=_mean, std=_std, var=_var, anynan=_anynan, allnan=_allnan, sort=_sort, array=_array, argmax=_argmax, argmin=_argmin, len=_len, cumsum=_cumsum, generic=_generic_callable) _impl_dict.update(('nan' + k, v) for k, v in list(_impl_dict.items()) if k not in funcs_no_separate_nan) def _aggregate_base(group_idx, a, func='sum', size=None, fill_value=0, order='C', dtype=None, axis=None, _impl_dict=_impl_dict, _nansqueeze=False, cache=None, **kwargs): group_idx, a, flat_size, ndim_idx, size = input_validation(group_idx, a, size=size, order=order, axis=axis) if group_idx.dtype == np.dtype("uint64"): # Force conversion to signed int, to avoid issues with bincount etc later group_idx = group_idx.astype(int) func = get_func(func, aliasing, _impl_dict) if not isstr(func): # do simple grouping and execute function in loop ret = _impl_dict.get('generic', _generic_callable)(group_idx, a, flat_size, fill_value, func=func, dtype=dtype, **kwargs) else: # deal with nans and find the function if func.startswith('nan'): if np.ndim(a) == 0: raise ValueError("nan-version not supported for scalar input.") if _nansqueeze: good = ~np.isnan(a) a = a[good] group_idx = group_idx[good] dtype = check_dtype(dtype, func, a, flat_size) check_fill_value(fill_value, dtype, func=func) func = _impl_dict[func] ret = func(group_idx, a, flat_size, fill_value=fill_value, dtype=dtype, **kwargs) # deal with ndimensional indexing if ndim_idx > 1: ret = ret.reshape(size, order=order) return ret def aggregate(group_idx, a, func='sum', size=None, fill_value=0, order='C', dtype=None, axis=None, **kwargs): return _aggregate_base(group_idx, a, size=size, fill_value=fill_value, order=order, dtype=dtype, func=func, axis=axis, _impl_dict=_impl_dict, _nansqueeze=True, **kwargs) aggregate.__doc__ = """ This is the pure numpy implementation of aggregate. """ + aggregate_common_doc def _fill_untouched(idx, ret, fill_value): """any elements of ret not indexed by idx are set to fill_value.""" untouched = np.ones_like(ret, dtype=bool) untouched[idx] = False ret[untouched] = fill_value numpy-groupies-0.9.13/numpy_groupies/aggregate_numpy_ufunc.py000066400000000000000000000071661372771202100246400ustar00rootroot00000000000000import numpy as np from .utils import get_func, check_boolean, isstr, aggregate_common_doc from .utils_numpy import aliasing, minimum_dtype, minimum_dtype_scalar from .aggregate_numpy import _aggregate_base def _anynan(group_idx, a, size, fill_value, dtype=None): return _any(group_idx, np.isnan(a), size, fill_value=fill_value, dtype=dtype) def _allnan(group_idx, a, size, fill_value, dtype=None): return _all(group_idx, np.isnan(a), size, fill_value=fill_value, dtype=dtype) def _any(group_idx, a, size, fill_value, dtype=None): check_boolean(fill_value) ret = np.full(size, fill_value, dtype=bool) if fill_value: ret[group_idx] = False # any-test should start from False np.logical_or.at(ret, group_idx, a) return ret def _all(group_idx, a, size, fill_value, dtype=None): check_boolean(fill_value) ret = np.full(size, fill_value, dtype=bool) if not fill_value: ret[group_idx] = True # all-test should start from True np.logical_and.at(ret, group_idx, a) return ret def _sum(group_idx, a, size, fill_value, dtype=None): dtype = minimum_dtype_scalar(fill_value, dtype, a) ret = np.full(size, fill_value, dtype=dtype) if fill_value != 0: ret[group_idx] = 0 # sums should start at 0 np.add.at(ret, group_idx, a) return ret def _len(group_idx, a, size, fill_value, dtype=None): return _sum(group_idx, 1, size, fill_value, dtype=int) def _prod(group_idx, a, size, fill_value, dtype=None): """Same as aggregate_numpy.py""" dtype = minimum_dtype_scalar(fill_value, dtype, a) ret = np.full(size, fill_value, dtype=dtype) if fill_value != 1: ret[group_idx] = 1 # product should start from 1 np.multiply.at(ret, group_idx, a) return ret def _min(group_idx, a, size, fill_value, dtype=None): """Same as aggregate_numpy.py""" dtype = minimum_dtype(fill_value, dtype or a.dtype) dmax = np.iinfo(a.dtype).max if issubclass(a.dtype.type, np.integer)\ else np.finfo(a.dtype).max ret = np.full(size, fill_value, dtype=dtype) if fill_value != dmax: ret[group_idx] = dmax # min starts from maximum np.minimum.at(ret, group_idx, a) return ret def _max(group_idx, a, size, fill_value, dtype=None): """Same as aggregate_numpy.py""" dtype = minimum_dtype(fill_value, dtype or a.dtype) dmin = np.iinfo(a.dtype).min if issubclass(a.dtype.type, np.integer)\ else np.finfo(a.dtype).min ret = np.full(size, fill_value, dtype=dtype) if fill_value != dmin: ret[group_idx] = dmin # max starts from minimum np.maximum.at(ret, group_idx, a) return ret _impl_dict = dict(min=_min, max=_max, sum=_sum, prod=_prod, all=_all, any=_any, allnan=_allnan, anynan=_anynan, len=_len) def aggregate(group_idx, a, func='sum', size=None, fill_value=0, order='C', dtype=None, axis=None, **kwargs): func = get_func(func, aliasing, _impl_dict) if not isstr(func): raise NotImplementedError("No such ufunc available") return _aggregate_base(group_idx, a, size=size, fill_value=fill_value, order=order, dtype=dtype, func=func, axis=axis, _impl_dict=_impl_dict, _nansqueeze=False, **kwargs) aggregate.__doc__ = """ Unlike ``aggregate_numpy``, which in most cases does some custom optimisations, this version simply uses ``numpy``'s ``ufunc.at``. As of version 1.14 this gives fairly poor performance. There should normally be no need to use this version, it is intended to be used in testing and benchmarking only. """ + aggregate_common_doc numpy-groupies-0.9.13/numpy_groupies/aggregate_pandas.py000066400000000000000000000044201372771202100235240ustar00rootroot00000000000000from functools import partial import numpy as np import pandas as pd from .utils import isstr, aggregate_common_doc, funcs_no_separate_nan from .utils_numpy import allnan, anynan, check_dtype from .aggregate_numpy import _aggregate_base def _wrapper(group_idx, a, size, fill_value, func='sum', dtype=None, ddof=0, **kwargs): funcname = func.__name__ if callable(func) else func kwargs = dict() if funcname in ('var', 'std'): kwargs['ddof'] = ddof df = pd.DataFrame({'group_idx': group_idx, 'a': a}) if func == "sort": grouped = df.groupby('group_idx', sort=True) else: grouped = df.groupby('group_idx', sort=False).aggregate(func, **kwargs) dtype = check_dtype(dtype, getattr(func, '__name__', funcname), a, size) if funcname.startswith('cum'): ret = grouped.values[:, 0] else: ret = np.full(size, fill_value, dtype=dtype) ret[grouped.index] = grouped.values[:, 0] return ret _supported_funcs = 'sum prod all any min max mean var std first last cumsum cumprod cummax cummin'.split() _impl_dict = {fn: partial(_wrapper, func=fn) for fn in _supported_funcs} _impl_dict.update(('nan' + fn, partial(_wrapper, func=fn)) for fn in _supported_funcs if fn not in funcs_no_separate_nan) _impl_dict.update(allnan=partial(_wrapper, func=allnan), anynan=partial(_wrapper, func=anynan), len=partial(_wrapper, func='count'), nanlen=partial(_wrapper, func='count'), argmax=partial(_wrapper, func='idxmax'), argmin=partial(_wrapper, func='idxmin'), generic=_wrapper) def aggregate(group_idx, a, func='sum', size=None, fill_value=0, order='C', dtype=None, axis=None, **kwargs): nansqueeze = isstr(func) and func.startswith('nan') return _aggregate_base(group_idx, a, size=size, fill_value=fill_value, order=order, dtype=dtype, func=func, axis=axis, _impl_dict=_impl_dict, _nansqueeze=nansqueeze, **kwargs) aggregate.__doc__ = """ This is the pandas implementation of aggregate. It makes use of `pandas`'s groupby machienery and is mainly used for reference and benchmarking. """ + aggregate_common_doc numpy-groupies-0.9.13/numpy_groupies/aggregate_purepy.py000066400000000000000000000106411372771202100236040ustar00rootroot00000000000000from __future__ import division import math import itertools import operator from .utils import (get_func, aliasing, funcs_no_separate_nan, aggregate_common_doc, isstr) # min - builtin # max - builtin # sum - builtin # all - builtin # any - builtin def _last(x): return x[-1] def _first(x): return x[0] def _array(x): return x def _mean(x): return sum(x) / len(x) def _var(x, ddof=0): mean = _mean(x) return sum((xx - mean) ** 2 for xx in x) / (len(x) - ddof) def _std(x, ddof=0): return math.sqrt(_var(x, ddof=ddof)) def _prod(x): r = x[0] for xx in x[1:]: r *= xx return r def _anynan(x): return any(math.isnan(xx) for xx in x) def _allnan(x): return all(math.isnan(xx) for xx in x) def _argmax(x_and_idx): return max(x_and_idx, key=operator.itemgetter(1))[0] _argmax.x_and_idx = True # tell aggregate what to use as first arg def _argmin(x_and_idx): return min(x_and_idx, key=operator.itemgetter(1))[0] _argmin.x_and_idx = True # tell aggregate what to use as first arg def _sort(group_idx, a, reverse=False): def _argsort(unordered): return sorted(range(len(unordered)), key=lambda k: unordered[k]) sortidx = _argsort(list((gi, aj) for gi, aj in zip(group_idx, -a if reverse else a))) revidx = _argsort(_argsort(group_idx)) a_srt = [a[si] for si in sortidx] return [a_srt[ri] for ri in revidx] _impl_dict = dict(min=min, max=max, sum=sum, prod=_prod, last=_last, first=_first, all=all, any=any, mean=_mean, std=_std, var=_var, anynan=_anynan, allnan=_allnan, sort=_sort, array=_array, argmax=_argmax, argmin=_argmin, len=len) _impl_dict.update(('nan' + k, v) for k, v in list(_impl_dict.items()) if k not in funcs_no_separate_nan) def aggregate(group_idx, a, func='sum', size=None, fill_value=0, order=None, dtype=None, axis=None, **kwargs): if axis is not None: raise NotImplementedError("axis arg not supported in purepy implementation.") # Check for 2d group_idx if size is None: size = 1 + int(max(group_idx)) for i in group_idx: try: i = int(i) except (TypeError, ValueError): if isinstance(i, (list, tuple)): raise NotImplementedError("pure python implementation doesn't" " accept ndim idx input.") else: try: len(i) except TypeError: raise ValueError("invalid value found in group_idx: %s" % i) else: raise NotImplementedError("pure python implementation doesn't " "accept ndim indexed input.") else: if i < 0: raise ValueError("group_idx contains negative value") func = get_func(func, aliasing, _impl_dict) if isinstance(a, (int, float)): if func not in ("sum", "prod", "len"): raise ValueError("scalar inputs are supported only for 'sum', " "'prod' and 'len'") a = [a] * len(group_idx) elif len(group_idx) != len(a): raise ValueError("group_idx and a must be of the same length") if isstr(func): if func.startswith('nan'): func = func[3:] # remove nans group_idx, a = zip(*((ix, val) for ix, val in zip(group_idx, a) if not math.isnan(val))) func = _impl_dict[func] if func is _sort: return _sort(group_idx, a, reverse=kwargs.get('reverse', False)) # sort data and evaluate function on groups ret = [fill_value] * size if not getattr(func, 'x_and_idx', False): data = sorted(zip(group_idx, a), key=operator.itemgetter(0)) for ix, group in itertools.groupby(data, key=operator.itemgetter(0)): ret[ix] = func(list(val for _, val in group), **kwargs) else: data = sorted(zip(range(len(a)), group_idx, a), key=operator.itemgetter(1)) for ix, group in itertools.groupby(data, key=operator.itemgetter(1)): ret[ix] = func(list((val_idx, val) for val_idx, _, val in group), **kwargs) return ret aggregate.__doc__ = """ This is the pure python implementation of aggregate. It is terribly slow. Using the numpy version is highly recommended. """ + aggregate_common_doc numpy-groupies-0.9.13/numpy_groupies/aggregate_weave.py000066400000000000000000000216111372771202100233660ustar00rootroot00000000000000import numpy as np try: from weave import inline except ImportError: from scipy.weave import inline from .utils import get_func, isstr, funcs_no_separate_nan, aggregate_common_doc, check_boolean from .utils_numpy import check_dtype, aliasing, check_fill_value, input_validation optimized_funcs = {'sum', 'min', 'max', 'amin', 'amax', 'mean', 'var', 'std', 'prod', 'len', 'nansum', 'nanmin', 'nanmax', 'nanmean', 'nanvar', 'nanstd', 'nanprod', 'nanlen', 'all', 'any', 'nanall', 'nanany', 'allnan', 'anynan', 'first', 'last', 'nanfirst', 'nanlast'} # c_funcs will contain all generated c code, so it can be read easily for debugging c_funcs = dict() c_iter = dict() c_iter_scalar = dict() c_finish = dict() # Set this for testing, to fail deprecated C-API calls #c_macros = [('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION')] c_macros = [] c_args = ['-Wno-cpp'] # Suppress the deprecation warnings created by weave def c_size(varname): return r""" long L%(varname)s = 1; for (int n=0; n=0; i--) { %(ri_redir)s %(iter)s } %(finish)s """ c_iter['sum'] = r""" counter[ri] = 0; ret[ri] += a[i];""" c_iter_scalar['sum'] = r""" counter[ri] = 0; ret[ri] += a;""" c_iter['prod'] = r""" counter[ri] = 0; ret[ri] *= a[i];""" c_iter_scalar['prod'] = r""" counter[ri] = 0; ret[ri] *= a;""" c_iter['len'] = r""" counter[ri] = 0; ret[ri] += 1;""" c_iter_scalar['len'] = r""" counter[ri] = 0; ret[ri] += 1;""" c_iter['all'] = r""" counter[ri] = 0; ret[ri] &= (a[i] != 0);""" c_iter['any'] = r""" counter[ri] = 0; ret[ri] |= (a[i] != 0);""" c_iter['last'] = r""" ret[ri] = a[i];""" c_iter['allnan'] = r""" counter[ri] = 0; ret[ri] &= (a[i] == a[i]);""" c_iter['anynan'] = r""" counter[ri] = 0; ret[ri] |= (a[i] == a[i]);""" c_iter['max'] = r""" if (counter[ri]) { ret[ri] = a[i]; counter[ri] = 0; } else if (ret[ri] < a[i]) ret[ri] = a[i];""" c_iter['min'] = r""" if (counter[ri]) { ret[ri] = a[i]; counter[ri] = 0; } else if (ret[ri] > a[i]) ret[ri] = a[i];""" c_iter['mean'] = r""" counter[ri]++; ret[ri] += a[i];""" c_finish['mean'] = r""" for (long ri=0; ri 1: ret = ret.reshape(size, order=order) return ret aggregate.__doc__ = """ This is the weave based implementation of aggregate. **NOTE:** If weave is installed but fails to run (probably because you have not setup a suitable compiler) then you can manually select the numpy implementation by using:: import numpy_groupies as npg # NOT THIS: npg.aggregate(...) npg.aggregate_np(...) """ + aggregate_common_doc numpy-groupies-0.9.13/numpy_groupies/benchmarks/000077500000000000000000000000001372771202100220135ustar00rootroot00000000000000numpy-groupies-0.9.13/numpy_groupies/benchmarks/__init__.py000066400000000000000000000000001372771202100241120ustar00rootroot00000000000000numpy-groupies-0.9.13/numpy_groupies/benchmarks/generic.py000066400000000000000000000100121372771202100237730ustar00rootroot00000000000000#!/usr/bin/python -B from __future__ import print_function import sys import platform import timeit from operator import itemgetter import numpy as np from numpy_groupies.tests import _implementations, aggregate_numpy from numpy_groupies.utils_numpy import allnan, anynan, nanfirst, nanlast def aggregate_grouploop(*args, **kwargs): """wraps func in lambda which prevents aggregate_numpy from recognising and optimising it. Instead it groups and loops.""" extrafuncs = {'allnan': allnan, 'anynan': anynan, 'first': itemgetter(0), 'last': itemgetter(-1), 'nanfirst': nanfirst, 'nanlast': nanlast} func = kwargs.pop('func') func = extrafuncs.get(func, func) if isinstance(func, str): raise NotImplementedError("Grouploop needs to be called with a function") return aggregate_numpy.aggregate(*args, func=lambda x: func(x), **kwargs) def arbitrary(iterator): tmp = 0 for i, x in enumerate(iterator, 1): tmp += x ** i return tmp func_list = (np.sum, np.prod, np.min, np.max, len, np.all, np.any, 'anynan', 'allnan', np.mean, np.std, np.var, 'first', 'last', 'argmax', 'argmin', np.nansum, np.nanprod, np.nanmin, np.nanmax, 'nanlen', 'nanall', 'nanany', np.nanmean, np.nanvar, np.nanstd, 'nanfirst', 'nanlast', 'cumsum', 'cumprod', 'cummax', 'cummin', arbitrary, 'sort') def benchmark_data(size=5e5, seed=100): rnd = np.random.RandomState(seed=seed) group_idx = rnd.randint(0, int(1e3), int(size)) a = rnd.random_sample(group_idx.size) a[a > 0.8] = 0 nana = a.copy() nana[(nana < 0.2) & (nana != 0)] = np.nan nan_share = np.mean(np.isnan(nana)) assert 0.15 < nan_share < 0.25, "%3f%% nans" % (nan_share * 100) return a, nana, group_idx def benchmark(implementations, repeat=5, size=5e5, seed=100): a, nana, group_idx = benchmark_data(size=size, seed=seed) print("function" + ''.join(impl.__name__.rsplit('_', 1)[1].rjust(14) for impl in implementations)) print("-" * (9 + 14 * len(implementations))) for func in func_list: func_name = getattr(func, '__name__', func) print(func_name.ljust(9), end='') results = [] used_a = nana if 'nan' in func_name else a for impl in implementations: if impl is None: print('----'.rjust(14), end='') continue aggregatefunc = impl.aggregate try: res = aggregatefunc(group_idx, used_a, func=func, cache=True) except NotImplementedError: print('----'.rjust(14), end='') continue except Exception: print('ERROR'.rjust(14), end='') else: results.append(res) try: np.testing.assert_array_almost_equal(res, results[0]) except AssertionError: print('FAIL'.rjust(14), end='') else: t0 = min(timeit.Timer(lambda: aggregatefunc(group_idx, used_a, func=func, cache=True)).repeat(repeat=repeat, number=1)) print(("%.3f" % (t0 * 1000)).rjust(14), end='') sys.stdout.flush() print() implementation_names = [impl.__name__.rsplit('_', 1)[1] for impl in implementations] postfix = '' if 'numba' in implementation_names: import numba postfix += ', Numba %s' % numba.__version__ if 'weave' in implementation_names: import weave postfix += ', Weave %s' % weave.__version__ if 'pandas' in implementation_names: import pandas postfix += ', Pandas %s' % pandas.__version__ print("%s(%s), Python %s, Numpy %s%s" % (platform.system(), platform.machine(), sys.version.split()[0], np.version.version, postfix)) if __name__ == '__main__': implementations = _implementations if '--purepy' in sys.argv else _implementations[1:] implementations = implementations if '--pandas' in sys.argv else implementations[:-1] benchmark(implementations) numpy-groupies-0.9.13/numpy_groupies/benchmarks/simple.py000066400000000000000000000102301372771202100236520ustar00rootroot00000000000000#!/usr/bin/python -B # -*- coding: utf-8 -*- from __future__ import print_function import timeit import numpy as np from numpy_groupies.utils import aliasing from numpy_groupies import aggregate_py, aggregate_np, aggregate_ufunc from numpy_groupies.aggregate_pandas import aggregate as aggregate_pd def aggregate_group_loop(*args, **kwargs): """wraps func in lambda which prevents aggregate_numpy from recognising and optimising it. Instead it groups and loops.""" func = kwargs['func'] del kwargs['func'] return aggregate_np(*args, func=lambda x: func(x), **kwargs) print("TODO: use more extensive tests") print("") print("-----simple examples----------") test_a = np.array([12.0, 3.2, -15, 88, 12.9]) test_group_idx = np.array([1, 0, 1, 4, 1 ]) print("test_a: ", test_a) print("test_group_idx: ", test_group_idx) print("aggregate(test_group_idx, test_a):") print(aggregate_np(test_group_idx, test_a)) # group vals by idx and sum # array([3.2, 9.9, 0., 0., 88.]) print("aggregate(test_group_idx, test_a, sz=8, func='min', fill_value=np.nan):") print(aggregate_np(test_group_idx, test_a, size=8, func='min', fill_value=np.nan)) # array([3.2, -15., nan, 88., nan, nan, nan, nan]) print("aggregate(test_group_idx, test_a, sz=5, func=lambda x: ' + '.join(str(xx) for xx in x),fill_value='')") print(aggregate_np(test_group_idx, test_a, size=5, func=lambda x: ' + '.join(str(xx) for xx in x), fill_value='')) print("") print("---------testing--------------") print("compare against group-and-loop with numpy") testable_funcs = {aliasing[f]: f for f in (np.sum, np.prod, np.any, np.all, np.min, np.max, np.std, np.var, np.mean)} test_group_idx = np.random.randint(0, int(1e3), int(1e5)) test_a = np.random.rand(int(1e5)) * 100 - 50 test_a[test_a > 25] = 0 # for use with bool functions for name, f in testable_funcs.items(): numpy_loop_group = aggregate_group_loop(test_group_idx, test_a, func=f) for acc_func, acc_name in [(aggregate_np, 'np-optimised'), (aggregate_ufunc, 'np-ufunc-at'), (aggregate_py, 'purepy'), (aggregate_pd, 'pandas')]: try: test_out = acc_func(test_group_idx, test_a, func=name) test_out = np.asarray(test_out) if not np.allclose(test_out, numpy_loop_group.astype(test_out.dtype)): print(name, acc_name, "FAILED test, output: [" + acc_name + "; correct]...") print(np.vstack((test_out, numpy_loop_group))) else: print(name, acc_name, "PASSED test") except NotImplementedError: print(name, acc_name, "NOT IMPLEMENTED") print("") print("----------benchmarking-------------") print("Note that the actual observed speedup depends on a variety of properties of the input.") print("Here we are using 100,000 indices uniformly picked from [0, 1000).") print("Specifically, about 25% of the values are 0 (for use with bool operations),") print("the remainder are uniformly distribuited on [-50,25).") print("Times are scaled to 10 repetitions (actual number of reps used may not be 10).") print(''.join(['function'.rjust(8), 'pure-py'.rjust(14), 'np-grouploop'.rjust(14), 'np-ufuncat'.rjust(14), 'np-optimised'.rjust(14), 'pandas'.rjust(14), 'ratio'.rjust(15)])) for name, f in testable_funcs.items(): print(name.rjust(8), end='') times = [None] * 5 for ii, acc_func in enumerate([aggregate_py, aggregate_group_loop, aggregate_ufunc, aggregate_np, aggregate_pd]): try: func = f if acc_func is aggregate_group_loop else name reps = 3 if acc_func is aggregate_py else 20 times[ii] = timeit.Timer(lambda: acc_func(test_group_idx, test_a, func=func)).timeit(number=reps) / reps * 10 print(("%.1fms" % ((times[ii] * 1000))).rjust(13), end='') except NotImplementedError: print("no-impl".rjust(13), end='') denom = min(t for t in times if t is not None) ratios = [("-".center(4) if t is None else str(round(t / denom, 1))).center(5) for t in times] print(" ", (":".join(ratios))) numpy-groupies-0.9.13/numpy_groupies/tests/000077500000000000000000000000001372771202100210405ustar00rootroot00000000000000numpy-groupies-0.9.13/numpy_groupies/tests/__init__.py000066400000000000000000000033631372771202100231560ustar00rootroot00000000000000import pytest from .. import aggregate_purepy, aggregate_numpy_ufunc, aggregate_numpy try: from .. import aggregate_numba except ImportError: aggregate_numba = None try: from .. import aggregate_weave except ImportError: aggregate_weave = None try: from .. import aggregate_pandas except ImportError: aggregate_pandas = None _implementations = [aggregate_purepy, aggregate_numpy_ufunc, aggregate_numpy, aggregate_numba, aggregate_weave, aggregate_pandas] _implementations = [i for i in _implementations if i is not None] def _impl_name(impl): if not impl: return return impl.__name__.rsplit('aggregate_', 1)[1].rsplit('_', 1)[-1] _not_implemented_by_impl_name = { 'numpy': ['cumprod','cummax', 'cummin'], 'purepy': ['cumprod','cummax', 'cummin'], 'numba': ('array', 'list'), 'pandas': ('array', 'list'), 'weave': ('argmin', 'argmax', 'array', 'list', 'cumsum', '', 'func_preserve_order', 'func_arbitrary')} def _wrap_notimplemented_xfail(impl, name=None): """Some implementations lack some functionality. That's ok, let's xfail that instead of raising errors.""" def _try_xfail(*args, **kwargs): try: return impl(*args, **kwargs) except NotImplementedError as e: func = kwargs.pop('func', None) if callable(func): func = func.__name__ wrap_funcs = _not_implemented_by_impl_name.get(func, None) if wrap_funcs is None or func in wrap_funcs: pytest.xfail("Functionality not implemented") else: raise e if name: _try_xfail.__name__ = name else: _try_xfail.__name__ = impl.__name__ return _try_xfail numpy-groupies-0.9.13/numpy_groupies/tests/test_compare.py000066400000000000000000000103711372771202100241010ustar00rootroot00000000000000""" In this test, aggregate_numpy is taken as a reference implementation and this results are compared against the results of the other implementations. Implementations may throw NotImplementedError in order to show missing functionality without throwing test errors. """ from itertools import product import numpy as np import pytest from . import (aggregate_purepy, aggregate_numpy_ufunc, aggregate_numpy, aggregate_weave, aggregate_numba, aggregate_pandas, _wrap_notimplemented_xfail, _impl_name) class AttrDict(dict): __getattr__ = dict.__getitem__ @pytest.fixture(params=['np/py', 'weave/np', 'ufunc/np', 'numba/np', 'pandas/np'], scope='module') def aggregate_cmp(request, seed=100): test_pair = request.param if test_pair == 'np/py': # Some functions in purepy are not implemented func_ref = _wrap_notimplemented_xfail(aggregate_purepy.aggregate) func = aggregate_numpy.aggregate group_cnt = 100 else: group_cnt = 1000 func_ref = aggregate_numpy.aggregate if 'ufunc' in request.param: impl = aggregate_numpy_ufunc elif 'numba' in request.param: impl = aggregate_numba elif 'weave' in request.param: impl = aggregate_weave elif 'pandas' in request.param: impl = aggregate_pandas else: impl = None if not impl: pytest.xfail("Implementation not available") name = _impl_name(impl) func = _wrap_notimplemented_xfail(impl.aggregate, 'aggregate_' + name) rnd = np.random.RandomState(seed=seed) # Gives 100000 duplicates of size 10 each group_idx = np.repeat(np.arange(group_cnt), 2) rnd.shuffle(group_idx) group_idx = np.repeat(group_idx, 10) a = rnd.randn(group_idx.size) nana = a.copy() nana[::3] = np.nan somea = a.copy() somea[somea < 0.3] = 0 somea[::31] = np.nan return AttrDict(locals()) def func_arbitrary(iterator): tmp = 0 for x in iterator: tmp += x * x return tmp def func_preserve_order(iterator): tmp = 0 for i, x in enumerate(iterator, 1): tmp += x ** i return tmp func_list = ('sum', 'prod', 'min', 'max', 'all', 'any', 'mean', 'std', 'var', 'len', 'argmin', 'argmax', 'anynan', 'allnan', 'cumsum', func_arbitrary, func_preserve_order, 'nansum', 'nanprod', 'nanmin', 'nanmax', 'nanmean', 'nanstd', 'nanvar','nanlen') @pytest.mark.parametrize(["func", "fill_value"], product(func_list, [0, 1, np.nan]), ids=lambda x: getattr(x, '__name__', x)) def test_cmp(aggregate_cmp, func, fill_value, decimal=10): a = aggregate_cmp.nana if 'nan' in getattr(func, '__name__', func) else aggregate_cmp.a try: ref = aggregate_cmp.func_ref(aggregate_cmp.group_idx, a, func=func, fill_value=fill_value) except ValueError: with pytest.raises(ValueError): aggregate_cmp.func(aggregate_cmp.group_idx, a, func=func, fill_value=fill_value) else: try: res = aggregate_cmp.func(aggregate_cmp.group_idx, a, func=func, fill_value=fill_value) except ValueError: if np.isnan(fill_value) and aggregate_cmp.test_pair.endswith('py'): pytest.skip("pure python version uses lists and does not raise ValueErrors when inserting nan into integers") else: raise if isinstance(ref, np.ndarray): assert res.dtype == ref.dtype np.testing.assert_allclose(res, ref, rtol=10**-decimal) @pytest.mark.parametrize(["ndim", "order"], product([2, 3], ["C", "F"])) def test_cmp_ndim(aggregate_cmp, ndim, order, outsize=100, decimal=14): nindices = int(outsize ** ndim) outshape = tuple([outsize] * ndim) group_idx = np.random.randint(0, outsize, size=(ndim, nindices)) a = np.random.random(group_idx.shape[1]) res = aggregate_cmp.func(group_idx, a, size=outshape, order=order) ref = aggregate_cmp.func_ref(group_idx, a, size=outshape, order=order) if ndim > 1 and order == 'F': # 1d arrays always return False here assert np.isfortran(res) else: assert not np.isfortran(res) assert res.shape == outshape np.testing.assert_array_almost_equal(res, ref, decimal=decimal) numpy-groupies-0.9.13/numpy_groupies/tests/test_generic.py000066400000000000000000000253661372771202100241010ustar00rootroot00000000000000""" Tests, that are run against all implemented versions of aggregate. """ import itertools import numpy as np import pytest from . import _implementations, _impl_name, _wrap_notimplemented_xfail @pytest.fixture(params=_implementations, ids=_impl_name) def aggregate_all(request): impl = request.param if impl is None: pytest.xfail("Implementation not available") name = _impl_name(impl) return _wrap_notimplemented_xfail(impl.aggregate, 'aggregate_' + name) def test_preserve_missing(aggregate_all): res = aggregate_all(np.array([0, 1, 3, 1, 3]), np.arange(101, 106, dtype=int)) np.testing.assert_array_equal(res, np.array([101, 206, 0, 208])) if not isinstance(res, list): assert 'int' in res.dtype.name @pytest.mark.parametrize("group_idx_type", [int, "uint32", "uint64"]) def test_uint_group_idx(aggregate_all, group_idx_type): group_idx = np.array([1, 1, 2, 2, 2, 2, 4, 4], dtype=group_idx_type) res = aggregate_all(group_idx, np.ones(group_idx.size), dtype=int) np.testing.assert_array_equal(res, np.array([0, 2, 4, 0, 2])) if not isinstance(res, list): assert 'int' in res.dtype.name def test_start_with_offset(aggregate_all): group_idx = np.array([1, 1, 2, 2, 2, 2, 4, 4]) res = aggregate_all(group_idx, np.ones(group_idx.size), dtype=int) np.testing.assert_array_equal(res, np.array([0, 2, 4, 0, 2])) if not isinstance(res, list): assert 'int' in res.dtype.name @pytest.mark.parametrize("floatfunc", [np.std, np.var, np.mean], ids=lambda x: x.__name__) def test_float_enforcement(aggregate_all, floatfunc): group_idx = np.arange(10).repeat(3) a = np.arange(group_idx.size) res = aggregate_all(group_idx, a, floatfunc) if not isinstance(res, list): assert 'float' in res.dtype.name assert np.all(np.array(res) > 0) def test_start_with_offset_prod(aggregate_all): group_idx = np.array([2, 2, 4, 4, 4, 7, 7, 7]) res = aggregate_all(group_idx, group_idx, func=np.prod, dtype=int) np.testing.assert_array_equal(res, np.array([0, 0, 4, 0, 64, 0, 0, 343])) def test_no_negative_indices(aggregate_all): for pos in (0, 10, -1): group_idx = np.arange(5).repeat(5) group_idx[pos] = -1 pytest.raises(ValueError, aggregate_all, group_idx, np.arange(len(group_idx))) def test_parameter_missing(aggregate_all): pytest.raises(TypeError, aggregate_all, np.arange(5)) def test_shape_mismatch(aggregate_all): pytest.raises(ValueError, aggregate_all, np.array((1, 2, 3)), np.array((1, 2))) def test_create_lists(aggregate_all): res = aggregate_all(np.array([0, 1, 3, 1, 3]), np.arange(101, 106, dtype=int), func=list) np.testing.assert_array_equal(np.array(res[0]), np.array([101])) assert res[2] == 0 np.testing.assert_array_equal(np.array(res[3]), np.array([103, 105])) def test_item_counting(aggregate_all): group_idx = np.array([0, 1, 2, 3, 3, 3, 3, 4, 5, 5, 5, 6, 5, 4, 3, 8, 8]) a = np.arange(group_idx.size) res = aggregate_all(group_idx, a, func=lambda x: len(x) > 1) np.testing.assert_array_equal(res, np.array([0, 0, 0, 1, 1, 1, 0, 0, 1])) @pytest.mark.parametrize(["func", "fill_value"], [(np.array, None), (np.sum, -1)], ids=["array", "sum"]) def test_fill_value(aggregate_all, func, fill_value): group_idx = np.array([0, 2, 2], dtype=int) res = aggregate_all(group_idx, np.arange(len(group_idx), dtype=int), func=func, fill_value=fill_value) assert res[1] == fill_value @pytest.mark.parametrize("order", ["C", "F"]) def test_array_ordering(aggregate_all, order, size=10): mat = np.zeros((size, size), order=order, dtype=float) mat.flat[:] = np.arange(size * size) assert aggregate_all(np.zeros(size, dtype=int), mat[0, :], order=order)[0] == sum(range(size)) @pytest.mark.parametrize(["ndim", "order"], itertools.product([1, 2, 3], ["C", "F"])) def test_ndim_indexing(aggregate_all, ndim, order, outsize=10): nindices = int(outsize ** ndim) outshape = tuple([outsize] * ndim) group_idx = np.random.randint(0, outsize, size=(ndim, nindices)) a = np.random.random(group_idx.shape[1]) res = aggregate_all(group_idx, a, size=outshape, order=order) if ndim > 1 and order == 'F': # 1d arrays always return False here assert np.isfortran(res) else: assert not np.isfortran(res) assert res.shape == outshape def test_len(aggregate_all, group_size=5): group_idx = np.arange(0, 100, 2, dtype=int).repeat(group_size) a = np.arange(group_idx.size) res = aggregate_all(group_idx, a, func='len') ref = aggregate_all(group_idx, 1, func='sum') if isinstance(res, np.ndarray): assert issubclass(res.dtype.type, np.integer) else: assert isinstance(res[0], int) np.testing.assert_array_equal(res, ref) group_idx = np.arange(0, 100, dtype=int).repeat(group_size) a = np.arange(group_idx.size) res = aggregate_all(group_idx, a, func=len) if isinstance(res, np.ndarray): assert np.all(res == group_size) else: assert all(x == group_size for x in res) def test_nan_len(aggregate_all): group_idx = np.arange(0, 20, 2, dtype=int).repeat(5) a = np.random.random(group_idx.size) a[::4] = np.nan a[::5] = np.nan res = aggregate_all(group_idx, a, func='nanlen') ref = aggregate_all(group_idx[~np.isnan(a)], 1, func='sum') if isinstance(res, np.ndarray): assert issubclass(res.dtype.type, np.integer) else: assert isinstance(res[0], int) np.testing.assert_array_equal(res, ref) @pytest.mark.parametrize("first_last", ["first", "last"]) def test_first_last(aggregate_all, first_last): group_idx = np.arange(0, 100, 2, dtype=int).repeat(5) a = np.arange(group_idx.size) res = aggregate_all(group_idx, a, func=first_last, fill_value=-1) ref = np.zeros(np.max(group_idx) + 1) ref.fill(-1) ref[::2] = np.arange(0 if first_last == 'first' else 4, group_idx.size, 5, dtype=int) np.testing.assert_array_equal(res, ref) @pytest.mark.parametrize(["first_last", "nanoffset"], itertools.product(["nanfirst", "nanlast"], [0, 2, 4])) def test_nan_first_last(aggregate_all, first_last, nanoffset): group_idx = np.arange(0, 100, 2, dtype=int).repeat(5) a = np.arange(group_idx.size, dtype=float) a[nanoffset::5] = np.nan res = aggregate_all(group_idx, a, func=first_last, fill_value=-1) ref = np.zeros(np.max(group_idx) + 1) ref.fill(-1) if first_last == "nanfirst": ref_offset = 1 if nanoffset == 0 else 0 else: ref_offset = 3 if nanoffset == 4 else 4 ref[::2] = np.arange(ref_offset, group_idx.size, 5, dtype=int) np.testing.assert_array_equal(res, ref) @pytest.mark.parametrize(["func", "ddof"], itertools.product(["var", "std"], [0, 1, 2])) def test_ddof(aggregate_all, func, ddof, size=20): group_idx = np.zeros(20, dtype=int) a = np.random.random(group_idx.size) res = aggregate_all(group_idx, a, func, ddof=ddof) ref_func = {'std': np.std, 'var': np.var}.get(func) ref = ref_func(a, ddof=ddof) assert abs(res[0] - ref) < 1e-10 @pytest.mark.parametrize("func", ["sum", "prod", "mean", "var", "std"]) def test_scalar_input(aggregate_all, func): group_idx = np.arange(0, 100, dtype=int).repeat(5) if func not in ("sum", "prod"): pytest.raises((ValueError, NotImplementedError), aggregate_all, group_idx, 1, func=func) else: res = aggregate_all(group_idx, 1, func=func) ref = aggregate_all(group_idx, np.ones_like(group_idx, dtype=int), func=func) np.testing.assert_array_equal(res, ref) @pytest.mark.parametrize("func", ["sum", "prod", "mean", "var", "std" , "all", "any"]) def test_nan_input(aggregate_all, func, groups=100): if aggregate_all.__name__.endswith('pandas'): pytest.skip("pandas automatically skip nan values") group_idx = np.arange(0, groups, dtype=int).repeat(5) a = np.random.random(group_idx.size) a[::2] = np.nan if func in ('all', 'any'): ref = np.ones(groups, dtype=bool) else: ref = np.full(groups, np.nan, dtype=float) res = aggregate_all(group_idx, a, func=func) np.testing.assert_array_equal(res, ref) def test_nan_input_len(aggregate_all, groups=100, group_size=5): if aggregate_all.__name__.endswith('pandas'): pytest.skip("pandas always skips nan values") group_idx = np.arange(0, groups, dtype=int).repeat(group_size) a = np.random.random(len(group_idx)) a[::2] = np.nan ref = np.full(groups, group_size, dtype=int) res = aggregate_all(group_idx, a, func=len) np.testing.assert_array_equal(res, ref) def test_argmin_argmax(aggregate_all): group_idx = np.array([0, 0, 0, 0, 3, 3, 3, 3]) a = np.array([4, 4, 3, 1, 10, 9, 9, 11]) res = aggregate_all(group_idx, a, func="argmax", fill_value=-1) np.testing.assert_array_equal(res, [0, -1, -1, 7]) res = aggregate_all(group_idx, a, func="argmin", fill_value=-1) np.testing.assert_array_equal(res, [3, -1, -1, 5]) def test_mean(aggregate_all): group_idx = np.array([0, 0, 0, 0, 3, 3, 3, 3]) a = np.arange(len(group_idx)) res = aggregate_all(group_idx, a, func="mean") np.testing.assert_array_equal(res, [1.5, 0, 0, 5.5]) def test_cumsum(aggregate_all): group_idx = np.array([4, 3, 3, 4, 4, 1, 1, 1, 7, 8, 7, 4, 3, 3, 1, 1]) a = np.array([3, 4, 1, 3, 9, 9, 6, 7, 7, 0, 8, 2, 1, 8, 9, 8]) ref = np.array([3, 4, 5, 6,15, 9,15,22, 7, 0,15,17, 6,14,31,39]) res = aggregate_all(group_idx, a, func="cumsum") np.testing.assert_array_equal(res, ref) def test_cummax(aggregate_all): group_idx = np.array([4, 3, 3, 4, 4, 1, 1, 1, 7, 8, 7, 4, 3, 3, 1, 1]) a = np.array([3, 4, 1, 3, 9, 9, 6, 7, 7, 0, 8, 2, 1, 8, 9, 8]) ref = np.array([3, 4, 4, 3, 9, 9, 9, 9, 7, 0, 8, 9, 4, 8, 9, 9]) res = aggregate_all(group_idx, a, func="cummax") np.testing.assert_array_equal(res, ref) @pytest.mark.parametrize("order", ["normal", "reverse"]) def test_list_ordering(aggregate_all, order): group_idx = np.repeat(np.arange(5), 4) a = np.arange(group_idx.size) if order == "reverse": a = a[::-1] ref = a[:4] try: res = aggregate_all(group_idx, a, func=list) except NotImplementedError: pytest.xfail("Function not yet implemented") else: np.testing.assert_array_equal(np.array(res[0]), ref) @pytest.mark.parametrize("order", ["normal", "reverse"]) def test_sort(aggregate_all, order): group_idx = np.array([3, 3, 3, 2, 2, 2, 1, 1, 1]) a = np.array([3, 2, 1, 3, 4, 5, 5,10, 1]) ref_normal = np.array([1, 2, 3, 3, 4, 5, 1, 5,10]) ref_reverse = np.array([3, 2, 1, 5, 4, 3,10, 5, 1]) reverse = order == "reverse" ref = ref_reverse if reverse else ref_normal res = aggregate_all(group_idx, a, func="sort", reverse=reverse) np.testing.assert_array_equal(res, ref) numpy-groupies-0.9.13/numpy_groupies/tests/test_indices.py000066400000000000000000000017321372771202100240720ustar00rootroot00000000000000import pytest import numpy as np from . import aggregate_weave, aggregate_numba, _impl_name _implementations = [aggregate_weave, aggregate_numba] @pytest.fixture(params=_implementations, ids=_impl_name) def aggregate_nb_wv(request): if request.param is None: pytest.xfail("Implementation not available") return request.param def test_step_indices_length(aggregate_nb_wv): group_idx = np.array([1, 1, 1, 2, 2, 3, 3, 4, 4, 2, 2], dtype=int) for _ in range(20): np.random.shuffle(group_idx) step_cnt_ref = np.count_nonzero(np.diff(group_idx)) assert aggregate_nb_wv.step_count(group_idx) == step_cnt_ref + 1 assert len(aggregate_nb_wv.step_indices(group_idx)) == step_cnt_ref + 2 def test_step_indices_fields(aggregate_nb_wv): group_idx = np.array([1, 1, 1, 2, 2, 3, 3, 4, 5, 2, 2], dtype=int) steps = aggregate_nb_wv.step_indices(group_idx) np.testing.assert_array_equal(steps, np.array([ 0, 3, 5, 7, 8, 9, 11])) numpy-groupies-0.9.13/numpy_groupies/tests/test_utils.py000066400000000000000000000015001372771202100236050ustar00rootroot00000000000000import numpy as np from ..utils_numpy import check_dtype, unpack def test_check_dtype(): dtype = check_dtype(None, "mean", np.arange(10, dtype=int), 10) assert np.issubdtype(dtype, np.floating) def test_unpack(): """Keep this test, in case unpack might get reimplemented again at some point.""" group_idx = np.arange(10) np.random.shuffle(group_idx) group_idx = np.repeat(group_idx, 3) vals = np.random.randn(np.max(group_idx) + 1) np.testing.assert_array_equal(unpack(group_idx, vals), vals[group_idx]) def test_unpack_long(): group_idx = np.repeat(np.arange(10000), 20) a = np.arange(group_idx.size, dtype=int) a = np.random.randn(np.max(group_idx) + 1) vals = np.random.randn(np.max(group_idx) + 1) np.testing.assert_array_equal(unpack(group_idx, vals), vals[group_idx]) numpy-groupies-0.9.13/numpy_groupies/utils.py000066400000000000000000000130031372771202100214050ustar00rootroot00000000000000"""Common helpers without certain dependencies.""" aggregate_common_doc = """ See readme file at https://github.com/ml31415/numpy-groupies for a full description. Below we reproduce the "Full description of inputs" section from that readme, note that the text below makes references to other portions of the readme that are not shown here. group_idx: this is an array of non-negative integers, to be used as the "labels" with which to group the values in ``a``. Although we have so far assumed that ``group_idx`` is one-dimesnaional, and the same length as ``a``, it can in fact be two-dimensional (or some form of nested sequences that can be converted to 2D). When ``group_idx`` is 2D, the size of the 0th dimension corresponds to the number of dimesnions in the output, i.e. ``group_idx[i,j]`` gives the index into the ith dimension in the output for ``a[j]``. Note that ``a`` should still be 1D (or scalar), with length matching ``group_idx.shape[1]``. a: this is the array of values to be aggregated. See above for a simple demonstration of what this means. ``a`` will normally be a one-dimensional array, however it can also be a scalar in some cases. func: default='sum' the function to use for aggregation. See the section above for details. Note that the simplest way to specify the function is using a string (e.g. ``func='max'``) however a number of aliases are also defined (e.g. you can use the ``func=np.max``, or even ``func=max``, where ``max`` is the builtin function). To check the available aliases see ``utils.py``. size: default=None the shape of the output array. If ``None``, the maximum value in ``group_idx`` will set the size of the output. Note that for multidimensional output you need to list the size of each dimension here, or give ``None``. fill_value: default=0 in the example above, group 2 does not have any data, so requires some kind of filling value - in this case the default of ``0`` is used. If you had set ``fill_value=nan`` or something else, that value would appear instead of ``0`` for the 2 element in the output. Note that there are some subtle interactions between what is permitted for ``fill_value`` and the input/output ``dtype`` - exceptions should be raised in most cases to alert the programmer if issue arrise. order: default='C' this is relevant only for multimensional output. It controls the layout of the output array in memory, can be ``'F'`` for fortran-style. dtype: default=None the ``dtype`` of the output. By default something sensible is chosen based on the input, aggregation function, and ``fill_value``. ddof: default=0 passed through into calculations of variance and standard deviation (see above). """ funcs_common = 'first last len mean var std allnan anynan max min argmax argmin cumsum cumprod cummax cummin'.split() funcs_no_separate_nan = frozenset(['sort', 'rsort', 'array', 'allnan', 'anynan']) _alias_str = { 'or': 'any', 'and': 'all', 'add': 'sum', 'count': 'len', 'plus': 'sum', 'multiply': 'prod', 'product': 'prod', 'times': 'prod', 'amax': 'max', 'maximum': 'max', 'amin': 'min', 'minimum': 'min', 'split': 'array', 'splice': 'array', 'sorted': 'sort', 'asort': 'sort', 'asorted': 'sort', 'rsorted': 'sort', 'dsort': 'sort', 'dsorted': 'rsort', } _alias_builtin = { all: 'all', any: 'any', len: 'len', max: 'max', min: 'min', sum: 'sum', sorted: 'sort', slice: 'array', list: 'array', } def get_aliasing(*extra): """The assembles the dict mapping strings and functions to the list of supported function names: e.g. alias['add'] = 'sum' and alias[sorted] = 'sort' This funciton should only be called during import. """ alias = dict((k, k) for k in funcs_common) alias.update(_alias_str) alias.update((fn, fn) for fn in _alias_builtin.values()) alias.update(_alias_builtin) for d in extra: alias.update(d) alias.update((k, k) for k in set(alias.values())) # Treat nan-functions as firstclass member and add them directly for key in set(alias.values()): if key not in funcs_no_separate_nan: key = 'nan' + key alias[key] = key return alias aliasing = get_aliasing() def get_func(func, aliasing, implementations): """ Return the key of a found implementation or the func itself """ try: func_str = aliasing[func] except KeyError: if callable(func): return func else: if func_str in implementations: return func_str if func_str.startswith('nan') and \ func_str[3:] in funcs_no_separate_nan: raise ValueError("%s does not have a nan-version".format(func_str[3:])) else: raise NotImplementedError("No such function available") raise ValueError("func %s is neither a valid function string nor a " "callable object".format(func)) def check_boolean(x): if x not in (0, 1): raise ValueError("Value not boolean") try: basestring # Attempt to evaluate basestring def isstr(s): return isinstance(s, basestring) except NameError: # Probably Python 3.x def isstr(s): return isinstance(s, str) numpy-groupies-0.9.13/numpy_groupies/utils_numpy.py000066400000000000000000000342561372771202100226520ustar00rootroot00000000000000"""Common helper functions for typing and general numpy tools.""" import numpy as np from .utils import get_aliasing, check_boolean _alias_numpy = { np.add: 'sum', np.sum: 'sum', np.any: 'any', np.all: 'all', np.multiply: 'prod', np.prod: 'prod', np.amin: 'min', np.min: 'min', np.minimum: 'min', np.amax: 'max', np.max: 'max', np.maximum: 'max', np.argmax: 'argmax', np.argmin: 'argmin', np.mean: 'mean', np.std: 'std', np.var: 'var', np.array: 'array', np.asarray: 'array', np.sort: 'sort', np.nansum: 'nansum', np.nanprod: 'nanprod', np.nanmean: 'nanmean', np.nanvar: 'nanvar', np.nanmax: 'nanmax', np.nanmin: 'nanmin', np.nanstd: 'nanstd', np.nanargmax: 'nanargmax', np.nanargmin: 'nanargmin', np.cumsum: 'cumsum', np.cumprod: 'cumprod', } aliasing = get_aliasing(_alias_numpy) _next_int_dtype = dict( bool=np.int8, uint8=np.int16, int8=np.int16, uint16=np.int32, int16=np.int32, uint32=np.int64, int32=np.int64 ) _next_float_dtype = dict( float16=np.float32, float32=np.float64, float64=np.complex64, complex64=np.complex128 ) def minimum_dtype(x, dtype=np.bool_): """returns the "most basic" dtype which represents `x` properly, which provides at least the same value range as the specified dtype.""" def check_type(x, dtype): try: converted = dtype.type(x) except (ValueError, OverflowError): return False # False if some overflow has happened return converted == x or np.isnan(x) def type_loop(x, dtype, dtype_dict, default=None): while True: try: dtype = np.dtype(dtype_dict[dtype.name]) if check_type(x, dtype): return np.dtype(dtype) except KeyError: if default is not None: return np.dtype(default) raise ValueError("Can not determine dtype of %r" % x) dtype = np.dtype(dtype) if check_type(x, dtype): return dtype if np.issubdtype(dtype, np.inexact): return type_loop(x, dtype, _next_float_dtype) else: return type_loop(x, dtype, _next_int_dtype, default=np.float32) def minimum_dtype_scalar(x, dtype, a): if dtype is None: dtype = np.dtype(type(a)) if isinstance(a, (int, float))\ else a.dtype return minimum_dtype(x, dtype) _forced_types = { 'array': np.object, 'all': np.bool_, 'any': np.bool_, 'nanall': np.bool_, 'nanany': np.bool_, 'len': np.int64, 'nanlen': np.int64, 'allnan': np.bool_, 'anynan': np.bool_, 'argmax': np.int64, 'argmin': np.int64, } _forced_float_types = {'mean', 'var', 'std', 'nanmean', 'nanvar', 'nanstd'} _forced_same_type = {'min', 'max', 'first', 'last', 'nanmin', 'nanmax', 'nanfirst', 'nanlast'} def check_dtype(dtype, func_str, a, n): if np.isscalar(a) or not a.shape: if func_str not in ("sum", "prod", "len"): raise ValueError("scalar inputs are supported only for 'sum', " "'prod' and 'len'") a_dtype = np.dtype(type(a)) else: a_dtype = a.dtype if dtype is not None: # dtype set by the user # Careful here: np.bool != np.bool_ ! if np.issubdtype(dtype, np.bool_) and \ not('all' in func_str or 'any' in func_str): raise TypeError("function %s requires a more complex datatype " "than bool" % func_str) if not np.issubdtype(dtype, np.integer) and func_str in ('len', 'nanlen'): raise TypeError("function %s requires an integer datatype" % func_str) # TODO: Maybe have some more checks here return np.dtype(dtype) else: try: return np.dtype(_forced_types[func_str]) except KeyError: if func_str in _forced_float_types: if np.issubdtype(a_dtype, np.floating): return a_dtype else: return np.dtype(np.float64) else: if func_str == 'sum': # Try to guess the minimally required int size if np.issubdtype(a_dtype, np.int64): # It's not getting bigger anymore # TODO: strictly speaking it might need float return np.dtype(np.int64) elif np.issubdtype(a_dtype, np.integer): maxval = np.iinfo(a_dtype).max * n return minimum_dtype(maxval, a_dtype) elif np.issubdtype(a_dtype, np.bool_): return minimum_dtype(n, a_dtype) else: # floating, inexact, whatever return a_dtype elif func_str in _forced_same_type: return a_dtype else: if isinstance(a_dtype, np.integer): return np.dtype(np.int64) else: return a_dtype def check_fill_value(fill_value, dtype, func=None): if func in ('all', 'any', 'allnan', 'anynan'): check_boolean(fill_value) else: try: return dtype.type(fill_value) except ValueError: raise ValueError("fill_value must be convertible into %s" % dtype.type.__name__) def check_group_idx(group_idx, a=None, check_min=True): if a is not None and group_idx.size != a.size: raise ValueError("The size of group_idx must be the same as " "a.size") if not issubclass(group_idx.dtype.type, np.integer): raise TypeError("group_idx must be of integer type") if check_min and np.min(group_idx) < 0: raise ValueError("group_idx contains negative indices") def input_validation(group_idx, a, size=None, order='C', axis=None, ravel_group_idx=True, check_bounds=True): """ Do some fairly extensive checking of group_idx and a, trying to give the user as much help as possible with what is wrong. Also, convert ndim-indexing to 1d indexing. """ if not isinstance(a, (int, float, complex)): a = np.asanyarray(a) group_idx = np.asanyarray(group_idx) if not np.issubdtype(group_idx.dtype, np.integer): raise TypeError("group_idx must be of integer type") # This check works for multidimensional indexing as well if check_bounds and np.any(group_idx < 0): raise ValueError("negative indices not supported") ndim_idx = np.ndim(group_idx) ndim_a = np.ndim(a) # Deal with the axis arg: if present, then turn 1d indexing into # multi-dimensional indexing along the specified axis. if axis is None: if ndim_a > 1: raise ValueError("a must be scalar or 1 dimensional, use .ravel to" " flatten. Alternatively specify axis.") elif axis >= ndim_a or axis < -ndim_a: raise ValueError("axis arg too large for np.ndim(a)") else: axis = axis if axis >= 0 else ndim_a + axis # negative indexing if ndim_idx > 1: # TODO: we could support a sequence of axis values for multiple # dimensions of group_idx. raise NotImplementedError("only 1d indexing currently" "supported with axis arg.") elif a.shape[axis] != len(group_idx): raise ValueError("a.shape[axis] doesn't match length of group_idx.") elif size is not None and not np.isscalar(size): raise NotImplementedError("when using axis arg, size must be" "None or scalar.") else: # Create the broadcast-ready multidimensional indexing. # Note the user could do this themselves, so this is # very much just a convenience. size_in = int(np.max(group_idx)) + 1 if size is None else size group_idx_in = group_idx group_idx = [] size = [] for ii, s in enumerate(a.shape): ii_idx = group_idx_in if ii == axis else np.arange(s) ii_shape = [1] * ndim_a ii_shape[ii] = s group_idx.append(ii_idx.reshape(ii_shape)) size.append(size_in if ii == axis else s) # Use the indexing, and return. It's a bit simpler than # using trying to keep all the logic below happy group_idx = np.ravel_multi_index(group_idx, size, order=order, mode='raise') flat_size = np.prod(size) ndim_idx = ndim_a return group_idx.ravel(), a.ravel(), flat_size, ndim_idx, size if ndim_idx == 1: if size is None: size = int(np.max(group_idx)) + 1 else: if not np.isscalar(size): raise ValueError("output size must be scalar or None") if check_bounds and np.any(group_idx > size - 1): raise ValueError("one or more indices are too large for " "size %d" % size) flat_size = size else: if size is None: size = int(np.max(group_idx, axis=1)) + 1 elif np.isscalar(size): raise ValueError("output size must be of length %d" % len(group_idx)) elif len(size) != len(group_idx): raise ValueError("%d sizes given, but %d output dimensions " "specified in index" % (len(size), len(group_idx))) if ravel_group_idx: group_idx = np.ravel_multi_index(group_idx, size, order=order, mode='raise') flat_size = np.prod(size) if not (np.ndim(a) == 0 or len(a) == group_idx.size): raise ValueError("group_idx and a must be of the same length, or a" " can be scalar") return group_idx, a, flat_size, ndim_idx, size ### General tools ### def unpack(group_idx, ret): """ Take an aggregate packed array and uncompress it to the size of group_idx. This is equivalent to ret[group_idx]. """ return ret[group_idx] def allnan(x): return np.all(np.isnan(x)) def anynan(x): return np.any(np.isnan(x)) def nanfirst(x): return x[~np.isnan(x)][0] def nanlast(x): return x[~np.isnan(x)][-1] def multi_arange(n): """By example: # 0 1 2 3 4 5 6 7 8 n = [0, 0, 3, 0, 0, 2, 0, 2, 1] res = [0, 1, 2, 0, 1, 0, 1, 0] That is it is equivalent to something like this : hstack((arange(n_i) for n_i in n)) This version seems quite a bit faster, at least for some possible inputs, and at any rate it encapsulates a task in a function. """ if n.ndim != 1: raise ValueError("n is supposed to be 1d array.") n_mask = n.astype(bool) n_cumsum = np.cumsum(n) ret = np.ones(n_cumsum[-1] + 1, dtype=int) ret[n_cumsum[n_mask]] -= n[n_mask] ret[0] -= 1 return np.cumsum(ret)[:-1] def label_contiguous_1d(X): """ WARNING: API for this function is not liable to change!!! By example: X = [F T T F F T F F F T T T] result = [0 1 1 0 0 2 0 0 0 3 3 3] Or: X = [0 3 3 0 0 5 5 5 1 1 0 2] result = [0 1 1 0 0 2 2 2 3 3 0 4] The ``0`` or ``False`` elements of ``X`` are labeled as ``0`` in the output. If ``X`` is a boolean array, each contiguous block of ``True`` is given an integer label, if ``X`` is not boolean, then each contiguous block of identical values is given an integer label. Integer labels are 1, 2, 3,..... (i.e. start a 1 and increase by 1 for each block with no skipped numbers.) """ if X.ndim != 1: raise ValueError("this is for 1d masks only.") is_start = np.empty(len(X), dtype=bool) is_start[0] = X[0] # True if X[0] is True or non-zero if X.dtype.kind == 'b': is_start[1:] = ~X[:-1] & X[1:] M = X else: M = X.astype(bool) is_start[1:] = X[:-1] != X[1:] is_start[~M] = False L = np.cumsum(is_start) L[~M] = 0 return L def relabel_groups_unique(group_idx): """ See also ``relabel_groups_masked``. keep_group: [0 3 3 3 0 2 5 2 0 1 1 0 3 5 5] ret: [0 3 3 3 0 2 4 2 0 1 1 0 3 4 4] Description of above: unique groups in input was ``1,2,3,5``, i.e. ``4`` was missing, so group 5 was relabled to be ``4``. Relabeling maintains order, just "compressing" the higher numbers to fill gaps. """ keep_group = np.zeros(np.max(group_idx) + 1, dtype=bool) keep_group[0] = True keep_group[group_idx] = True return relabel_groups_masked(group_idx, keep_group) def relabel_groups_masked(group_idx, keep_group): """ group_idx: [0 3 3 3 0 2 5 2 0 1 1 0 3 5 5] 0 1 2 3 4 5 keep_group: [0 1 0 1 1 1] ret: [0 2 2 2 0 0 4 0 0 1 1 0 2 4 4] Description of above in words: remove group 2, and relabel group 3,4, and 5 to be 2, 3 and 4 respecitvely, in order to fill the gap. Note that group 4 was never used in the input group_idx, but the user supplied mask said to keep group 4, so group 5 is only moved up by one place to fill the gap created by removing group 2. That is, the mask describes which groups to remove, the remaining groups are relabled to remove the gaps created by the falsy elements in ``keep_group``. Note that ``keep_group[0]`` has no particular meaning because it refers to the zero group which cannot be "removed". ``keep_group`` should be bool and ``group_idx`` int. Values in ``group_idx`` can be any order, and """ keep_group = keep_group.astype(bool, copy=not keep_group[0]) if not keep_group[0]: # ensuring keep_group[0] is True makes life easier keep_group[0] = True relabel = np.zeros(keep_group.size, dtype=group_idx.dtype) relabel[keep_group] = np.arange(np.count_nonzero(keep_group)) return relabel[group_idx] numpy-groupies-0.9.13/setup.cfg000066400000000000000000000003241372771202100164310ustar00rootroot00000000000000[metadata] description-file = README.md [aliases] test=pytest [versioneer] VCS = git style = pep440 versionfile_source = numpy_groupies/_version.py versionfile_build = numpy_groupies/_version.py tag_prefix = v numpy-groupies-0.9.13/setup.py000066400000000000000000000065551372771202100163360ustar00rootroot00000000000000#!/usr/bin/env python import os import versioneer from setuptools import setup from distutils import log from distutils.command.clean import clean from distutils.dir_util import remove_tree base_path = os.path.dirname(os.path.abspath(__file__)) long_description = """ This package consists of a couple of optimised tools for doing things that can roughly be considered "group-indexing operations". The most prominent tool is `aggregate`. `aggregate` takes an array of values, and an array giving the group number for each of those values. It then returns the sum (or mean, or std, or any, ...etc.) of the values in each group. You have probably come across this idea before, using `matlab` accumarray, `pandas` groupby, or generally MapReduce algorithms and histograms. There are different implementations of `aggregate` provided, based on plain `numpy`, `numba` and `weave`. Performance is a main concern, and so far we comfortably beat similar implementations in other packages (check the benchmarks). """ class NumpyGroupiesClean(clean): """Custom clean command to tidy up the project root.""" def run(self): clean.run(self) for folder in ('build', 'numpy_groupies.egg-info'): path = os.path.join(base_path, folder) if os.path.isdir(path): remove_tree(path, dry_run=self.dry_run) if not self.dry_run: self._rm_walk() def _rm_walk(self): for path, dirs, files in os.walk(base_path): if any(p.startswith('.') for p in path.split(os.path.sep)): # Skip hidden directories like the git folder right away continue if path.endswith('__pycache__'): remove_tree(path, dry_run=self.dry_run) else: for fname in files: if fname.endswith('.pyc') or fname.endswith('.so'): fpath = os.path.join(path, fname) os.remove(fpath) log.info("removing '%s'", fpath) setup(name='numpy_groupies', version=versioneer.get_version(), author="@ml31415 and @d1manson", author_email="npgroupies@occam.com.ua", license='BSD', description="Optimised tools for group-indexing operations: aggregated sum and more.", long_description=long_description, url="https://github.com/ml31415/numpy-groupies", download_url="https://github.com/ml31415/numpy-groupies/archive/master.zip", keywords=[ "accumarray", "aggregate", "groupby", "grouping", "indexing"], packages=['numpy_groupies'], install_requires=[], setup_requires=['pytest-runner'], tests_require=['pytest', 'numpy', 'numba'], classifiers=['Development Status :: 4 - Beta', 'Intended Audience :: Science/Research', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], cmdclass=dict(clean=NumpyGroupiesClean, **versioneer.get_cmdclass()), ) numpy-groupies-0.9.13/versioneer.py000066400000000000000000002060031372771202100173450ustar00rootroot00000000000000 # Version: 0.18 """The Versioneer - like a rocketeer, but for versions. The Versioneer ============== * like a rocketeer, but for versions! * https://github.com/warner/python-versioneer * Brian Warner * License: Public Domain * Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy * [![Latest Version] (https://pypip.in/version/versioneer/badge.svg?style=flat) ](https://pypi.python.org/pypi/versioneer/) * [![Build Status] (https://travis-ci.org/warner/python-versioneer.png?branch=master) ](https://travis-ci.org/warner/python-versioneer) This is a tool for managing a recorded version number in distutils-based python projects. The goal is to remove the tedious and error-prone "update the embedded version string" step from your release process. Making a new release should be as easy as recording a new tag in your version-control system, and maybe making new tarballs. ## Quick Install * `pip install versioneer` to somewhere to your $PATH * add a `[versioneer]` section to your setup.cfg (see below) * run `versioneer install` in your source tree, commit the results ## Version Identifiers Source trees come from a variety of places: * a version-control system checkout (mostly used by developers) * a nightly tarball, produced by build automation * a snapshot tarball, produced by a web-based VCS browser, like github's "tarball from tag" feature * a release tarball, produced by "setup.py sdist", distributed through PyPI Within each source tree, the version identifier (either a string or a number, this tool is format-agnostic) can come from a variety of places: * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows about recent "tags" and an absolute revision-id * the name of the directory into which the tarball was unpacked * an expanded VCS keyword ($Id$, etc) * a `_version.py` created by some earlier build step For released software, the version identifier is closely related to a VCS tag. Some projects use tag names that include more than just the version string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool needs to strip the tag prefix to extract the version identifier. For unreleased software (between tags), the version identifier should provide enough information to help developers recreate the same tree, while also giving them an idea of roughly how old the tree is (after version 1.2, before version 1.3). Many VCS systems can report a description that captures this, for example `git describe --tags --dirty --always` reports things like "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has uncommitted changes. The version identifier is used for multiple purposes: * to allow the module to self-identify its version: `myproject.__version__` * to choose a name and prefix for a 'setup.py sdist' tarball ## Theory of Operation Versioneer works by adding a special `_version.py` file into your source tree, where your `__init__.py` can import it. This `_version.py` knows how to dynamically ask the VCS tool for version information at import time. `_version.py` also contains `$Revision$` markers, and the installation process marks `_version.py` to have this marker rewritten with a tag name during the `git archive` command. As a result, generated tarballs will contain enough information to get the proper version. To allow `setup.py` to compute a version too, a `versioneer.py` is added to the top level of your source tree, next to `setup.py` and the `setup.cfg` that configures it. This overrides several distutils/setuptools commands to compute the version when invoked, and changes `setup.py build` and `setup.py sdist` to replace `_version.py` with a small static file that contains just the generated version data. ## Installation See [INSTALL.md](./INSTALL.md) for detailed installation instructions. ## Version-String Flavors Code which uses Versioneer can learn about its version string at runtime by importing `_version` from your main `__init__.py` file and running the `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can import the top-level `versioneer.py` and run `get_versions()`. Both functions return a dictionary with different flavors of version information: * `['version']`: A condensed version string, rendered using the selected style. This is the most commonly used value for the project's version string. The default "pep440" style yields strings like `0.11`, `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section below for alternative styles. * `['full-revisionid']`: detailed revision identifier. For Git, this is the full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". * `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the commit date in ISO 8601 format. This will be None if the date is not available. * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that this is only accurate if run in a VCS checkout, otherwise it is likely to be False or None * `['error']`: if the version string could not be computed, this will be set to a string describing the problem, otherwise it will be None. It may be useful to throw an exception in setup.py if this is set, to avoid e.g. creating tarballs with a version string of "unknown". Some variants are more useful than others. Including `full-revisionid` in a bug report should allow developers to reconstruct the exact code being tested (or indicate the presence of local changes that should be shared with the developers). `version` is suitable for display in an "about" box or a CLI `--version` output: it can be easily compared against release notes and lists of bugs fixed in various releases. The installer adds the following text to your `__init__.py` to place a basic version in `YOURPROJECT.__version__`: from ._version import get_versions __version__ = get_versions()['version'] del get_versions ## Styles The setup.cfg `style=` configuration controls how the VCS information is rendered into a version string. The default style, "pep440", produces a PEP440-compliant string, equal to the un-prefixed tag name for actual releases, and containing an additional "local version" section with more detail for in-between builds. For Git, this is TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and that this commit is two revisions ("+2") beyond the "0.11" tag. For released software (exactly equal to a known tag), the identifier will only contain the stripped tag, e.g. "0.11". Other styles are available. See [details.md](details.md) in the Versioneer source tree for descriptions. ## Debugging Versioneer tries to avoid fatal errors: if something goes wrong, it will tend to return a version of "0+unknown". To investigate the problem, run `setup.py version`, which will run the version-lookup code in a verbose mode, and will display the full contents of `get_versions()` (including the `error` string, which may help identify what went wrong). ## Known Limitations Some situations are known to cause problems for Versioneer. This details the most significant ones. More can be found on Github [issues page](https://github.com/warner/python-versioneer/issues). ### Subprojects Versioneer has limited support for source trees in which `setup.py` is not in the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are two common reasons why `setup.py` might not be in the root: * Source trees which contain multiple subprojects, such as [Buildbot](https://github.com/buildbot/buildbot), which contains both "master" and "slave" subprojects, each with their own `setup.py`, `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI distributions (and upload multiple independently-installable tarballs). * Source trees whose main purpose is to contain a C library, but which also provide bindings to Python (and perhaps other langauges) in subdirectories. Versioneer will look for `.git` in parent directories, and most operations should get the right version string. However `pip` and `setuptools` have bugs and implementation details which frequently cause `pip install .` from a subproject directory to fail to find a correct version string (so it usually defaults to `0+unknown`). `pip install --editable .` should work correctly. `setup.py install` might work too. Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in some later version. [Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking this issue. The discussion in [PR #61](https://github.com/warner/python-versioneer/pull/61) describes the issue from the Versioneer side in more detail. [pip PR#3176](https://github.com/pypa/pip/pull/3176) and [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve pip to let Versioneer work correctly. Versioneer-0.16 and earlier only looked for a `.git` directory next to the `setup.cfg`, so subprojects were completely unsupported with those releases. ### Editable installs with setuptools <= 18.5 `setup.py develop` and `pip install --editable .` allow you to install a project into a virtualenv once, then continue editing the source code (and test) without re-installing after every change. "Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a convenient way to specify executable scripts that should be installed along with the python package. These both work as expected when using modern setuptools. When using setuptools-18.5 or earlier, however, certain operations will cause `pkg_resources.DistributionNotFound` errors when running the entrypoint script, which must be resolved by re-installing the package. This happens when the install happens with one version, then the egg_info data is regenerated while a different version is checked out. Many setup.py commands cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into a different virtualenv), so this can be surprising. [Bug #83](https://github.com/warner/python-versioneer/issues/83) describes this one, but upgrading to a newer version of setuptools should probably resolve it. ### Unicode version strings While Versioneer works (and is continually tested) with both Python 2 and Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. Newer releases probably generate unicode version strings on py2. It's not clear that this is wrong, but it may be surprising for applications when then write these strings to a network connection or include them in bytes-oriented APIs like cryptographic checksums. [Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates this question. ## Updating Versioneer To upgrade your project to a new release of Versioneer, do the following: * install the new Versioneer (`pip install -U versioneer` or equivalent) * edit `setup.cfg`, if necessary, to include any new configuration settings indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. * re-run `versioneer install` in your source tree, to replace `SRC/_version.py` * commit any changed files ## Future Directions This tool is designed to make it easily extended to other version-control systems: all VCS-specific components are in separate directories like src/git/ . The top-level `versioneer.py` script is assembled from these components by running make-versioneer.py . In the future, make-versioneer.py will take a VCS name as an argument, and will construct a version of `versioneer.py` that is specific to the given VCS. It might also take the configuration arguments that are currently provided manually during installation by editing setup.py . Alternatively, it might go the other direction and include code from all supported VCS systems, reducing the number of intermediate scripts. ## License To make Versioneer easier to embed, all its code is dedicated to the public domain. The `_version.py` that it creates is also in the public domain. Specifically, both are released under the Creative Commons "Public Domain Dedication" license (CC0-1.0), as described in https://creativecommons.org/publicdomain/zero/1.0/ . """ from __future__ import print_function try: import configparser except ImportError: import ConfigParser as configparser import errno import json import os import re import subprocess import sys class VersioneerConfig: """Container for Versioneer configuration parameters.""" def get_root(): """Get the project root directory. We require that all commands are run from the project root, i.e. the directory that contains setup.py, setup.cfg, and versioneer.py . """ root = os.path.realpath(os.path.abspath(os.getcwd())) setup_py = os.path.join(root, "setup.py") versioneer_py = os.path.join(root, "versioneer.py") if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): # allow 'python path/to/setup.py COMMAND' root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) setup_py = os.path.join(root, "setup.py") versioneer_py = os.path.join(root, "versioneer.py") if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): err = ("Versioneer was unable to run the project root directory. " "Versioneer requires setup.py to be executed from " "its immediate directory (like 'python setup.py COMMAND'), " "or in a way that lets it use sys.argv[0] to find the root " "(like 'python path/to/setup.py COMMAND').") raise VersioneerBadRootError(err) try: # Certain runtime workflows (setup.py install/develop in a setuptools # tree) execute all dependencies in a single python process, so # "versioneer" may be imported multiple times, and python's shared # module-import table will cache the first one. So we can't use # os.path.dirname(__file__), as that will find whichever # versioneer.py was first imported, even in later projects. me = os.path.realpath(os.path.abspath(__file__)) me_dir = os.path.normcase(os.path.splitext(me)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) if me_dir != vsr_dir: print("Warning: build in %s is using versioneer.py from %s" % (os.path.dirname(me), versioneer_py)) except NameError: pass return root def get_config_from_root(root): """Read the project setup.cfg file to determine Versioneer config.""" # This might raise EnvironmentError (if setup.cfg is missing), or # configparser.NoSectionError (if it lacks a [versioneer] section), or # configparser.NoOptionError (if it lacks "VCS="). See the docstring at # the top of versioneer.py for instructions on writing your setup.cfg . setup_cfg = os.path.join(root, "setup.cfg") parser = configparser.SafeConfigParser() with open(setup_cfg, "r") as f: parser.readfp(f) VCS = parser.get("versioneer", "VCS") # mandatory def get(parser, name): if parser.has_option("versioneer", name): return parser.get("versioneer", name) return None cfg = VersioneerConfig() cfg.VCS = VCS cfg.style = get(parser, "style") or "" cfg.versionfile_source = get(parser, "versionfile_source") cfg.versionfile_build = get(parser, "versionfile_build") cfg.tag_prefix = get(parser, "tag_prefix") if cfg.tag_prefix in ("''", '""'): cfg.tag_prefix = "" cfg.parentdir_prefix = get(parser, "parentdir_prefix") cfg.verbose = get(parser, "verbose") return cfg class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" # these dictionaries contain VCS-specific tools LONG_VERSION_PY = {} HANDLERS = {} def register_vcs_handler(vcs, method): # decorator """Decorator to mark a method as the handler for a particular VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None for c in commands: try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git p = subprocess.Popen([c] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None)) break except EnvironmentError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue if verbose: print("unable to run %s" % dispcmd) print(e) return None, None else: if verbose: print("unable to find command, tried %s" % (commands,)) return None, None stdout = p.communicate()[0].strip() if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) return None, p.returncode return stdout, p.returncode LONG_VERSION_PY['git'] = ''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. # This file is released into the public domain. Generated by # versioneer-0.18 (https://github.com/warner/python-versioneer) """Git implementation of _version.py.""" import errno import os import re import subprocess import sys def get_keywords(): """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must # each be defined on a line of their own. _version.py will just call # get_keywords(). git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} return keywords class VersioneerConfig: """Container for Versioneer configuration parameters.""" def get_config(): """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py cfg = VersioneerConfig() cfg.VCS = "git" cfg.style = "%(STYLE)s" cfg.tag_prefix = "%(TAG_PREFIX)s" cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" cfg.verbose = False return cfg class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" LONG_VERSION_PY = {} HANDLERS = {} def register_vcs_handler(vcs, method): # decorator """Decorator to mark a method as the handler for a particular VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None for c in commands: try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git p = subprocess.Popen([c] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None)) break except EnvironmentError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue if verbose: print("unable to run %%s" %% dispcmd) print(e) return None, None else: if verbose: print("unable to find command, tried %%s" %% (commands,)) return None, None stdout = p.communicate()[0].strip() if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: print("unable to run %%s (error)" %% dispcmd) print("stdout was %%s" %% stdout) return None, p.returncode return stdout, p.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both the project name and a version string. We will also support searching up two directory levels for an appropriately named parent directory """ rootdirs = [] for i in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} else: rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: print("Tried directories %%s but none started with prefix %%s" %% (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. keywords = {} try: f = open(versionfile_abs, "r") for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) if line.strip().startswith("git_date ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["date"] = mo.group(1) f.close() except EnvironmentError: pass return keywords @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if not keywords: raise NotThisMethod("no keywords at all, weird") date = keywords.get("date") if date is not None: # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because # it's been around since git-1.5.3, and it's too difficult to # discover which version we're using, or to work around using an # older one. date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %%d # expansion behaves like git log --decorate=short and strips out the # refs/heads/ and refs/tags/ prefixes that would let us distinguish # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". tags = set([r for r in refs if re.search(r'\d', r)]) if verbose: print("discarding '%%s', no digits" %% ",".join(refs - tags)) if verbose: print("likely tags: %%s" %% ",".join(sorted(tags))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] if verbose: print("picking %%s" %% r) return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %%s not under git control" %% root) raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", "%%s*" %% tag_prefix], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() pieces = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out # look for -dirty suffix dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: git_describe = git_describe[:git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparseable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%%s'" %% describe_out) return pieces # tag full_tag = mo.group(1) if not full_tag.startswith(tag_prefix): if verbose: fmt = "tag '%%s' doesn't start with prefix '%%s'" print(fmt %% (full_tag, tag_prefix)) pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" %% (full_tag, tag_prefix)) return pieces pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) # commit: short hex revision ID pieces["short"] = mo.group(3) else: # HEX: no tags pieces["closest-tag"] = None count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" def render_pep440(pieces): """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty Exceptions: 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += plus_or_dot(pieces) rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_pre(pieces): """TAG[.post.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post.devDISTANCE """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += ".post.dev%%d" %% pieces["distance"] else: # exception #1 rendered = "0.post.dev%%d" %% pieces["distance"] return rendered def render_pep440_post(pieces): """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards (a dirty tree will appear "older" than the corresponding clean one), but you shouldn't be releasing software with -dirty anyways. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%%s" %% pieces["short"] else: # exception #1 rendered = "0.post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += "+g%%s" %% pieces["short"] return rendered def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. Eexceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" else: # exception #1 rendered = "0.post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" return rendered def render_git_describe(pieces): """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render_git_describe_long(pieces): """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. The distance/hash is unconditional. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, "error": pieces["error"], "date": None} if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: raise ValueError("unknown style '%%s'" %% style) return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date")} def get_versions(): """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which # case we can only use expanded keywords. cfg = get_config() verbose = cfg.verbose try: return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass try: root = os.path.realpath(__file__) # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. for i in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to find root of source tree", "date": None} try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) return render(pieces, cfg.style) except NotThisMethod: pass try: if cfg.parentdir_prefix: return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) except NotThisMethod: pass return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to compute version", "date": None} ''' @register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. keywords = {} try: f = open(versionfile_abs, "r") for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) if line.strip().startswith("git_date ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["date"] = mo.group(1) f.close() except EnvironmentError: pass return keywords @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if not keywords: raise NotThisMethod("no keywords at all, weird") date = keywords.get("date") if date is not None: # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because # it's been around since git-1.5.3, and it's too difficult to # discover which version we're using, or to work around using an # older one. date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d # expansion behaves like git log --decorate=short and strips out the # refs/heads/ and refs/tags/ prefixes that would let us distinguish # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". tags = set([r for r in refs if re.search(r'\d', r)]) if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: print("likely tags: %s" % ",".join(sorted(tags))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] if verbose: print("picking %s" % r) return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %s not under git control" % root) raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", "%s*" % tag_prefix], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() pieces = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out # look for -dirty suffix dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: git_describe = git_describe[:git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparseable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%s'" % describe_out) return pieces # tag full_tag = mo.group(1) if not full_tag.startswith(tag_prefix): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix)) return pieces pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) # commit: short hex revision ID pieces["short"] = mo.group(3) else: # HEX: no tags pieces["closest-tag"] = None count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces def do_vcs_install(manifest_in, versionfile_source, ipy): """Git-specific installation logic for Versioneer. For Git, this means creating/changing .gitattributes to mark _version.py for export-subst keyword substitution. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] files = [manifest_in, versionfile_source] if ipy: files.append(ipy) try: me = __file__ if me.endswith(".pyc") or me.endswith(".pyo"): me = os.path.splitext(me)[0] + ".py" versioneer_file = os.path.relpath(me) except NameError: versioneer_file = "versioneer.py" files.append(versioneer_file) present = False try: f = open(".gitattributes", "r") for line in f.readlines(): if line.strip().startswith(versionfile_source): if "export-subst" in line.strip().split()[1:]: present = True f.close() except EnvironmentError: pass if not present: f = open(".gitattributes", "a+") f.write("%s export-subst\n" % versionfile_source) f.close() files.append(".gitattributes") run_command(GITS, ["add", "--"] + files) def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both the project name and a version string. We will also support searching up two directory levels for an appropriately named parent directory """ rootdirs = [] for i in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} else: rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") SHORT_VERSION_PY = """ # This file was generated by 'versioneer.py' (0.18) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. import json version_json = ''' %s ''' # END VERSION_JSON def get_versions(): return json.loads(version_json) """ def versions_from_file(filename): """Try to determine the version from _version.py if present.""" try: with open(filename) as f: contents = f.read() except EnvironmentError: raise NotThisMethod("unable to read _version.py") mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) if not mo: mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) if not mo: raise NotThisMethod("no version_json in _version.py") return json.loads(mo.group(1)) def write_to_version_file(filename, versions): """Write the given version number to the given _version.py file.""" os.unlink(filename) contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) with open(filename, "w") as f: f.write(SHORT_VERSION_PY % contents) print("set %s to '%s'" % (filename, versions["version"])) def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" def render_pep440(pieces): """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty Exceptions: 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += plus_or_dot(pieces) rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_pre(pieces): """TAG[.post.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post.devDISTANCE """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += ".post.dev%d" % pieces["distance"] else: # exception #1 rendered = "0.post.dev%d" % pieces["distance"] return rendered def render_pep440_post(pieces): """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards (a dirty tree will appear "older" than the corresponding clean one), but you shouldn't be releasing software with -dirty anyways. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%s" % pieces["short"] else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += "+g%s" % pieces["short"] return rendered def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. Eexceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" return rendered def render_git_describe(pieces): """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render_git_describe_long(pieces): """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. The distance/hash is unconditional. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, "error": pieces["error"], "date": None} if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: raise ValueError("unknown style '%s'" % style) return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date")} class VersioneerBadRootError(Exception): """The project root directory is unknown or missing key files.""" def get_versions(verbose=False): """Get the project version from whatever source is available. Returns dict with two keys: 'version' and 'full'. """ if "versioneer" in sys.modules: # see the discussion in cmdclass.py:get_cmdclass() del sys.modules["versioneer"] root = get_root() cfg = get_config_from_root(root) assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" handlers = HANDLERS.get(cfg.VCS) assert handlers, "unrecognized VCS '%s'" % cfg.VCS verbose = verbose or cfg.verbose assert cfg.versionfile_source is not None, \ "please set versioneer.versionfile_source" assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" versionfile_abs = os.path.join(root, cfg.versionfile_source) # extract version from first of: _version.py, VCS command (e.g. 'git # describe'), parentdir. This is meant to work for developers using a # source checkout, for users of a tarball created by 'setup.py sdist', # and for users of a tarball/zipball created by 'git archive' or github's # download-from-tag feature or the equivalent in other VCSes. get_keywords_f = handlers.get("get_keywords") from_keywords_f = handlers.get("keywords") if get_keywords_f and from_keywords_f: try: keywords = get_keywords_f(versionfile_abs) ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) if verbose: print("got version from expanded keyword %s" % ver) return ver except NotThisMethod: pass try: ver = versions_from_file(versionfile_abs) if verbose: print("got version from file %s %s" % (versionfile_abs, ver)) return ver except NotThisMethod: pass from_vcs_f = handlers.get("pieces_from_vcs") if from_vcs_f: try: pieces = from_vcs_f(cfg.tag_prefix, root, verbose) ver = render(pieces, cfg.style) if verbose: print("got version from VCS %s" % ver) return ver except NotThisMethod: pass try: if cfg.parentdir_prefix: ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) if verbose: print("got version from parentdir %s" % ver) return ver except NotThisMethod: pass if verbose: print("unable to compute version") return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to compute version", "date": None} def get_version(): """Get the short version string for this project.""" return get_versions()["version"] def get_cmdclass(): """Get the custom setuptools/distutils subclasses used by Versioneer.""" if "versioneer" in sys.modules: del sys.modules["versioneer"] # this fixes the "python setup.py develop" case (also 'install' and # 'easy_install .'), in which subdependencies of the main project are # built (using setup.py bdist_egg) in the same python process. Assume # a main project A and a dependency B, which use different versions # of Versioneer. A's setup.py imports A's Versioneer, leaving it in # sys.modules by the time B's setup.py is executed, causing B to run # with the wrong versioneer. Setuptools wraps the sub-dep builds in a # sandbox that restores sys.modules to it's pre-build state, so the # parent is protected against the child's "import versioneer". By # removing ourselves from sys.modules here, before the child build # happens, we protect the child from the parent's versioneer too. # Also see https://github.com/warner/python-versioneer/issues/52 cmds = {} # we add "version" to both distutils and setuptools from distutils.core import Command class cmd_version(Command): description = "report generated version string" user_options = [] boolean_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): vers = get_versions(verbose=True) print("Version: %s" % vers["version"]) print(" full-revisionid: %s" % vers.get("full-revisionid")) print(" dirty: %s" % vers.get("dirty")) print(" date: %s" % vers.get("date")) if vers["error"]: print(" error: %s" % vers["error"]) cmds["version"] = cmd_version # we override "build_py" in both distutils and setuptools # # most invocation pathways end up running build_py: # distutils/build -> build_py # distutils/install -> distutils/build ->.. # setuptools/bdist_wheel -> distutils/install ->.. # setuptools/bdist_egg -> distutils/install_lib -> build_py # setuptools/install -> bdist_egg ->.. # setuptools/develop -> ? # pip install: # copies source tree to a tempdir before running egg_info/etc # if .git isn't copied too, 'git describe' will fail # then does setup.py bdist_wheel, or sometimes setup.py install # setup.py egg_info -> ? # we override different "build_py" commands for both environments if "setuptools" in sys.modules: from setuptools.command.build_py import build_py as _build_py else: from distutils.command.build_py import build_py as _build_py class cmd_build_py(_build_py): def run(self): root = get_root() cfg = get_config_from_root(root) versions = get_versions() _build_py.run(self) # now locate _version.py in the new build/ directory and replace # it with an updated value if cfg.versionfile_build: target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) cmds["build_py"] = cmd_build_py if "cx_Freeze" in sys.modules: # cx_freeze enabled? from cx_Freeze.dist import build_exe as _build_exe # nczeczulin reports that py2exe won't like the pep440-style string # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. # setup(console=[{ # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION # "product_version": versioneer.get_version(), # ... class cmd_build_exe(_build_exe): def run(self): root = get_root() cfg = get_config_from_root(root) versions = get_versions() target_versionfile = cfg.versionfile_source print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) _build_exe.run(self) os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] f.write(LONG % {"DOLLAR": "$", "STYLE": cfg.style, "TAG_PREFIX": cfg.tag_prefix, "PARENTDIR_PREFIX": cfg.parentdir_prefix, "VERSIONFILE_SOURCE": cfg.versionfile_source, }) cmds["build_exe"] = cmd_build_exe del cmds["build_py"] if 'py2exe' in sys.modules: # py2exe enabled? try: from py2exe.distutils_buildexe import py2exe as _py2exe # py3 except ImportError: from py2exe.build_exe import py2exe as _py2exe # py2 class cmd_py2exe(_py2exe): def run(self): root = get_root() cfg = get_config_from_root(root) versions = get_versions() target_versionfile = cfg.versionfile_source print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) _py2exe.run(self) os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] f.write(LONG % {"DOLLAR": "$", "STYLE": cfg.style, "TAG_PREFIX": cfg.tag_prefix, "PARENTDIR_PREFIX": cfg.parentdir_prefix, "VERSIONFILE_SOURCE": cfg.versionfile_source, }) cmds["py2exe"] = cmd_py2exe # we override different "sdist" commands for both environments if "setuptools" in sys.modules: from setuptools.command.sdist import sdist as _sdist else: from distutils.command.sdist import sdist as _sdist class cmd_sdist(_sdist): def run(self): versions = get_versions() self._versioneer_generated_versions = versions # unless we update this, the command will keep using the old # version self.distribution.metadata.version = versions["version"] return _sdist.run(self) def make_release_tree(self, base_dir, files): root = get_root() cfg = get_config_from_root(root) _sdist.make_release_tree(self, base_dir, files) # now locate _version.py in the new base_dir directory # (remembering that it may be a hardlink) and replace it with an # updated value target_versionfile = os.path.join(base_dir, cfg.versionfile_source) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, self._versioneer_generated_versions) cmds["sdist"] = cmd_sdist return cmds CONFIG_ERROR = """ setup.cfg is missing the necessary Versioneer configuration. You need a section like: [versioneer] VCS = git style = pep440 versionfile_source = src/myproject/_version.py versionfile_build = myproject/_version.py tag_prefix = parentdir_prefix = myproject- You will also need to edit your setup.py to use the results: import versioneer setup(version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), ...) Please read the docstring in ./versioneer.py for configuration instructions, edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. """ SAMPLE_CONFIG = """ # See the docstring in versioneer.py for instructions. Note that you must # re-run 'versioneer.py setup' after changing this section, and commit the # resulting files. [versioneer] #VCS = git #style = pep440 #versionfile_source = #versionfile_build = #tag_prefix = #parentdir_prefix = """ INIT_PY_SNIPPET = """ from ._version import get_versions __version__ = get_versions()['version'] del get_versions """ def do_setup(): """Main VCS-independent setup function for installing Versioneer.""" root = get_root() try: cfg = get_config_from_root(root) except (EnvironmentError, configparser.NoSectionError, configparser.NoOptionError) as e: if isinstance(e, (EnvironmentError, configparser.NoSectionError)): print("Adding sample versioneer config to setup.cfg", file=sys.stderr) with open(os.path.join(root, "setup.cfg"), "a") as f: f.write(SAMPLE_CONFIG) print(CONFIG_ERROR, file=sys.stderr) return 1 print(" creating %s" % cfg.versionfile_source) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] f.write(LONG % {"DOLLAR": "$", "STYLE": cfg.style, "TAG_PREFIX": cfg.tag_prefix, "PARENTDIR_PREFIX": cfg.parentdir_prefix, "VERSIONFILE_SOURCE": cfg.versionfile_source, }) ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") if os.path.exists(ipy): try: with open(ipy, "r") as f: old = f.read() except EnvironmentError: old = "" if INIT_PY_SNIPPET not in old: print(" appending to %s" % ipy) with open(ipy, "a") as f: f.write(INIT_PY_SNIPPET) else: print(" %s unmodified" % ipy) else: print(" %s doesn't exist, ok" % ipy) ipy = None # Make sure both the top-level "versioneer.py" and versionfile_source # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so # they'll be copied into source distributions. Pip won't be able to # install the package without this. manifest_in = os.path.join(root, "MANIFEST.in") simple_includes = set() try: with open(manifest_in, "r") as f: for line in f: if line.startswith("include "): for include in line.split()[1:]: simple_includes.add(include) except EnvironmentError: pass # That doesn't cover everything MANIFEST.in can do # (http://docs.python.org/2/distutils/sourcedist.html#commands), so # it might give some false negatives. Appending redundant 'include' # lines is safe, though. if "versioneer.py" not in simple_includes: print(" appending 'versioneer.py' to MANIFEST.in") with open(manifest_in, "a") as f: f.write("include versioneer.py\n") else: print(" 'versioneer.py' already in MANIFEST.in") if cfg.versionfile_source not in simple_includes: print(" appending versionfile_source ('%s') to MANIFEST.in" % cfg.versionfile_source) with open(manifest_in, "a") as f: f.write("include %s\n" % cfg.versionfile_source) else: print(" versionfile_source already in MANIFEST.in") # Make VCS-specific changes. For git, this means creating/changing # .gitattributes to mark _version.py for export-subst keyword # substitution. do_vcs_install(manifest_in, cfg.versionfile_source, ipy) return 0 def scan_setup_py(): """Validate the contents of setup.py against Versioneer's expectations.""" found = set() setters = False errors = 0 with open("setup.py", "r") as f: for line in f.readlines(): if "import versioneer" in line: found.add("import") if "versioneer.get_cmdclass()" in line: found.add("cmdclass") if "versioneer.get_version()" in line: found.add("get_version") if "versioneer.VCS" in line: setters = True if "versioneer.versionfile_source" in line: setters = True if len(found) != 3: print("") print("Your setup.py appears to be missing some important items") print("(but I might be wrong). Please make sure it has something") print("roughly like the following:") print("") print(" import versioneer") print(" setup( version=versioneer.get_version(),") print(" cmdclass=versioneer.get_cmdclass(), ...)") print("") errors += 1 if setters: print("You should remove lines like 'versioneer.VCS = ' and") print("'versioneer.versionfile_source = ' . This configuration") print("now lives in setup.cfg, and should be removed from setup.py") print("") errors += 1 return errors if __name__ == "__main__": cmd = sys.argv[1] if cmd == "setup": errors = do_setup() errors += scan_setup_py() if errors: sys.exit(1)