diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5aabdc2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e6a9b28 --- /dev/null +++ b/LICENSE @@ -0,0 +1,124 @@ +Apache License +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by +Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting +the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are +controlled by, or are under common control with that entity. For the purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the direction or management of such entity, +whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding +shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software +source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, +including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, +as indicated by a copyright notice that is included in or attached to the work (an example is provided in +the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) +the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, +as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not +include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work +and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any +modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to +Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to +submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of +electronic, verbal, or written communication sent to the Licensor or its representatives, including but not +limited to communication on electronic mailing lists, source code control systems, and issue tracking systems +that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner +as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been +received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license +to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute +the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated +in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer +the Work, where such license applies only to those patent claims licensable by such Contributor that are +necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work +to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including +a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the +Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under +this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any +medium, with or without modifications, and in Source or Object form, provided that You meet the following +conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, +trademark, and attribution notices from the Source form of the Work, excluding those notices that do not +pertain to any part of the Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You +distribute must include a readable copy of the attribution notices contained within such NOTICE file, +excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source +form or documentation, if provided along with the Derivative Works; or, within a display generated by the +Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file +are for informational purposes only and do not modify the License. You may add Your own attribution notices +within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different +license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such +Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise +complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally +submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this +License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall +supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or +product names of the Licensor, except as required for reasonable and customary use in describing the origin +of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for +determining the appropriateness of using or redistributing the Work and assume any risks associated with Your +exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) +or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, +special, incidental, or consequential damages of any character arising as a result of this License or out +of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work +stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such +Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, +You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other +liability obligations and/or rights consistent with this License. However, in accepting such obligations, +You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred +by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional +liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5dd03c --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Locally Optimized Product Quantization + +This is Python training and testing code for Locally Optimized Product Quantization (LOPQ) models, as well as Spark scripts to scale training to hundreds of millions of vectors. The resulting model can be used in Python with code provided here or deployed via a Protobuf format to, e.g., search backends for high performance approximate nearest neighbor search. + +### Overview + +Locally Optimized Product Quantization (LOPQ) [1] is a hierarchical quantization algorithm that produces codes of configurable length for data points. These codes are efficient representations of the original vector and can be used in a variety of ways depending on application, including as hashes that preserve locality, as a compressed vector from which an approximate vector in the data space can be reconstructed, and as a representation from which to compute an approximation of the Euclidean distance between points. + +Conceptually, the LOPQ quantization process can be broken into 4 phases. The training process also fits these phases to the data in the same order. + +1. The raw data vector is PCA'd to `D` dimensions (possibly the original dimensionality). This allows subsequent quantization to more efficiently represent the variation present in the data. +2. The PCA'd data is then product quantized [2] by two k-means quantizers. This means that each vector is split into two subvectors each of dimension `D / 2`, and each of the two subspaces is quantized independently with a vocabulary of size `V`. Since the two quantizations occur independently, the dimensions of the vectors are permuted such that the total variance in each of the two subspaces is approximately equal, which allows the two vocabularies to be equally important in terms of capturing the total variance of the data. This results in a pair of cluster ids that we refer to as "coarse codes". +3. The residuals of the data after coarse quantization are computed. The residuals are then locally projected independently for each coarse cluster. This projection is another application of PCA and dimension permutation on the residuals and it is "local" in the sense that there is a different projection for each cluster in each of the two coarse vocabularies. These local rotations make the next and final step, another application of product quantization, very efficient in capturing the variance of the residuals. +4. The locally projected data is then product quantized a final time by `M` subquantizers, resulting in `M` "fine codes". Usually the vocabulary for each of these subquantizers will be a power of 2 for effective storage in a search index. With vocabularies of size 256, the fine codes for each indexed vector will require `M` bytes to store in the index. + +The final LOPQ code for a vector is a `(coarse codes, fine codes)` pair, e.g. `((3, 2), (14, 164, 83, 49, 185, 29, 196, 250))`. + +### Nearest Neighbor Search + +A nearest neighbor index can be built from these LOPQ codes by indexing each document into its corresponding coarse code bucket. That is, each pair of coarse codes (which we refer to as a "cell") will index a bucket of the vectors quantizing to that cell. + +At query time, an incoming query vector undergoes substantially the same process. First, the query is split into coarse subvectors and the distance to each coarse centroid is computed. These distances can be used to efficiently compute a priority-ordered sequence of cells [3] such that cells later in the sequence are less likely to have near neighbors of the query than earlier cells. The items in cell buckets are retrieved in this order until some desired quota has been met. + +After this retrieval phase, the fine codes are used to rank by approximate Euclidean distance. The query is projected into each local space and the distance to each indexed item is estimated as the sum of the squared distances of the query subvectors to the corresponding subquantizer centroid indexed by the fine codes. + +NN search with LOPQ is highly scalable and has excellent properties in terms of both index storage requirements and query-time latencies when implemented well. + +#### References + +For more information and performance benchmarks can be found at http://image.ntua.gr/iva/research/lopq/. + +1. Y. Kalantidis, Y. Avrithis. [Locally Optimized Product Quantization for Approximate Nearest Neighbor Search.](http://image.ntua.gr/iva/files/lopq.pdf) CVPR 2014. +2. H. Jegou, M. Douze, and C. Schmid. [Product quantization for nearest neighbor search.](https://lear.inrialpes.fr/pubs/2011/JDS11/jegou_searching_with_quantization.pdf) PAMI, 33(1), 2011. +3. A. Babenko and V. Lempitsky. [The inverted multi-index.](http://www.computer.org/csdl/trans/tp/preprint/06915715.pdf) CVPR 2012. + +### Python + +Full LOPQ training and evaluation in implemented in the `lopq` python module. Please refer to the README in `python/` for more detail. + +### Spark + +The training algorithm is also implemented on Spark using `pyspark` to scale parameter fitting to large datasets. Please refer to the README in `spark/` for documentation and usage information. + +#### Running Tests + +Tests can be run during development by running: + +```bash +cd python/ +bash test.sh +``` + +To run tests in a virtual environment this project uses [tox](http://tox.testrun.org/). Tox can be installed with `pip install tox` and run from the `python/` directory: + +```bash +cd python/ +tox +``` + +#### License + +Code licensed under the Apache License, Version 2.0 license. See LICENSE file for terms. diff --git a/data/oxford/README.md b/data/oxford/README.md new file mode 100644 index 0000000..94fbdf2 --- /dev/null +++ b/data/oxford/README.md @@ -0,0 +1,5 @@ +### Oxford dataset + +This test dataset is included for tests and the short `example.py` tutorial. The `oxford_features.fvecs` files contains 5063 128-dimensional vectors representing each of the images in the Oxford5k dataset (http://www.robots.ox.ac.uk/~vgg/data/oxbuildings/). The data format is that described here: http://corpus-texmex.irisa.fr/. + +The features themselves are the rectified fc7 layer outputs of the BVLC Caffe reference model (https://github.com/BVLC/caffe/tree/master/models/bvlc_reference_caffenet) extracted from resized images and then PCA'd to 128 dimensions from their original 4096. diff --git a/data/oxford/oxford_features.fvecs b/data/oxford/oxford_features.fvecs new file mode 100644 index 0000000..d6c00a9 Binary files /dev/null and b/data/oxford/oxford_features.fvecs differ diff --git a/protobuf/README.md b/protobuf/README.md new file mode 100644 index 0000000..57657ae --- /dev/null +++ b/protobuf/README.md @@ -0,0 +1,32 @@ +# Protocol buffer schema + +The `.proto` file defines the data schema for LOPQ model parameters. +It can be compiled into libraries for any target languages +that will use this data (i.e., Java and Python). A compiled version +for Python is included in the `lopq` module. + +See: [https://developers.google.com/protocol-buffers/docs/overview](https://developers.google.com/protocol-buffers/docs/overview) +for details on protocol buffers, and how to update the schema. +Note, to keep the schema backwards compatable, it is important not to +alter the `tags` (ints) assigned to each field in `.proto` file. + + +### Compiling `.proto` file + +Compile for Java: + +```bash +# from the repository root +protoc -I=protobuf \ + --java_out=. \ + protobuf/lopq_model.proto +``` + +Compile for Python: + +```bash +# from the repository root +protoc -I=protobuf \ + --python_out=python/lopq \ + protobuf/lopq_model.proto +``` diff --git a/protobuf/lopq_model.proto b/protobuf/lopq_model.proto new file mode 100644 index 0000000..092ee36 --- /dev/null +++ b/protobuf/lopq_model.proto @@ -0,0 +1,41 @@ +// Copyright 2015, Yahoo Inc. +// Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. + +// define a namespace for protobuffer +package com.flickr.vision.lopq; + +// options for how java class should be built and packaged +option java_package = "com.flickr.vision.lopq"; +option java_outer_classname = "LOPQModelParameters"; +option optimize_for = SPEED; + + +///////////////////////////////////////////////////////////////// +// GENERIC TYPES + +message Vector { + repeated float values = 1 [packed = true]; +} + +message Matrix { + repeated float values = 1 [packed = true]; + repeated uint32 shape = 2; +} + +///////////////////////////////////////////////////////////////// +// LOPQ PARAMS + +// lopq model params +// file extension: .lopq +message LOPQModelParams { + + optional uint32 D = 1; // dimensionality of original vectors + optional uint32 V = 2; // number of coarse quantizer centroids + optional uint32 M = 3; // number of subvectors + optional uint32 num_subquantizers = 4; // number of subquantizer clusters + + repeated Matrix Cs = 5; // coarse quantizer centroids - 2 of these; size V x (D/2) + repeated Matrix Rs = 6; // rotations - 2 * V of these, each size D/2 x D/2 + repeated Vector mus = 7; // residual means - 2 * V of these, each size D/2 + repeated Matrix subs = 8; // subquantizer centroids - M of these, each size num_subquantizers x (D/2)) +} diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..9ff6816 --- /dev/null +++ b/python/README.md @@ -0,0 +1,55 @@ +# Python LOPQ module + +This module implements training and testing of LOPQ models along with a variety of other utilities useful for evaluation and data management. It includes a simple implementation of approximate nearest neighbor search with an LOPQ index. + +## Usage + +```python +from lopq import LOPQModel, LOPQSearcher + +# Define a model and fit it to data +model = LOPQModel(V=8, M=4) +model.fit(data) + +# Compute the LOPQ codes for a vector +code = model.predict(x) + +# Create a searcher to index data with the model +searcher = LOPQSearcher(model) +searcher.add_data(data) + +# Retrieve ranked nearest neighbors +nns = searcher.search(x, quota=100) +``` + +A more detailed usage walk-through is found in `scripts/example.py`. + +## Training + +Refer to the documentation in the `model` submodules and, in particular, the `LOPQModel` class for more usage information. + +Available parameters for fitting data: + +| Name | Default | Description | +| ------------------------- | ------- | ------------------------------------------------------------------------- | +| V | 8 | The number of clusters per coarse quantizer. | +| M | 4 | The total number of fine codes. | +| kmeans_coarse_iters | 10 | The number of iterations of k-means for training coarse quantizers. | +| kmeans_local_iters | 20 | The number of iterations of k-means for training subquantizers. | +| subquantizer_clusters | 256 | The number of clusters to train per subquantizer. | +| subquantizer_sample_ratio | 1.0 | The ratio of the data to sample for training subquantizers. | +| random_state | None | A seed for seeding random operations during training. | +| parameters | None | A tuple of trained model parameters to instantiate the model with. | +| verbose | False | A boolean indicating whether to produce verbose output. | + +## Submodules + +There are a handful of submodules, here is a brief description of each. + +| Submodule | Description | +| -------------- | ----------- | +| model | Core training algorithm and the `LOPQModel` class that encapsulates model parameters. +| search | An implementation of the multisequence algorithm for retrieval on a multi-index as well as the `LOPQSearcher` class, a simple Python implementation of an LOPQ search index and LOPQ ranking. | +| eval | Functions to aid in evaluating and benchmarking trained LOPQ models. | +| utils | Miscellaneous utility functions. | +| lopq_model_pb2 | Protobuf generated module. | diff --git a/python/lopq/__init__.py b/python/lopq/__init__.py new file mode 100644 index 0000000..33be870 --- /dev/null +++ b/python/lopq/__init__.py @@ -0,0 +1,9 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +import model +import search +import utils +from .model import LOPQModel +from .search import LOPQSearcher, multisequence + +__all__ = [LOPQModel, LOPQSearcher, multisequence, model, search, utils] diff --git a/python/lopq/eval.py b/python/lopq/eval.py new file mode 100644 index 0000000..5d5ae48 --- /dev/null +++ b/python/lopq/eval.py @@ -0,0 +1,161 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +import time +import numpy as np + + +def compute_all_neighbors(data1, data2=None, just_nn=True): + """ + For each point in data1, compute a ranked list of neighbor indices from data2. + If data2 is not provided, compute neighbors relative to data1 + + :param ndarray data1: + an m1 x n dim matrix with observations on the rows + :param ndarray data2: + an m2 x n dim matrix with observations on the rows + + :returns ndarray: + an m1 x m2 dim matrix with the distance-sorted indices of neighbors on the rows + """ + from scipy.spatial.distance import cdist + + if data2 is None: + data2 = data1 + + dists = cdist(data1, data2) + + if just_nn: + nns = np.zeros((data1.shape[0]), dtype=int) + else: + nns = np.zeros(dists.shape, dtype=int) + + for i in xrange(dists.shape[0]): + if just_nn: + nns[i] = np.argmin(dists[i]) + else: + nns[i] = np.argsort(dists[i]) + + return nns + + +def get_proportion_nns_with_same_coarse_codes(data, model, nns=None): + """ + """ + N = data.shape[0] + + # Compute nearest neighbors if not provided + if nns is None: + nns = compute_all_neighbors(data) + + # Compute coarse codes for data + coarse_codes = [] + for d in data: + c = model.predict_coarse(d) + coarse_codes.append(c) + + # Count the number of NNs that share the same coarse codes + count = 0 + for i in xrange(N): + nn = nns[i] + if coarse_codes[i] == coarse_codes[nn]: + count += 1 + + return float(count) / N + + +def get_cell_histogram(data, model): + # Compute cells for data + cells = [] + for d in data: + c = model.predict_coarse(d) + cell = model.get_cell_id_for_coarse_codes(c) + cells.append(cell) + + return np.histogram(cells, bins=range(model.V ** 2))[0] + + +def get_proportion_of_reconstructions_with_same_codes(data, model): + N = data.shape[0] + + # Compute coarse codes for data + count = 0 + for d in data: + c1 = model.predict(d) + r = model.reconstruct(c1) + c2 = model.predict(r) + if c1 == c2: + count += 1 + + return float(count) / N + + +def get_recall(searcher, queries, nns, thresholds=[1, 10, 100, 1000], normalize=True, verbose=False): + """ + Given a LOPQSearcher object with indexed data and groundtruth nearest neighbors for a set of test + query vectors, collect and return recall statistics. + + :param LOPQSearcher searcher: + a searcher that contains the indexed nearest neighbors + :param ndarray queries: + a collect of test vectors with vectors on the rows + :param ndarray nns: + a list of true nearest neighbor ids for each vector in queries + :param list thresholds: + the recall thresholds to evaluate - the last entry defines the number of + results to retrieve before ranking + :param bool normalize: + flag to indicate whether the result should be normalized by the number of queries + :param bool verbose: + flag to print every 50th search to visualize progress + + :return list: + a list of recalls for each specified threshold level + :return float: + the elapsed query time + """ + + recall = np.zeros(len(thresholds)) + query_time = 0.0 + for i, d in enumerate(queries): + + nn = nns[i] + + start = time.clock() + results, cells_visited = searcher.search(d, thresholds[-1]) + query_time += time.clock() - start + + if verbose and i % 50 == 0: + print '%d cells visitied for query %d' % (cells_visited, i) + + for j, res in enumerate(results): + rid, code = res + + if rid == nn: + for k, t in enumerate(thresholds): + if j < t: + recall[k] += 1 + + if normalize: + N = queries.shape[0] + return recall / N, query_time / N + else: + return recall, query_time + + +def get_subquantizer_distortion(data, model): + from .model import compute_residuals, project_residuals_to_local + + first_half, second_half = np.split(data, 2, axis=1) + + r1, a1 = compute_residuals(first_half, model.Cs[0]) + r2, a2 = compute_residuals(second_half, model.Cs[1]) + + p1 = project_residuals_to_local(r1, a1, model.Rs[0], model.mus[0]) + p2 = project_residuals_to_local(r2, a2, model.Rs[1], model.mus[1]) + + pall = np.concatenate((p1, p2), axis=1) + suball = model.subquantizers[0] + model.subquantizers[1] + + dists = np.array([sum(np.linalg.norm(compute_residuals(d, c)[0], ord=2, axis=1) ** 2) for c, d in zip(suball, np.split(pall, 8, axis=1))]) + + return dists / data.shape[0] diff --git a/python/lopq/lopq_model_pb2.py b/python/lopq/lopq_model_pb2.py new file mode 100644 index 0000000..2109a8c --- /dev/null +++ b/python/lopq/lopq_model_pb2.py @@ -0,0 +1,209 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: lopq_model.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='lopq_model.proto', + package='com.flickr.vision.lopq', + serialized_pb=_b('\n\x10lopq_model.proto\x12\x16\x63om.flickr.vision.lopq\"\x1c\n\x06Vector\x12\x12\n\x06values\x18\x01 \x03(\x02\x42\x02\x10\x01\"+\n\x06Matrix\x12\x12\n\x06values\x18\x01 \x03(\x02\x42\x02\x10\x01\x12\r\n\x05shape\x18\x02 \x03(\r\"\x80\x02\n\x0fLOPQModelParams\x12\t\n\x01\x44\x18\x01 \x01(\r\x12\t\n\x01V\x18\x02 \x01(\r\x12\t\n\x01M\x18\x03 \x01(\r\x12\x19\n\x11num_subquantizers\x18\x04 \x01(\r\x12*\n\x02\x43s\x18\x05 \x03(\x0b\x32\x1e.com.flickr.vision.lopq.Matrix\x12*\n\x02Rs\x18\x06 \x03(\x0b\x32\x1e.com.flickr.vision.lopq.Matrix\x12+\n\x03mus\x18\x07 \x03(\x0b\x32\x1e.com.flickr.vision.lopq.Vector\x12,\n\x04subs\x18\x08 \x03(\x0b\x32\x1e.com.flickr.vision.lopq.MatrixB/\n\x16\x63om.flickr.vision.lopqB\x13LOPQModelParametersH\x01') +) +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + + + + +_VECTOR = _descriptor.Descriptor( + name='Vector', + full_name='com.flickr.vision.lopq.Vector', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='values', full_name='com.flickr.vision.lopq.Vector.values', index=0, + number=1, type=2, cpp_type=6, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\020\001'))), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + serialized_start=44, + serialized_end=72, +) + + +_MATRIX = _descriptor.Descriptor( + name='Matrix', + full_name='com.flickr.vision.lopq.Matrix', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='values', full_name='com.flickr.vision.lopq.Matrix.values', index=0, + number=1, type=2, cpp_type=6, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\020\001'))), + _descriptor.FieldDescriptor( + name='shape', full_name='com.flickr.vision.lopq.Matrix.shape', index=1, + number=2, type=13, cpp_type=3, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + serialized_start=74, + serialized_end=117, +) + + +_LOPQMODELPARAMS = _descriptor.Descriptor( + name='LOPQModelParams', + full_name='com.flickr.vision.lopq.LOPQModelParams', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='D', full_name='com.flickr.vision.lopq.LOPQModelParams.D', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='V', full_name='com.flickr.vision.lopq.LOPQModelParams.V', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='M', full_name='com.flickr.vision.lopq.LOPQModelParams.M', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='num_subquantizers', full_name='com.flickr.vision.lopq.LOPQModelParams.num_subquantizers', index=3, + number=4, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='Cs', full_name='com.flickr.vision.lopq.LOPQModelParams.Cs', index=4, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='Rs', full_name='com.flickr.vision.lopq.LOPQModelParams.Rs', index=5, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='mus', full_name='com.flickr.vision.lopq.LOPQModelParams.mus', index=6, + number=7, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='subs', full_name='com.flickr.vision.lopq.LOPQModelParams.subs', index=7, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + oneofs=[ + ], + serialized_start=120, + serialized_end=376, +) + +_LOPQMODELPARAMS.fields_by_name['Cs'].message_type = _MATRIX +_LOPQMODELPARAMS.fields_by_name['Rs'].message_type = _MATRIX +_LOPQMODELPARAMS.fields_by_name['mus'].message_type = _VECTOR +_LOPQMODELPARAMS.fields_by_name['subs'].message_type = _MATRIX +DESCRIPTOR.message_types_by_name['Vector'] = _VECTOR +DESCRIPTOR.message_types_by_name['Matrix'] = _MATRIX +DESCRIPTOR.message_types_by_name['LOPQModelParams'] = _LOPQMODELPARAMS + +Vector = _reflection.GeneratedProtocolMessageType('Vector', (_message.Message,), dict( + DESCRIPTOR = _VECTOR, + __module__ = 'lopq_model_pb2' + # @@protoc_insertion_point(class_scope:com.flickr.vision.lopq.Vector) + )) +_sym_db.RegisterMessage(Vector) + +Matrix = _reflection.GeneratedProtocolMessageType('Matrix', (_message.Message,), dict( + DESCRIPTOR = _MATRIX, + __module__ = 'lopq_model_pb2' + # @@protoc_insertion_point(class_scope:com.flickr.vision.lopq.Matrix) + )) +_sym_db.RegisterMessage(Matrix) + +LOPQModelParams = _reflection.GeneratedProtocolMessageType('LOPQModelParams', (_message.Message,), dict( + DESCRIPTOR = _LOPQMODELPARAMS, + __module__ = 'lopq_model_pb2' + # @@protoc_insertion_point(class_scope:com.flickr.vision.lopq.LOPQModelParams) + )) +_sym_db.RegisterMessage(LOPQModelParams) + + +DESCRIPTOR.has_options = True +DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n\026com.flickr.vision.lopqB\023LOPQModelParametersH\001')) +_VECTOR.fields_by_name['values'].has_options = True +_VECTOR.fields_by_name['values']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\020\001')) +_MATRIX.fields_by_name['values'].has_options = True +_MATRIX.fields_by_name['values']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\020\001')) +# @@protoc_insertion_point(module_scope) diff --git a/python/lopq/model.py b/python/lopq/model.py new file mode 100644 index 0000000..f665e14 --- /dev/null +++ b/python/lopq/model.py @@ -0,0 +1,760 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +import numpy as np +from sklearn.cluster import KMeans +import logging +import sys +from collections import namedtuple +from .utils import iterate_splits, predict_cluster + +logger = logging.getLogger(__name__) +logger.setLevel(logging.WARNING) +logger.addHandler(logging.StreamHandler(sys.stdout)) + + +######################################## +# Core training algo +######################################## + +def eigenvalue_allocation(num_buckets, eigenvalues): + """ + Compute a permutation of eigenvalues to balance variance accross buckets + of dimensions. + + Described in section 3.2.4 in http://research.microsoft.com/pubs/187499/cvpr13opq.pdf + + Note, the following slides indicate this function will break when fed eigenvalues < 1 + without the scaling trick implemented below: + + https://www.robots.ox.ac.uk/~vgg/rg/slides/ge__cvpr2013__optimizedpq.pdf + + + :param int num_buckets: + the number of dimension buckets over which to allocate eigenvalues + :param ndarray eigenvalues: + a vector of eigenvalues + + :returns ndarray: + a vector of indices by which to permute the eigenvectors + """ + D = len(eigenvalues) + dims_per_bucket = D / num_buckets + eigenvalue_product = np.zeros(num_buckets, dtype=float) + bucket_size = np.zeros(num_buckets, dtype=int) + permutation = np.zeros((num_buckets, dims_per_bucket), dtype=int) + + # We first must scale the eigenvalues by dividing by their + # smallets non-zero value to avoid problems with the algorithm + # when eigenvalues are less than 1. + min_non_zero_eigenvalue = np.min(np.abs(eigenvalues[np.nonzero(eigenvalues)])) + eigenvalues = eigenvalues / min_non_zero_eigenvalue + + # Iterate eigenvalues in descending order + sorted_inds = np.argsort(eigenvalues)[::-1] + log_eigs = np.log2(abs(eigenvalues)) + for ind in sorted_inds: + + # Find eligible (not full) buckets + eligible = (bucket_size < dims_per_bucket).nonzero() + + # Find eligible bucket with least eigenvalue product + i = eigenvalue_product[eligible].argmin(0) + bucket = eligible[0][i] + + # Update eigenvalue product for this bucket + eigenvalue_product[bucket] = eigenvalue_product[bucket] + log_eigs[ind] + + # Store bucket assignment and update size + permutation[bucket, bucket_size[bucket]] = ind + bucket_size[bucket] += 1 + + return np.reshape(permutation, D) + + +def compute_local_rotations(data, C, num_buckets): + """ + Compute a rotation matrix for each cluster in the model by estimating + the covariance matrix for each cluster and balancing variance across + buckets of the dimensions. + + :param ndarray data: + data points, each row an observation + :param ndarray C: + a VxD matrix of cluster centroids + :param int num_buckets: + the number of buckets accross which to balance variance in the rotation matrix + + :returns ndarray: + a VxDxD tensor containing the rotation matrix for each cluster + where V is the number of clusters and D the dimension of the data (which is split here) + :returns ndarray: + a Nx1 vector of cluster assignments for each data point + :returns ndarray: + a VxD vector of mean residuals for each cluster + :returns ndarray: + an NxD matrix of residuals + """ + + logger.info('Fitting local rotations...') + + A, mu, count, assignments, residuals = accumulate_covariance_estimators(data, C) + + R, mu = compute_rotations_from_accumulators(A, mu, count, num_buckets) + + logger.info('Done fitting local rotations.') + + return R, mu, assignments, residuals + + +def accumulate_covariance_estimators(data, C): + """ + Accumulate covariance estimators for each cluster with a pass through the data. + + :param ndarray data: + NxD array - observations on the rows + :param ndarray C: + VxD array of cluster centroids + + :returns ndarray A: + VxDxD array - total sum of residual outer products for each cluster + :returns ndarray mu: + VxD array of total sum of residuals per cluster + :returns ndarray count: + Vx1 array of cluster sizes + :returns ndarray assignments: + Nx1 array of cluster assignments + :returns ndarray residuals: + NxD array of data residuals + """ + + V = C.shape[0] + N = data.shape[0] + D = data.shape[1] + + # Essential variables + A = np.zeros((V, D, D)) # accumulators for covariance estimator per cluster + mu = np.zeros((V, D)) # residual means + count = np.zeros(V, dtype=int) # count of points per cluster + assignments = np.zeros(N, dtype=int) # point cluster assignments + residuals = np.zeros((N, D)) # residual for data points given cluster assignment + + # Iterate data points, accumulate estimators + for i in xrange(N): + d = data[i] + + # Find cluster assignment and residual + cluster = predict_cluster(d, C) + centroid = C[cluster] + residual = d - centroid + assignments[i] = cluster + + # Accumulate estimators for covariance matrix for the assigned cluster + mu[cluster] += residual + count[cluster] += 1 + A[cluster] += np.outer(residual, residual) + residuals[i] = residual + + return A, mu, count, assignments, residuals + + +def compute_rotations_from_accumulators(A, mu, count, num_buckets): + """ + Given accumulators computed on cluster residuals, compute the optimal + rotation matrix. The A and mu variables are modified in place and returned to + avoid memory allocation. + + :param ndarray A: + a VxDxD array - total sum of outer products of residuals per cluster + :param ndarray mu: + a VxD array - total sum of residuals per cluster + :param ndarray count: + a Vx1 array - count of points for each cluster + :param int num_buckets: + the number of subvectors to balance variance across + + :returns ndarray A: + a VxDxD array - per cluster local rotations of size DxD on the first dimension + :returns ndarray mu: + a VxD array of mean residuals per cluster + """ + + V, D = mu.shape + + # For each cluster, use accumulator variables to estimate covariance matrix + # and compute rotation matrix + for i in xrange(V): + + # Normalize + num_points = count[i] + mu[i] /= num_points + + # Compute covariance estimator + cov = (A[i] + A[i].transpose()) / (2 * (num_points - 1)) - np.outer(mu[i], mu[i]) + + # Compute eigenvalues, reuse A matrix + if num_points < D: + logger.warn('Fewer points (%d) than dimensions (%d) in rotation computation for cluster %d' % (num_points, D, i)) + eigenvalues = np.ones(D) + A[i] = np.eye(D) + else: + eigenvalues, A[i] = np.linalg.eigh(cov) + + # Permute eigenvectors to balance variance in subquantizers + permuted_inds = eigenvalue_allocation(num_buckets, eigenvalues) + A[i] = A[i, :, permuted_inds] + + return A, mu + + +def project_residuals_to_local(residuals, assignments, Rs, mu): + """ + Given residuals for training datapoints, their cluster assignments, + the mean of cluster residuals, and the cluster rotation matrices, project + all residuals to their appropriate local frame. This is run to generate + points to cluster for subquantizer training. + + :params ndarray residuals: + an NxD array of residuals + :params ndarray assignments: + an Nx1 array of cluster ids assignments for residuals + :params ndarray Rs: + a VxDxD array of rotation matrices for each cluster + :params ndarray mu: + a VxD matrix of mean residuals for each cluster + + :returns ndarray: + an NxD array of locally projected residuals + """ + projected = np.zeros(residuals.shape) + for i in xrange(residuals.shape[0]): + res = residuals[i] + a = assignments[i] + projected[i] = np.dot(Rs[a], res - mu[a]) + + return projected + + +def compute_residuals(data, C): + assignments = np.apply_along_axis(predict_cluster, 1, data, C) + residuals = data - C[assignments] + return residuals, assignments + + +def train_coarse(data, V=8, kmeans_coarse_iters=10, n_init=10, random_state=None): + """ + Train a kmeans model. + + :param ndarray data: + an NxD array with observations on the rows + :param int V: + the number of clusters + :param int kmeans_coarse_iters: + the nubmer of iterations + :param int random_state: + a random state to seed the clustering + + :returns ndarray: + a VxD matrix of cluster centroids + """ + + logger.info('Fitting coarse quantizer...') + + # Fit coarse model + model = KMeans(n_clusters=V, init="k-means++", max_iter=kmeans_coarse_iters, n_init=n_init, n_jobs=1, verbose=False, random_state=random_state) + model.fit(data) + + logger.info('Done fitting coarse quantizer.') + + return model.cluster_centers_ + + +def train_subquantizers(data, num_buckets, subquantizer_clusters=256, kmeans_local_iters=20, n_init=10, random_state=None): + """ + Fit a set of num_buckets subquantizers for corresponding subvectors. + """ + + subquantizers = list() + for i, d in enumerate(np.split(data, num_buckets, axis=1)): + model = KMeans(n_clusters=subquantizer_clusters, init="k-means++", max_iter=kmeans_local_iters, + n_init=n_init, n_jobs=1, verbose=False, random_state=random_state) + model.fit(d) + subquantizers.append(model.cluster_centers_) + logger.info('Fit subquantizer %d of %d.' % (i + 1, num_buckets)) + + return subquantizers + + +def train(data, V=8, M=4, subquantizer_clusters=256, parameters=None, + kmeans_coarse_iters=10, kmeans_local_iters=20, n_init=10, + subquantizer_sample_ratio=1.0, random_state=None, verbose=False): + """ + Fit an LOPQ model. + + :param ndarray data: + a NxD matrix of training data points with observations on the rows + + :param int V: + number of coarse clusters + :param int M: + number of fine codes; same as number of bytes per compressed + vector in memory with 256 subquantizer clusters + :param int subquantizer_clusters: + the number of clusters for each subquantizer + :param tuple parameters: + a tuple of parameters - missing parameters are allowed to be None + + :param int kmeans_coarse_iters: + kmeans iterations + :param int kmeans_local_iters: + kmeans iterations for subquantizers + :param int n_init: + the number of independent kmeans runs for all kmeans when training - set low for faster training + :param float subquantizer_sample_ratio: + the proportion of the training data to sample for training subquantizers - since the number of + subquantizer clusters is much smaller then the number of coarse clusters, less data is needed + :param int random_state: + a random seed used in all random operations during training if provided + :param bool verbose: + a bool enabling verbose output during training + + :returns tuple: + a tuple of model parameters that can be used to instantiate an LOPQModel object + """ + + # Set logging level for verbose mode + if (verbose): + logger.setLevel(logging.DEBUG) + + # Extract parameters + Cs = Rs = mus = subquantizers = None + if parameters is not None: + Cs, Rs, mus, subquantizers = parameters + + # Enforce parameter dependencies + if Rs is None or mus is None: + Rs = mus = None + + # Split vectors + # TODO: permute dims here if this hasn't already been done + first_half, second_half = np.split(data, 2, axis=1) + + # Cluster coarse splits + if Cs is not None: + logger.info('Using existing coarse quantizers.') + C1, C2 = Cs + else: + C1 = train_coarse(first_half, V, kmeans_coarse_iters, n_init, random_state) + C2 = train_coarse(second_half, V, kmeans_coarse_iters, n_init, random_state) + + # Compute local rotations + if Rs is not None and mus is not None: + logger.info('Using existing rotations.') + Rs1, Rs2 = Rs + mu1, mu2 = mus + assignments1 = assignments2 = residuals1 = residuals2 = None + else: + Rs1, mu1, assignments1, residuals1 = compute_local_rotations(first_half, C1, M / 2) + Rs2, mu2, assignments2, residuals2 = compute_local_rotations(second_half, C2, M / 2) + + # Subquantizers don't need as much data, so we could sample here + subquantizer_sample_ratio = min(subquantizer_sample_ratio, 1.0) + N = data.shape[0] + N2 = int(np.floor(subquantizer_sample_ratio * N)) + sample_inds = np.random.RandomState(random_state).choice(N, N2, False) + logger.info('Sampled training data for subquantizers with %f proportion (%d points).' % (subquantizer_sample_ratio, N2)) + + # Use assignments and residuals from rotation computation if available + if assignments1 is not None: + residuals1 = residuals1[sample_inds] + residuals2 = residuals2[sample_inds] + assignments1 = assignments1[sample_inds] + assignments2 = assignments2[sample_inds] + else: + residuals1, assignments1 = compute_residuals(first_half[sample_inds], C1) + residuals2, assignments2 = compute_residuals(second_half[sample_inds], C2) + + # Project residuals + logger.info('Projecting residuals to local frame...') + projected1 = project_residuals_to_local(residuals1, assignments1, Rs1, mu1) + projected2 = project_residuals_to_local(residuals2, assignments2, Rs2, mu2) + + logger.info('Fitting subquantizers...') + subquantizers1 = train_subquantizers(projected1, M / 2, subquantizer_clusters, kmeans_local_iters, n_init, random_state=random_state) + subquantizers2 = train_subquantizers(projected2, M / 2, subquantizer_clusters, kmeans_local_iters, n_init, random_state=random_state) + logger.info('Done fitting subquantizers.') + + return (C1, C2), (Rs1, Rs2), (mu1, mu2), (subquantizers1, subquantizers2) + +######################################## +# Model class +######################################## + +# Named tuple type for LOH codes +LOPQCode = namedtuple('LOPQCode', ['coarse', 'fine']) + + +class LOPQModel(object): + def __init__(self, V=8, M=4, subquantizer_clusters=256, parameters=None): + """ + Create an LOPQModel instance that encapsulates a complete LOPQ model with parameters and hyperparameters. + + :param int V: + the number of clusters per a coarse split + :param int M: + the total number of subvectors (equivalent to the total number of subquantizers) + :param int subquantizer_clusters: + the number of clusters for each subquantizer + :param tuple parameters: + a tuple of parameters - missing parameters are allowed to be None + + the tuple will look like the following + + ((C1, C2), (Rs1, Rs2), (mu1, mu2), (subquantizers1, subquantizers2)) + + where each element is itself a pair with one split of parameters for the each of the coarse splits. + + the parameters have the following data types (V and M have the meaning described above, + D is the total dimension of the data, and S is the number of subquantizer clusters): + + C: VxD/2 ndarray of coarse centroids + R: VxD/2xD/2 ndarray of fitted rotation matrices for each coarse cluster + mu: VxD/2 ndarray of mean residuals for each coar cluster + subquantizer: length M/2 list of SxD/M ndarrays of cluster centroids for each subvector + """ + + # If learned parameters are passed in explicitly, derive the model params by inspection. + self.Cs, self.Rs, self.mus, self.subquantizers = parameters if parameters is not None else (None, None, None, None) + + if self.Cs is not None: + self.V = self.Cs[0].shape[0] + self.num_coarse_splits = len(self.Cs) + else: + self.V = V + self.num_coarse_splits = 2 + + if self.subquantizers is not None: + self.num_fine_splits = len(self.subquantizers[0]) + self.M = self.num_fine_splits * self.num_coarse_splits + self.subquantizer_clusters = self.subquantizers[0][0].shape[0] + else: + self.num_fine_splits = M / 2 + self.M = M + self.subquantizer_clusters = subquantizer_clusters + + def fit(self, data, kmeans_coarse_iters=10, kmeans_local_iters=20, n_init=10, subquantizer_sample_ratio=1.0, random_state=None, verbose=False): + """ + Fit a model with the current model parameters. This method will use existing parameters and only + train missing parameters. + + :param int kmeans_coarse_iters: + the number of kmeans iterations for coarse quantizer training + :param int kmeans_local_iters: + the number of kmeans iterations for subquantizer taining + :param int n_init: + the number of independent kmeans runs for all kmeans when training - set low for faster training + :param float subquantizer_sample_ratio: + the proportion of the training data to sample for training subquantizers - since the number of + subquantizer clusters is much smaller then the number of coarse clusters, less data is needed + :param int random_state: + a random seed used in all random operations during training if provided + :param bool verbose: + a bool enabling verbose output during training + """ + existing_parameters = (self.Cs, self.Rs, self.mus, self.subquantizers) + + parameters = train(data, self.V, self.M, self.subquantizer_clusters, existing_parameters, + kmeans_coarse_iters, kmeans_local_iters, n_init, subquantizer_sample_ratio, + random_state, verbose) + + self.Cs, self.Rs, self.mus, self.subquantizers = parameters + + def get_split_parameters(self, split): + """ + A helper to return parameters for a given coarse split. + + :params int split: + the coarse split + + :returns ndarray: + a matrix of centroids for the coarse model + :returns list: + a list of residual means for each cluster + :returns list: + a list of rotation matrices for each cluster + :returns list: + a list of centroid matrices for each subquantizer in this coarse split + """ + return self.Cs[split] if self.Cs is not None else None, \ + self.Rs[split] if self.Rs is not None else None, \ + self.mus[split] if self.mus is not None else None, \ + self.subquantizers[split] if self.subquantizers is not None else None + + def predict(self, x): + """ + Compute both coarse and fine codes for a datapoint. + + :param ndarray x: + the point to code + + :returns tuple: + a tuple of coarse codes + :returns tuple: + a tuple of fine codes + """ + # Compute coarse quantizer codes + coarse_codes = self.predict_coarse(x) + + # Compute fine codes + fine_codes = self.predict_fine(x, coarse_codes) + + return LOPQCode(coarse_codes, fine_codes) + + def predict_coarse(self, x): + """ + Compute the coarse codes for a datapoint. + + :param ndarray x: + the point to code + + :returns tuple: + a tuple of coarse codes + """ + return tuple([predict_cluster(cx, self.Cs[split]) for cx, split in iterate_splits(x, self.num_coarse_splits)]) + + def predict_fine(self, x, coarse_codes=None): + """ + Compute the fine codes for a datapoint. + + :param ndarray x: + the point to code + :param ndarray coarse_codes: + the coarse codes for the point + if they are already computed + + :returns tuple: + a tuple of fine codes + """ + if coarse_codes is None: + coarse_codes = self.predict_coarse(x) + + px = self.project(x, coarse_codes) + + fine_codes = [] + for cx, split in iterate_splits(px, self.num_coarse_splits): + + # Get product quantizer parameters for this split + _, _, _, subC = self.get_split_parameters(split) + + # Compute subquantizer codes + fine_codes += [predict_cluster(fx, subC[sub_split]) for fx, sub_split in iterate_splits(cx, self.num_fine_splits)] + + return tuple(fine_codes) + + def project(self, x, coarse_codes, coarse_split=None): + """ + Project this vector to its local residual space defined by the coarse codes. + + :param ndarray x: + the point to project + :param ndarray coarse_codes: + the coarse codes defining the local space + :param int coarse_split: + index of the coarse split to get distances for - if None then all splits + are computed + + :returns ndarray: + the projected vector + """ + px = [] + + if coarse_split is None: + split_iter = iterate_splits(x, self.num_coarse_splits) + else: + split_iter = [(np.split(x, self.num_coarse_splits)[coarse_split], coarse_split)] + + for cx, split in split_iter: + + # Get product quantizer parameters for this split + C, R, mu, _ = self.get_split_parameters(split) + + # Retrieve already computed coarse cluster + cluster = coarse_codes[split] + + # Compute residual + r = cx - C[cluster] + + # Project residual to local frame + pr = np.dot(R[cluster], r - mu[cluster]) + px.append(pr) + + return np.concatenate(px) + + def reconstruct(self, codes): + """ + Given a code tuple, reconstruct an approximate vector. + + :param tuple codes: + a code tuple as returned from the predict method + + :returns ndarray: + a reconstructed vector + """ + coarse_codes, fine_codes = codes + + x = [] + for fc, split in iterate_splits(fine_codes, self.num_coarse_splits): + + # Get product quantizer parameters for this split + C, R, mu, subC = self.get_split_parameters(split) + + # Concatenate the cluster centroids for this split of fine codes + sx = reduce(lambda acc, c: np.concatenate((acc, subC[c[0]][c[1]])), enumerate(fc), []) + + # Project residual out of local space + cluster = coarse_codes[split] + r = np.dot(R[cluster].transpose(), sx) + mu[cluster] + + # Reconstruct from cluster centroid + x = np.concatenate((x, r + C[cluster])) + + return x + + def get_subquantizer_distances(self, x, coarse_codes, coarse_split=None): + """ + Project a given query vector to the local space of the given coarse codes + and compute the distances of each subvector to the corresponding subquantizer + clusters. + + :param ndarray x: + a query vector + :param tuple coarse_codes: + the coarse codes defining which local space to project to + :param int coarse_split: + index of the coarse split to get distances for - if None then all splits + are computed + + :returns list: + a list of distances to each subquantizer cluster for each subquantizer + """ + + px = self.project(x, coarse_codes) + subquantizer_dists = [] + + if coarse_split is None: + split_iter = iterate_splits(px, self.num_coarse_splits) + else: + split_iter = [(np.split(px, self.num_coarse_splits)[coarse_split], coarse_split)] + + # for cx, split in iterate_splits(px, self.num_coarse_splits): + for cx, split in split_iter: + _, _, _, subC = self.get_split_parameters(split) + subquantizer_dists += [((fx - subC[sub_split]) ** 2).sum(axis=1) for fx, sub_split in iterate_splits(cx, self.num_fine_splits)] + + return subquantizer_dists + + def get_cell_id_for_coarse_codes(self, coarse_codes): + return coarse_codes[1] + coarse_codes[0] * self.V + + def get_coarse_codes_for_cell_id(self, cell_id): + return (int(np.floor(float(cell_id) / self.V)), cell_id % self.V) + + def export_mat(self, filename): + """ + Export model parameters in .mat file format. + + Splits in the parameters (coarse splits and fine splits) are concatenated together in the + resulting arrays. For example, the Cs paramaters become a 2 x V x D array where the first dimension + indexes the split. The subquantizer centroids are encoded similarly as a 2 x (M/2) x 256 x (D/M) array. + """ + from scipy.io import savemat + from .utils import concat_new_first + + Cs = concat_new_first(self.Cs) + Rs = concat_new_first(self.Rs) + mus = concat_new_first(self.mus) + subs = concat_new_first(map(concat_new_first, self.subquantizers)) + + savemat(filename, {'Cs': Cs, 'Rs': Rs, 'mus': mus, 'subs': subs, 'V': self.V, 'M': self.M}) + + @staticmethod + def load_mat(filename): + """ + Reconstitute an LOPQModel in the format exported by the `export_mat` method above. + """ + from scipy.io import loadmat + + d = loadmat(filename) + + M = d['M'][0][0] + Cs = tuple(map(np.squeeze, np.split(d['Cs'], 2, axis=0))) + Rs = tuple(map(np.squeeze, np.split(d['Rs'], 2, axis=0))) + mus = tuple(map(np.squeeze, np.split(d['mus'], 2, axis=0))) + + subs = tuple([map(np.squeeze, np.split(half, M / 2, axis=0)) for half in map(np.squeeze, np.split(d['subs'], 2, axis=0))]) + + return LOPQModel(parameters=(Cs, Rs, mus, subs)) + + def export_proto(self, f): + """ + Export model parameters in protobuf format. + """ + from .lopq_model_pb2 import LOPQModelParams + from itertools import chain + + lopq_params = LOPQModelParams() + lopq_params.D = 2 * self.Cs[0].shape[1] + lopq_params.V = self.V + lopq_params.M = self.M + lopq_params.num_subquantizers = self.subquantizer_clusters + + def matrix_from_ndarray(m, a): + m.values.extend(map(float, np.nditer(a, order='C'))) + m.shape.extend(a.shape) + return m + + def vector_from_ndarray(m, a): + m.values.extend(map(float, np.nditer(a, order='C'))) + return m + + for C in self.Cs: + matrix_from_ndarray(lopq_params.Cs.add(), C) + for R in chain(*self.Rs): + matrix_from_ndarray(lopq_params.Rs.add(), R) + for mu in chain(*self.mus): + vector_from_ndarray(lopq_params.mus.add(), mu) + for sub in chain(*self.subquantizers): + matrix_from_ndarray(lopq_params.subs.add(), sub) + + if type(f) is str: + f = open(f, 'wb') + f.write(lopq_params.SerializeToString()) + f.close() + + @staticmethod + def load_proto(filename): + """ + Reconstitute a model from parameters stored in protobuf format. + """ + from .lopq_model_pb2 import LOPQModelParams + from .utils import concat_new_first + + def halves(arr): + return [arr[:len(arr) / 2], arr[len(arr) / 2:]] + + lopq_params = LOPQModelParams() + + try: + f = open(filename) + lopq_params.ParseFromString(f.read()) + f.close() + + Cs = [np.reshape(C.values, C.shape) for C in lopq_params.Cs] + Rs = map(concat_new_first, halves([np.reshape(R.values, R.shape) for R in lopq_params.Rs])) + mus = map(concat_new_first, halves([np.array(mu.values) for mu in lopq_params.mus])) + subs = halves([np.reshape(sub.values, sub.shape) for sub in lopq_params.subs]) + + return LOPQModel(parameters=(Cs, Rs, mus, subs)) + + except IOError: + print filename + ": Could not open file." + return None diff --git a/python/lopq/search.py b/python/lopq/search.py new file mode 100644 index 0000000..e9d98a0 --- /dev/null +++ b/python/lopq/search.py @@ -0,0 +1,238 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +import heapq +from collections import defaultdict, namedtuple +import numpy as np +from .utils import iterate_splits, parmap, get_chunk_ranges + + +def multisequence(x, centroids): + """ + Implementation of multi-sequence algorithm for traversing a multi-index. + + The algorithm is described in http://download.yandex.ru/company/cvpr2012.pdf. + + :param ndarray x: + a query vector + :param list centroids: + a list of ndarrays containing cluster centroids for each subvector + + :yields int d: + the cell distance approximation used to order cells + :yields tuple cell: + the cell indices + """ + + # Infer parameters + splits = len(centroids) + V = centroids[0].shape[0] + + # Compute distances to each coarse cluster and sort + cluster_dists = [] + sorted_inds = [] + for cx, split in iterate_splits(x, splits): + + dists = ((cx - centroids[split]) ** 2).sum(axis=1) + inds = np.argsort(dists) + + cluster_dists.append(dists) + sorted_inds.append(inds) + + # Some helper functions used below + def cell_for_inds(inds): + return tuple([sorted_inds[s][i] for s, i in enumerate(inds)]) + + def dist_for_cell(cell): + return sum([cluster_dists[s][i] for s, i in enumerate(cell)]) + + def inds_in_range(inds): + for i in inds: + if i >= V: + return False + return True + + # Initialize priority queue + h = [] + traversed = set() + start_inds = tuple(0 for _ in xrange(splits)) + start_dist = dist_for_cell(cell_for_inds(start_inds)) + heapq.heappush(h, (start_dist, start_inds)) + + # Traverse cells + while len(h): + d, inds = heapq.heappop(h) + yield d, cell_for_inds(inds) + traversed.add(inds) + + # Add neighboring cells to queue + if inds[1] == 0 or (inds[0] + 1, inds[1] - 1) in traversed: + c = (inds[0] + 1, inds[1]) + if inds_in_range(c): + dist = dist_for_cell(cell_for_inds(c)) + heapq.heappush(h, (dist, c)) + + if inds[0] == 0 or (inds[0] - 1, inds[1] + 1) in traversed: + c = (inds[0], inds[1] + 1) + if inds_in_range(c): + dist = dist_for_cell(cell_for_inds(c)) + heapq.heappush(h, (dist, c)) + + +class LOPQSearcher(object): + def __init__(self, model): + """ + Create an LOPQSearcher instance that encapsulates retrieving and ranking + with LOPQ. Requires an LOPQModel instance. + """ + self.model = model + self.index = defaultdict(list) + + def add_data(self, data, ids=None, num_procs=1): + """ + Add raw data into the search index. + + :param ndarray data: + an ndarray with data points on the rows + :param ndarray ids: + an optional array of ids for each data point; + defaults to the index of the data point if not provided + :param int num_procs: + an integer specifying the number of processes to use to + compute codes for the data + """ + N = data.shape[0] + + # If a list of ids is not provided, assume it is the index of the data + if ids is None: + ids = range(N) + + # function to index a partition of the data + def index_partition(work): + from collections import defaultdict + + data, ids = work + index = defaultdict(list) + for item_id, d in zip(ids, data): + code = self.model.predict(d) + cell = code[0] + index[cell].append((item_id, code)) + return index + + def merge_dicts(a, b): + for k, v in b.iteritems(): + a[k] += v + return a + + if num_procs > 1: + tasks = [(data[a:b], ids[a:b]) for a, b in get_chunk_ranges(N, num_procs)] + index_dicts = parmap(index_partition, tasks, num_procs) + else: + index_dicts = map(index_partition, [(data, ids)]) + + self.index = reduce(merge_dicts, index_dicts, self.index) + + def get_result_quota(self, x, quota=10): + """ + Given a query vector and result quota, retrieve as many cells as necessary + to fill the quota. + + :param ndarray x: + a query vector + :param int quota: + the desired number of items to retrieve + + :returns list retrieved: + a list of index items + :returns int visited: + the number of multi-index cells visited + """ + retrieved = [] + visited = 0 + for _, cell in multisequence(x, self.model.Cs): + if cell not in self.index: + continue + + retrieved += self.index[cell] + visited += 1 + + if len(retrieved) >= quota: + break + + return retrieved, visited + + def compute_distances(self, x, items): + """ + Given a query and a list of index items, compute the approximate distance of the query + to each item and return a list of tuples that contain the distance and the item. + Memoize subquantizer distances per coarse cluster to save work. + + :param ndarray x: + a query vector + :param list items: + a list of items from the index + + :returns list: + a list of items with distance + """ + memoized_subquant_dists = [{}, {}] + + def get_subquantizer_distances(x, coarse): + + d0, d1 = memoized_subquant_dists + c0, c1 = coarse + + if c0 not in d0: + d0[c0] = self.model.get_subquantizer_distances(x, coarse, coarse_split=0) + + if c1 not in d1: + d1[c1] = self.model.get_subquantizer_distances(x, coarse, coarse_split=1) + + return d0[c0] + d1[c1] + + results = [] + for item in items: + + codes = item[1] + coarse, fine = codes + + subquantizer_distances = get_subquantizer_distances(x, coarse) + dist = sum([subquantizer_distances[i][fc] for i, fc in enumerate(fine)]) + + results.append((dist, item)) + + return results + + def search(self, x, quota=10, with_dists=False): + """ + Return euclidean distance ranked results, along with the number of cells + traversed to fill the quota. + + :param ndarray x: + a query vector + :param int quota: + the number of desired results + :param bool with_dists: + boolean indicating whether result items should be returned with their distance + + :returns list results: + the list of ranked results + :returns int visited: + the number of cells visited in the query + """ + # Retrieve results with multi-index + retrieved, visited = self.get_result_quota(x, quota) + + # Compute distance for results + results = self.compute_distances(x, retrieved) + + # Sort by distance + results = sorted(results, key=lambda d: d[0]) + + if with_dists: + Result = namedtuple('Result', ['id', 'code', 'dist']) + results = map(lambda d: Result(d[1][0], d[1][1], d[0]), results) + else: + Result = namedtuple('Result', ['id', 'code']) + results = map(lambda d: Result(d[1][0], d[1]), results) + + return results, visited diff --git a/python/lopq/utils.py b/python/lopq/utils.py new file mode 100644 index 0000000..9cd40d8 --- /dev/null +++ b/python/lopq/utils.py @@ -0,0 +1,158 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +import numpy as np +import multiprocessing + + +def iterate_splits(x, splits): + """ + A helper to iterate subvectors. + + :param ndarray x: + a vector to iterate over + :param int splits: + the number of subvectors + :returns (np.array, int): + subvector, split index pairs + """ + split_size = len(x) / splits + for split in xrange(splits): + start = split * split_size + yield x[start:start + split_size], split + + +def concat_new_first(arrs): + """ + Helper to concatenate a list of ndarrays along a new first dimension. + """ + arrs = map(lambda x: x[np.newaxis, ...], arrs) + return np.concatenate(arrs, axis=0) + + +def predict_cluster(x, centroids): + """ + Given a vector of dimension D and a matrix of centroids of dimension VxD, + return the id of the closest cluster + + :params np.array x: + the data to assign + :params np.array centroids: + a matrix of cluster centroids + :returns int: + cluster assignment + """ + return ((x - centroids) ** 2).sum(axis=1).argmin(axis=0) + + +def load_xvecs(filename, base_type='f', max_num=None): + """ + A helper to read in sift1m binary dataset. This parses the + binary format described at http://corpus-texmex.irisa.fr/. + + :returns ndarray: + a N x D array, where N is the number of observations + and D is the number of features + """ + import os + import struct + + format_code, format_size, py_type = { + 'f': ('f', 4, float), + 'i': ('I', 4, int), + 'b': ('B', 1, float) + }[base_type] + + size = os.path.getsize(filename) + + f = open(filename, 'rb') + D = np.uint32(struct.unpack('I', f.read(4))[0]) + N = size / (4 + D * format_size) + + if max_num is None: + max_num = N + + f.seek(0) + A = np.zeros((max_num, D), dtype=py_type) + for i in xrange(max_num): + for j in xrange(D + 1): + if j == 0: + np.uint32(struct.unpack(format_code, f.read(4))) + else: + A[i, j - 1] = py_type(struct.unpack(format_code, f.read(format_size))[0]) + f.close() + return np.squeeze(A) + + +def save_xvecs(data, filename, base_type='f'): + """ + A helper to save an ndarray in the binary format as is expected in + load_xvecs above. + """ + import struct + + format_code, format_size, py_type = { + 'f': ('f', 4, float), + 'i': ('I', 4, int), + 'b': ('B', 1, float) + }[base_type] + + f = open(filename, 'wb') + for d in data: + + if hasattr(d, "__len__"): + D = len(d) + + f.write(struct.pack('\xc2\xb6?\x1a\x12\x0b\x8c\x85*\xef?\x80#\xc4\xac|n\xad?\xcb\xebt\xdf\xc9W\xe3?\xe8\xcd 7W\xec\xd2?\xda.+C?\x15\xdc?\x1c\x0e\xedP\xb3\xc3\xe6?\x88\x14m*\xe8\xc9\xcd?bD\xd8\xcd\x8b\xbf\xed?,\x89\xd1$\xd1#\xd3?\x8fC\x19C\x9e\x91\xe8?\x19\xa5\xa6`\xb9\x8c\xe3?\xc4\xbe\x9d^"V\xc0?4KC\x92\x0e\x1b\xc0?\xe6\xff\x14\x0e\xddD\xd3?|&&\xfc\x11\x00\xd8?\xc00\xa1\xa1S\x8d\xea?\xc8\xfbi\x96\n\x1d\xe5?\x94\xe3\xac\x9a\x06\xec\xe0?j)\x0c\x88\x06\x1c\xed?\x98\xb2I`\xd5"\xec?\x9c\xa1i\xed\x06\xd3\xe5?\x96\xb6~d\x92\x91\xde?\xe6+\xdf\xb1\xf38\xec?o19#\x9d~\xe7?\x14`\xe0\xf1?\xbe\xe3?\xd8\xba\xfc\x13\xd6\x97\xbe?\xedV\xda.QH\xe9?\xbd\xe2\x8cG]\x94\xef?\xf06\x8fW\xe6U\xb9?`\x08\xe0 \xf7\x8f\x9d?\xeb\x81\xf6a\xcf\x1b\xd4\xec?\x97\xb6\xa4\xf5|E\xe6?\xfa\x18\xa2\xd4\x02\x99\xee?\x8e\x9c|z\xd2\\\xee?\xdaa\xfc\xdc.(\xde?\xe5q\x88\x01\x9a\x97\xe1?\x98\xab\x1c1r\x1b\xd3?\xe4\xb3\x806\xd0Q\xe8?\xfa\x7f\x9fb\xac\x83\xed?|\xbc2y\xe9\xea\xe3?\xa4\xb8f\x15Rw\xe7?\x08\xc9\xac\xc1\x0b\x93\xb9?:-\xddqUS\xed?l\xde\x92\xb4\x8e\xba\xdb?\x882\xaa^\xa2\x93\xc7?\x105.\xb1<\xa5\xdc?g\xf0Z\xe4\x95\x8b\xe4?\x0e\x18\xf7\xe3\xf0\x19\xe6?\xb0\xffH\x98\x98\xbc\xcd?\xa5\x00\t\x02\xd5\xfa\xeb?\r\x10=\xbcx{\xe9?\x86e\xf3\x1b\xf0\x84\xef?1]\x1b\x0bw\xfe\xef?\r\xdd\xa4K\r>\xec?I\xea\xe1k\xdb\xdd\xe4?~\x1bU\x9c\xfb\x87\xd6?\xc0|\x83\xc3]J\xa9?\xc0\x9d]\xf8_l\x91?=W\xd0\xde\xf6-\xef?"\xe4\xaf\x9a\x89\xa9\xd3?\x00^\xd4\xff\x1a\x11\xc6?\xce\xb0\xcfk\xd5\t\xdc?q\xc7\xa5z\xc8b\xe3?\xc2\x05\xb9\xf24\xd0\xd3?\xd2B$\x9c\n\xe2\xe0?0\xe6\xfb\x84L@\xd9?\xac6gqR}\xc3?\xe8d\xea!{\xbb\xc7?\xe2\xb0\xc7\rqy\xd0?\xca\x92\x13\x07H\xcd\xd3?\xb85\x17\xa5\xc2\x1f\xb1? 6\xed\xdf\xde\xc7\xa9?8\t\x03)\xc6x\xef?P4Hr\xa9\x16\xd0?\xe0q\x9dD\xe9$\xe0?)C3\xde\x8b0\xe2?\x9aL\x93\xfaj\xa2\xd8?\x0c#\x07\xc31Z\xce?\xf8\xb3\xb8B:2\xd7?\xbb\x8f\xc9\x92G\x8e\xee?\xf4#\x08J\xe4\x84\xc6?>\xc7\xedQf%\xed?\x00\xf0{bp\x97,?X\xcf\xfc\xb3\x11\xcc\xe8?\x85\xa0\xc9s\r\x14\xe9?k\xc0}h\xcd\xa2\xe6?\xa9\xbcI\xbf\x85N\xef?\xc2}\xd3\xf50A\xe4?\x9c\xf0\xe0\xc9i\x8f\xd1?\xac-D\x13\x82\x9b\xcc?\x8cr\xc7 7\x15\xed?\xa3S8p\xa0\xc5\xe4?\x18p\x13\x1c\xaa{\xd3?h\x05-c\xf4s\xe7?\xa8\xd9\xcf\x0fu\x04\xbc?\xcbX\rm@\x05\xea?\xd0Z\xb3\x9eqp\xca?\x02\xae^\x08\x7f`\xd3?\xb0\xa1|\xc1[\xfe\xe8?0\xdc\xf7\xec\x00\xf9\xaf?\xce\xac\xb2o\xe4\xa4\xea?\xcc\x1d\xc2"\xad,\xc0?\xb0\x14\xd9g\'\x14\xd8?q \xa4\xd5\x15\xea\xe2?\x8b\xa3p\xc6@\xd8\xe6?\xb4\xe2\xc6\xf6W\xa1\xe6?\x96B7R\x95\xf5\xd7?4?\xe4/\xb8\xa3\xce?i\x88\x9e\xe6\xcc\x8e\xe8?\x99V\xc7g\x9f\xeb\xe7?\xc4t\xabQ\x86\xd5\xce?N\xd3\xd5\x11\xb7\x07\xe0?\xf0\xec\xb5\xd1\x8e;\xce?\x1f\xcenq\x1b&\xe5?\xe4\xfd\xaag\x13\xab\xd1?\x0c5\x920;|\xc9?y\xe1\xa7}\xaf8\xee?H\x1ao\xd0\x7f\xfb\xba?\x18\x13~\x8a\xc1\xe8\xef?\x19\x8e\xc2>\xb3\xf6\xee?0fh\'\xbb\xd5\xca?\x90\\\xfb\x8f\xf1\xb0\xbb?m\x0fA\x95\x10\x1b\xec?J\xbbC\x7f\x88\x8f\xe9?M\xde\xf2\x9aL\x9d\xe0?Ng"\xe2w\xcc\xee?\xd8K\xc2tz\x06\xc6?\xa7\xe3\xc1`\xb5\xb1\xe0?.[Y\xdeV\xd2\xd4?3\xd90\x1ePE\xe1?\xfc\xbf\xac_t\xad\xcc?\xe9\xa9\x06\x18\xb0\xac\xe7?j\xed-r@\xa7\xd8?\xea\xb0E\x91\xc7u\xdd?\xd4\xd8l\x87\xa2\xf4\xe2?\xf3a0\x92"\xed\xe3?\x0e\xde\xa4\t\xf0Q\xea?M\xd58\x80 )\xe7?W\xf5!\xb6\xe2+\xe7?h^\x15\xa8\x8e\xf8\xb0?.\xc4\xfc*\xf5\xf9\xd7?\xd6\xe0k)\xb52\xd9?0\xbc\xab\xd2\x91n\xeb?\xef_yI\xe6\xcb\xe1?**\xac\xa0\xa7)\xe8?\x14\xa4\xfb\x16\x1d\xe9\xe3?\x14}!\xc3\xa5\xa3\xdd?\xb6\x8f\x1c\xc2\x96B\xe1?\xad\xa8\xd7\xcd\xa3\x90\xe9?\xc0QJ\xfb\xe50\x84?\xba\x91\x99T+_\xef?j\x84\xc2\xa9I\r\xd9?\x1bz\xdfS\xdbZ\xe5?\xe0&\x14\x08P\x1d\xa4?hI\xc53o\x87\xc5?`\xeb\xa0\x98j\x16\xe9?\x08\x17*z\xfb\xe1\xd4?M?\x91z\xd5\x11\xec?0\xa8\' s\x85\xb3?*\x19\xf4\xa2\xaea\xee?\xb0\x0e|\xb7\xbb\xf1\xac?a\xd7\x0c\x91\xafI\xed?:\xc4[\x8b\xf6}\xd7?\'\xfe\xa6\xce}\x02\xed?\x08\xd4\xa1\xdfBD\xef?\xf0\xbd\'+#\x92\xc2?\x0c2V\xee\xcf\xe1\xec?d\xc9\x88Q_\x1a\xd0?\xe3\x80\xda\xeb\xe8\xaf\xe5?\x81\xa3\xca\x84\x9a\xf9\xe9?_\xc6\xdf\xc6\x94\x8d\xe3?\x9a<\xc8\xfd\x9c\xa0\xdc?5\x89\xfe@\x95\xdf\xec?\xb41\xd96\xd5\xe3\xc8?\xd9}\xe0\xdbA|\xea?\xa0\xa0\xce\xb0V}\x9f?F@\xfa\xfe\xacw\xe6?\xdcP\xcd\xa6V\x1c\xcf?\x0c3\n% \x9f\xce?' +tbg1 +(g2 +(I0 +tS'b' +tRp6 +(I1 +(I10 +I8 +tg5 +I00 +S'\xf6\x14\xb9\xf3\xc3\xd9\xd2?\x81\x8ccT\xe55\xea?h4\x07Z\x1d\x92\xe8?\xc8kF\x7fJ\xc0\xd0?`# L\x809\xde?\xb9266\\G\xe7?\x86\xbe#\x11\x93\xb0\xef?\x80Cvl$\xd6q?\xc0\x06I\x0bR\xca\xbf?\x15\xf67\x11\x93\x8a\xe5?\xb8\x1a\xb4\xd4\xe3\xbf\xc4?N\x89\xca\xf3\x01\xd5\xdf?\xc0\xfc\x0e%\xcc)\xbc?\xe6\xcb3Z\x91=\xe0?"G\xddKz\xb7\xe1?\xd2\xa5\xb0\x8d&\xa9\xd7?\xf8\xc3\\\x03t\xf7\xe7?\x18\xb4\xfc\xc5\xd0:\xe6?8\x1fn\x90\x12R\xb3?\x97\x9f\xe1\x023\xb1\xe6?\xb0\xb5\xa6<\x9fz\xc3?T\x9f;\xa0\x90\x85\xd1?,\x1f~\x82\xd6D\xdd?\x0bJ\xd7F\xf6\x81\xef?\xc6Q\xf5|\xbfW\xd5?\xf4\xe1\xa8z1\xc0\xca?\xdf#\xb5a\xe4\x7f\xe5?\xd8Q+\xb4\x7f4\xbd?0\xf5\xc4g+\x87\xce?\xf6\x9faU\xbb\x8b\xe4?k\xeag\xf44q\xef?\xde\xdcN\x97q\xb1\xe2?Yx,\x98<\xb2\xe3?k\xd0Q]\xd4v\xe6?\xa2.1R\xd6\xdb?^}\x0c\xbd\xa9T\xda?*\xc0\xc4d=,\xeb?\x13\xd9\x94\xba\xa2*\xea?h\xb52$\x7f\xff\xe1?\\[\xdd\x94\x9e\x8a\xe2?\x18\x93\xf9\xcc\xfb\x18\xbe?\x10w\xc6]\x00%\xd4?\xd8\x8d6\x89\xb8V\xc8?\x93\x05v\x1e\x1dx\xec?\x10\x07\x83F\xfa\xd2\xdf?\xb0\x83I\xc7\x83Z\xda?\x00]k\xc2\x0b\xe4\x97?\xb3W\rg\x92\x97\xe3?\xc3\x95\x01\xc3\xf7=\xea?\xf9JJ\x97\x9a\x8c\xe6?G\xf0\xf22#\x85\xe2?\xe4\xb34\x1d\x11\xac\xdd?\xff\xaf\x86\xbd{N\xe8?\x8b?\\Zl\xbc\xe8?\xd0\xe9\xc7 \xb4J\xdd?f\xd7\xef\xa8\x90x\xe4?\xa8W)\x858#\xc1?\xaf\x99\xb1\x04\xbc\xef\xee?' +tbt. \ No newline at end of file diff --git a/python/test/testdata/test_accumulate_covariance_estimators_output.pkl b/python/test/testdata/test_accumulate_covariance_estimators_output.pkl new file mode 100644 index 0000000..ad65fc3 --- /dev/null +++ b/python/test/testdata/test_accumulate_covariance_estimators_output.pkl @@ -0,0 +1,80 @@ +(cnumpy.core.multiarray +_reconstruct +p1 +(cnumpy +ndarray +p2 +(I0 +tS'b' +tRp3 +(I1 +(I10 +I8 +I8 +tcnumpy +dtype +p4 +(S'f8' +I0 +I1 +tRp5 +(I3 +S'<' +NNNI-1 +I-1 +I0 +tbI00 +S'\x8c@\x08\x1a\x18[\xa1?\x9e\x10\xefH\x87\xa9\x82?\xeef\xd1\x92K\xfb\xa3\xbfM\xeb\x8fO\xfe\xa9\x9e?\xe0\xf8\x9dA\x9dx\xb3?E\xb28\xd9\x04\x82\xa1?\xe4\xd1\x9c\xc5I\xb6\xbe?R\xe8o\x10\xcb\x83\x95\xbf\x9e\x10\xefH\x87\xa9\x82?]G\x8f\xd5\xff\x82\x84?\x96l\xcas\x860\x8f\xbf\x90\xcd4l\'\x96~?\x1aL\xd3\x99\xcd\xa8\x87?\xb4\x04-\tdD\x8f\xbf\x8aNS\x92\xef&\x82?L\xab\x8cr\xabma\xbf\xeef\xd1\x92K\xfb\xa3\xbf\x96l\xcas\x860\x8f\xbf\xfeR\xa5=tC\xb0?.y\x9eY@W\xa3\xbf\n\xd3\xe5>\x1c\x14\xb9\xbf\'\r\xfe\xff\xba5\xad?bm\x9f\xae\xd7S\xb6\xbf/6q\x81\xef;\x8f\xbfM\xeb\x8fO\xfe\xa9\x9e?\x90\xcd4l\'\x96~?.y\x9eY@W\xa3\xbf1\x1c[H\x86\x03\x9c?i\xdc3\xbb\x84U\xb2?\xe6mnK\xac\xc9\x8a?\x00[W\xa5\x7fj\xb9?\xe6\x7f\xd6\xd5\x0f\xb1\x84\xbf\xe0\xf8\x9dA\x9dx\xb3?\x1aL\xd3\x99\xcd\xa8\x87?\n\xd3\xe5>\x1c\x14\xb9\xbfi\xdc3\xbb\x84U\xb2?\xbb\'q\xff E\xc9?\x00\\-\xd1\x80-\x97?\xbe\xc7 \xff\xe3\x9d\xd0?\x17\x9d\x81@\x9c \x8b\xbfE\xb28\xd9\x04\x82\xa1?\xb4\x04-\tdD\x8f\xbf\'\r\xfe\xff\xba5\xad?\xe6mnK\xac\xc9\x8a?\x00\\-\xd1\x80-\x97?.:+\xd0}\x19\xe1?\xd1\xd1#2\x9a\x9f\xd8?\x18\x03\x98\xf8\x1f\xe6\xcc\xbf\xe4\xd1\x9c\xc5I\xb6\xbe?\x8aNS\x92\xef&\x82?bm\x9f\xae\xd7S\xb6\xbf\x00[W\xa5\x7fj\xb9?\xbe\xc7 \xff\xe3\x9d\xd0?\xd1\xd1#2\x9a\x9f\xd8?\x19\xe7\xaf>L\x97\xe2?\xe4\xa2\xc68N\xf7\xc5\xbfR\xe8o\x10\xcb\x83\x95\xbfL\xab\x8cr\xabma\xbf/6q\x81\xef;\x8f\xbf\xe6\x7f\xd6\xd5\x0f\xb1\x84\xbf\x17\x9d\x81@\x9c \x8b\xbf\x18\x03\x98\xf8\x1f\xe6\xcc\xbf\xe4\xa2\xc68N\xf7\xc5\xbf\xd7+\xf1\xdf\xf4u\xba?P\x11\xe7\xfe\xf7\xbb\x9f?bj\x0f\x1c9\xf8\x91\xbfG\xb6*\xfe\x19\x8e\x87?\xe6Of\xd8\xc9 \xb4\xbf00\x12d@\xddT\xbf\x8f\x8c\xf6\xaf\xe0S\xb0?\xfe\x03#\xf0\x8a\x04\xb2\xbfG\xd5\xd4\x9b\xe7\xfa\xa0?bj\x0f\x1c9\xf8\x91\xbf^\x1dum(\xe1\xd2?Fa\x91\x017\xab\xc3\xbf\xcd\xc8\xef\x176\xfb\xb4?"V!?\xf2\xa9\xb9\xbf\x8e\xf2t\xfa"\x99\xcd\xbfK:NA\xd1\xcb\xbb?\x9d\x9b}\xb0(\x16\xb0\xbfG\xb6*\xfe\x19\x8e\x87?Fa\x91\x017\xab\xc3\xbf\xbcS\x96O\n\xb4\xc0?\x7f\xa7\x93S\xa3\xbf\xfeG\xe8"G/s\xbf\xe8\xb5\x0eOx\xfdw?)\x1c3\xa9\xd3\xec\x99\xbf\x7f\x84h\x94\xb1\xf7\xbf\xbfN\x96\x81\x00qr\x91?\xc0\xc8\xd7\xcc\xde\x91\xb0?\xb6*\x0bp#\x93\xc3\xbf\xf0\rV3\xc8D\xc5\xbfR\x1be\xc2u\xb0\xbe?D\xd6)\xffn[\xa1?\x1c\xbdI\xb0\xdf\xd5\xb4\xbf\x02\x8b\x00\x8c\x11\xb8\xd2?\xc2\r\x1f\x06\xd7z\xb2\xbf\xb6*\x0bp#\x93\xc3\xbf\xb6\xdfb\xcaw\xe9\xdf?eL\xa5\'\xa9A\xd2?\xfc\xf3\x80\xd3/K\xbf\xbfx\x89\x97*\xa4\x12\xaf\xbf6P\x81\xfd\xa9\xe4\xc4?dX\xc8\x8c\xa5M\xe0?\x10\xa7\xdc>\x93S\xa3\xbf\xf0\rV3\xc8D\xc5\xbfeL\xa5\'\xa9A\xd2?\xbe\xe2\xaf\xa2\x1b\x95\xe2?\xb5\x11(\xa5\'\t\xdc\xbf\xc6\xcb\x80\xfc\xd8\x88\xbb\xbf\xd0\r\xad\xcb\xcbY\xd1?n\x19<\xf4\xb7\x80\xcf\xbf\xfeG\xe8"G/s\xbfR\x1be\xc2u\xb0\xbe?\xfc\xf3\x80\xd3/K\xbf\xbf\xb5\x11(\xa5\'\t\xdc\xbf\xf2\xc6$\xdc\x80O\xda?\xb36\xaf\x96\x98\xfc\xb5?"N^\xe1I\xfa\xc7\xbf\xb1\x03$N\'|\xb4\xbf\xe8\xb5\x0eOx\xfdw?D\xd6)\xffn[\xa1?x\x89\x97*\xa4\x12\xaf\xbf\xc6\xcb\x80\xfc\xd8\x88\xbb\xbf\xb36\xaf\x96\x98\xfc\xb5?\xec\xdd\n\xe8\xd5h\x95?\x907\x1a*2x\xa9\xbf~\xe1]\xdda\xcd\xd0?)\x1c3\xa9\xd3\xec\x99\xbf\x1c\xbdI\xb0\xdf\xd5\xb4\xbf6P\x81\xfd\xa9\xe4\xc4?\xd0\r\xad\xcb\xcbY\xd1?"N^\xe1I\xfa\xc7\xbf\x907\x1a*2x\xa9\xbfv\x03d[\x13\xb9\xc0?\xedw\xe7+\xb5\xf8\xa2?\x0b\xe4\xab!n\xcf\xa2\xbfW\x14_\xfc\xfd\xc4\x96?\xf2\xa2\xc9R\x8f\xc4\xa2\xbfl\x8a.\xf7\x04\x93\x96?HA\xbe0i}\xb0\xbf\xdb\xb7\x02\xdd\x15\xf5\x87?\xabJ\'\xad\xb6\xd3\x95\xbf\x0b\xe4\xab!n\xcf\xa2\xbf\x87j\xb0K\\\x94\xac?\x18$(\xb9"\x90Q\xbf]uB^\x8a[\xb7?\x00\x0cCc\xff\xfc\xa3\xbf\xdcZ\xfd_U\xb9\xbc?\xd8\xcb\xa8\xef\xb5\xc9\x9e\xbf~\x1e4\x1e\x98\xf1\x97?W\x14_\xfc\xfd\xc4\x96?\x18$(\xb9"\x90Q\xbf\xd9\x1d=\xe4\x19\xb0\xb0?S\x05\x80\xce\x95\xb5Z?\xe2\xa6r5\x9a,\xa6\xbf~\xf2\xed\xb4:\rz\xbfT\xcaRK\'\xe8\x92?$\xf55\x15\xed\xd3\x80\xbf\xf2\xa2\xc9R\x8f\xc4\xa2\xbf]uB^\x8a[\xb7?S\x05\x80\xce\x95\xb5Z??\x06\x8b\xf6i$\xce?hn\xc9\x81\x88|\x98\xbf\x1e\xb2\x13Y\x94\xce\xcc?\x91\x02\xba-\xdf4\xba\xbf cC\x9d[k\x99?l\x8a.\xf7\x04\x93\x96?\x00\x0cCc\xff\xfc\xa3\xbf\xe2\xa6r5\x9a,\xa6\xbfhn\xc9\x81\x88|\x98\xbfi\xddj\x98\x1e\xe7\xb3?\xd8V%\xe2S:\xac\xbf{\xae\x97Jn\xff\x91\xbf\xccy\x85\xf2\xd8\xe1\x91\xbfHA\xbe0i}\xb0\xbf\xdcZ\xfd_U\xb9\xbc?~\xf2\xed\xb4:\rz\xbf\x1e\xb2\x13Y\x94\xce\xcc?\xd8V%\xe2S:\xac\xbf\\\xa1\xe4\xdc\x00\x82\xcf?\xe2\x9a\xca^\xc5\x80\xb6\xbf`\xed\xd2\x0c\xc6\x16\xa5?\xdb\xb7\x02\xdd\x15\xf5\x87?\xd8\xcb\xa8\xef\xb5\xc9\x9e\xbfT\xcaRK\'\xe8\x92?\x91\x02\xba-\xdf4\xba\xbf{\xae\x97Jn\xff\x91\xbf\xe2\x9a\xca^\xc5\x80\xb6\xbf?\xe7CS\x94:\xac?\x0b\x9f0F\xfc\xdfz\xbf\xabJ\'\xad\xb6\xd3\x95\xbf~\x1e4\x1e\x98\xf1\x97?$\xf55\x15\xed\xd3\x80\xbf cC\x9d[k\x99?\xccy\x85\xf2\xd8\xe1\x91\xbf`\xed\xd2\x0c\xc6\x16\xa5?\x0b\x9f0F\xfc\xdfz\xbf\xbbiCy\xf9\xf7\x89?Q8\x18Z\x9c\x06\xc2?\xb9\xeaO\x0c4\xca\xbc?( \xa9\xf6\xeb,\xa6\xbf)8X\x9cx\xf0\xbc?9Ec\x90\x97r\xc4?\x8e\xe2K\xee)\xcd\xb8?\n\xe3l\xf0\xff\x9a\xa5\xbf\x89\x1e\x1c\xb7^\xe6\xa1\xbf\xb9\xeaO\x0c4\xca\xbc?\x03\xbe\xc0p\xa4\xfd\xb6?\x0cCs o\xb5\xa1\xbf\xce\x9d\xf2\xb43\x1c\xb7?\xaf!X\xa43T\xc0?\n\x18\xe3\xa6D\xce\xb3?>\xcd0\xa1\xe7@\xa1\xbf\x0f\x9e\xc6\xca\xb5\x96\x9c\xbf( \xa9\xf6\xeb,\xa6\xbf\x0cCs o\xb5\xa1\xbf!\x89Av\xc8G\x8b?\xfe\x14\x8f\x05\xf9\xcc\xa1\xbfnG\xc5]\xa0\'\xa9\xbf\xa9\x1bI}\xc7\x82\x9e\xbf\xd5e\x81\xdbD\x94\x8a?\x9f\xc6TJB\x05\x86?)8X\x9cx\xf0\xbc?\xce\x9d\xf2\xb43\x1c\xb7?\xfe\x14\x8f\x05\xf9\xcc\xa1\xbf\xb0\n\xec\x97\xeb:\xb7?T\xdf\'\x04\xe8i\xc0?\x15\xf8\xf7\x1c\x98\xe8\xb3?je\xea\xa1\xd6W\xa1\xbfb\xa7\xcb\xe8\xb5\xbc\x9c\xbf9Ec\x90\x97r\xc4?\xaf!X\xa43T\xc0?nG\xc5]\xa0\'\xa9\xbfT\xdf\'\x04\xe8i\xc0?px\xa8s\xde1\xc7?\xaf\xcf!x/"\xbc?\xd4\x1a\x16~\x19\x82\xa8\xbfe\x1dN\t\x05N\xa4\xbf\x8e\xe2K\xee)\xcd\xb8?\n\x18\xe3\xa6D\xce\xb3?\xa9\x1bI}\xc7\x82\x9e\xbf\x15\xf8\xf7\x1c\x98\xe8\xb3?\xaf\xcf!x/"\xbc?Y\x1dYP\xda\x0f\xb1?\xee\x10\xe3\xf9\x01\xba\x9d\xbf\xeaG\xca\xe4\xcd\xa0\x98\xbf\n\xe3l\xf0\xff\x9a\xa5\xbf>\xcd0\xa1\xe7@\xa1\xbf\xd5e\x81\xdbD\x94\x8a?je\xea\xa1\xd6W\xa1\xbf\xd4\x1a\x16~\x19\x82\xa8\xbf\xee\x10\xe3\xf9\x01\xba\x9d\xbf \xa0\xd8\x83^\xe5\x89?\x19l\xa0B[t\x85?\x89\x1e\x1c\xb7^\xe6\xa1\xbf\x0f\x9e\xc6\xca\xb5\x96\x9c\xbf\x9f\xc6TJB\x05\x86?b\xa7\xcb\xe8\xb5\xbc\x9c\xbfe\x1dN\t\x05N\xa4\xbf\xeaG\xca\xe4\xcd\xa0\x98\xbf\x19l\xa0B[t\x85?\xa2\xadz\xbeZ\xc6\x81?\xc6\n\xac\xa5E]\xbc?$\x97?c\xbc\xd9\xc3\xbf&B\xbd;\xce\x85\xbe?\x08\xc2\xbe_\xdc\x06\xa4\xbf\xff\xc2\xeb|\xaa\x93\xb5\xbf\xb5T\xcb\xa51\xde\xac\xbf\xf2\xec\x95\x05o\n\xb7\xbf\x98\x1f8\x8e\x85\xd6\x81?$\x97?c\xbc\xd9\xc3\xbf\xa0\xceO\x9dD\x11\xd7?@#G@\x97\xe6[?\x80\xc71\x9b\xc7\xeaS?\x81f.k\xb2\t\xb0?*9\x17\x15\xf1\xce\xc5?o6\xa7\x06\xff\xc0\xb6?\xc6\xbb f\xf4\x8f\xa1?&B\xbd;\xce\x85\xbe?@#G@\x97\xe6[?J\x98"\x86n@\xd9?\x14\x9d|V\xa9\x96\xc6\xbf\x0b\xd2\x10K\xa8\x03\xb4\xbfj\x07w\xde*p\xbd?O\xe6\x0f\xe2\x14\xd1\xb5\xbf.\x00\xd9j0\xe1\xb2\xbf\x08\xc2\xbe_\xdc\x06\xa4\xbf\x80\xc71\x9b\xc7\xeaS?\x14\x9d|V\xa9\x96\xc6\xbf\xd3\xf4\xfe\x07h\xe1\xbb?\x9e\x02\x8c\xa4w\xb4\x9e\xbf\x937\x04\xe8\xdd\xa9\xb6\xbf\xd3\xedf\xdc\xe9\x9a\x86\xbf}\x1d\x91%\xf4\x8a\xc0?\xff\xc2\xeb|\xaa\x93\xb5\xbf\x81f.k\xb2\t\xb0?\x0b\xd2\x10K\xa8\x03\xb4\xbf\x9e\x02\x8c\xa4w\xb4\x9e\xbf\xa7\xb1\x0e\x83k\xfc\xc7?]\x12\x14\x06D\xf9\x96?a\x88\xd5h\x8b\x8e\xdb?\xf7\x19\xca%\xe7\x0e\xd7\xbf\xedG!`\xd5\xc4\xd0\xbf\x10\x9f\xa4\xe3\xf7\x19\x82?\xa0q\xd2\x8f\xd47\xd2?\x9c\x11\xb6\xee\xddh\xc2?l\xdf\x87\xa4C*\xf6?\xc2S\x8a &\x13\xbd?R\x06\x8f\x08\xa1\xee\xb2\xbfp \xf0\xf9R*j?\xe8\x13RVI\x87\xbe?S\xa7\xb3.!L\xb8?\'\x80\x97\x84\x01\xa9\x90?>\x12\x14\x06D\xf9\x96?\xc2S\x8a &\x13\xbd?\xf0\x92\t\xe5\xb1\x1d\xc4?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00mH\xff3\xd1\xbd\xdf?\x9dt\x87-\x90\x14\xda?~V4\xe8LA\xc9?\x8b\xa5\x02\xa2\xe7O\xb4\xbf\xa4h^~\x80\x01\x9a\xbf\xe2\x96.\xcf\xe2\r\xe2\xbfR/\xde\xf6\xbd\x0f\xbd?|4\xd1%f\xe6\xb9\xbf\x9dt\x87-\x90\x14\xda?\x12U\'\x1b\x9f\x1d\xe5?\x80mM\x9e]\x9b\x94\xbft&x\xd6=\xb3\xc6\xbfJ\x16\x7f\xf9\x9f=\x9e?\tr\xd1z\x19`\xe1\xbf\x9e\xb9\xeb\x8e\xa3\xc8\xb4\xbfP\'#\xa8y\x83o\xbf~V4\xe8LA\xc9?\x80mM\x9e]\x9b\x94\xbf\xbb\xceM\x86\xcf\xfe\xe3?\xd4\xf7MjNk\xe0?.\x9c8Vp\x8d\xd9\xbf\x07\xe2\x1f/\xfe\xe4\xd1\xbf\x11\xdeK\xbd<\x15\xc6\xbf\x80\xba\xab\xcf\x7f\xd1\xb5?\x8b\xa5\x02\xa2\xe7O\xb4\xbft&x\xd6=\xb3\xc6\xbf\xd4\xf7MjNk\xe0?-zY_&q\xf2?\x01\xc5\xfa\x0e\rl\xe1\xbf`\xaaW9\x06\xa4\xd1?\xa9FB\xcb\xe4k\xc8\xbf\x90\xfda\x00(\xd4\xc8?\xa4h^~\x80\x01\x9a\xbfJ\x16\x7f\xf9\x9f=\x9e?.\x9c8Vp\x8d\xd9\xbf\x01\xc5\xfa\x0e\rl\xe1\xbf\x06\x83!\x98XI\xdc?\x1a\xba\t_\x9a4\xaf\xbfF6&\x99\xf6\xf9\xc5?T\x81\xd7z\xed[\xc2\xbf\xe2\x96.\xcf\xe2\r\xe2\xbf\tr\xd1z\x19`\xe1\xbf\x07\xe2\x1f/\xfe\xe4\xd1\xbf`\xaaW9\x06\xa4\xd1?\x1a\xba\t_\x9a4\xaf\xbfuZ\xb7\x88\xd4g\xee?\xc6\x9c\x04\xd9\xafK\xa1?\xd4\x8eboE\t\xb7?R/\xde\xf6\xbd\x0f\xbd?\x9e\xb9\xeb\x8e\xa3\xc8\xb4\xbf\x11\xdeK\xbd<\x15\xc6\xbf\xa9FB\xcb\xe4k\xc8\xbfF6&\x99\xf6\xf9\xc5?\xc6\x9c\x04\xd9\xafK\xa1?\x8a\xea\x12\xff\xea\xdd\xd9?N\xdb\xeb`.\xa4\xc7\xbf|4\xd1%f\xe6\xb9\xbfP\'#\xa8y\x83o\xbf\x80\xba\xab\xcf\x7f\xd1\xb5?\x90\xfda\x00(\xd4\xc8?T\x81\xd7z\xed[\xc2\xbf\xd4\x8eboE\t\xb7?N\xdb\xeb`.\xa4\xc7\xbf\xbc\x98\x98\x88\x9a0\xbc?' +tbg1 +(g2 +(I0 +tS'b' +tRp6 +(I1 +(I10 +I8 +tg5 +I00 +S'\xf0_\x02\xa5Z\x95\xcb\xbf\x00\x80\xf1\x85\x94\xd8\xf1>0\xeb\'c\x06]\xb3?\x84\xd6\x10\xde/\xda\xc4\xbfR\xae\x0f\xb0\x0cg\xda\xbf\xe4\xc8\xc7\x9e\x9e\xc7\xf1\xbfr\xa4\xcd\xec;\x1f\xf4\xbf\xde{\x8c\x07\xcd\x08\xdf?0@x\xeb\x9d.\xa7?\xe64a{ \xe6\xe5\xbf\x07\x02\xc2;\xd5\xd6\xe7?\x07\x8a\xccow\x14\xeb\xbfx?t\xe6Qg\x04@ \xb8rC_\x05\xf2?\x00\xb6\xff9\xc9\xda\xc1?\xd8\x8e~\xa7\x04[\xcf?r\xfd\x85\x81\x0e\xd3\xd0\xbf\x8c\xf2&\x80\x00\x1b\xdf?r;t\xfa(\x07\xe0?\x12\x1a\xfe\xa0\xb32\xe2\xbf\xd57&I\xb8b\xf4?@\xe4\xc6\xbd^\xe8\xe7?\xa2\xb67\x12\xe5P\xdd?2LJ\x95\xcc\x00\xf2\xbfZ\xcbn\xa8u\xde\xf4?\\\x89*\xd1n(\xc3\xbf\x8e\x87o\xd0J0\xd9\xbf\xfe\xe7\x85W\xd3~\xec?\\l\x8e\xeaU\xee\xf3?`O#c>8\xe9\xbf\xb4\tm\x85Y\x18\xcd\xbfC$\xdd\xfcp\xc7\xe3?h\xa4\x9e\xe1\xe7M\xc8\xbf\xbc\xc8ce\x14\x1b\xd8?\x00VQI\x1c\xc1\x8c?}\xc1\xd8\x88q\x0c\xea?\xb8\x9c\x18\xa8\xf62\xc7\xbf\xea\xb7\xcd\x89$@\xeb?\x9az\x1b!3\x8b\xd4\xbf\xdc6m\xa2b\x10\xc0?\xaa\x1f\x8c\xd4g\x04\xd8?zx^\xbb\xf2-\xd3?\xf8\xcd\xdb(\xce\x8b\xbd\xbf\xcc\xd0\t\x18qG\xd3?\xb4Puzt>\xdb?8\xb7\xdc.\xbe\x85\xd0?\xd0\x98\x0f\xd1a\xc9\xbc\xbf8\x7f\xe7\xddr\xd9\xb7\xbf\xf6J\x8d\x19\x93\xd4\xd5?\xb3\xca}\x02$\x07\xe9\xbf\xe2SH\xbc\x02\'\xd7\xbf\xb8\xf8W\x88&\xcc\xd8?\xc2w\xce\xed\x8a\x16\xe0\xbf\x8aT\x15\xe1\x03\xad\xec\xbf(C\x8c\xb6\xf0\xf0\xda\xbf12\x96]\xd5\xcb\xee?3\xb7y5\xafT\xf0?\x8c+\x11\x91\xa1\x8b\xd7\xbf\xef1\x83f\xc3\x03\xed\xbf\x1b\xa6\x8a\xe3\xd8\xf9\xba\xbf\xa4\x90\x8dP>\x07\xc7?\x14N\xbc\xb0\xf0\xdd\xd1\xbf@\xd25\xa2MA\xcf?XkW\xbe\x97{\xba\xbf\xe2\x94=&\xefQ\xd2\xbf\xc0Eq\x02d\x87\x85\xbf\xba\x8as\xa6\x98S\xe6?(;\xec\xd1>\x8e\xd5\xbf\xf0,\xa5\xa5\x01\xf7\xb4?\x8c\x07r?\'\xd7\xe6\xbf\xf4\xe8\x10:\x19\x08\xe3\xbf\xc6\x8c3\xd6_\xfb\xda?\x88"(y{V\xd3?p\x81f\x9ae\x08\xa7\xbf\x805\xbc\xac\xc3d}?\x00\xf5\xe6\xb4\x13\x13\xb1\xbfPR\x18\x8eI\x96\xb1\xbf\x18\x81\x8d\x99\xca\x18\xc1?\xa0\xd3\xfa\x84\x15\xfe\xad\xbf\x84.K\xccy>\xe3?\x88\r1\x9f.\x96\xd1\xbf\x80\xfa\xf5\x03#\x10\xd8?\x98r|\xa3U\x15\xb2\xbf\xe0\xf2\x8f\xf7GE\x93?\xf8w\xb0*\xbbp\xb5\xbf\xa0\xbc\x9aYd\xb4\xaa?\xca\xccP^o\xaa\xe2\xbf\x1cJ\x83\xdf\x1a\x0f\xc3?\xa0\x1c\xaao\x05\xea\xb9?TB\xc4\xc0\xd0\xd5\xd7?\x86\x9c\xda`\xd7\xc9\xd4\xbf\xaa\x1f\x8c\xd4g\x04\xd8?zx^\xbb\xf2-\xd3?\xf8\xcd\xdb(\xce\x8b\xbd\xbf\xcc\xd0\t\x18qG\xd3?\xb4Puzt>\xdb?8\xb7\xdc.\xbe\x85\xd0?\xd0\x98\x0f\xd1a\xc9\xbc\xbf8\x7f\xe7\xddr\xd9\xb7\xbf\x80\xccs\xdd\x9cg\xc6\xbf\x80\xb2&\xb1\x84\xb2\x9d\xbfT\xb9\x16\xb6\xff\x08\xcc?\x18<\xc5\xd2\xa1\xd5\xc4\xbf\xda"\x12\xda\x80`\xdc\xbfp\x86\xfd\xc9\x87\xdf\xb5\xbf\xa4\x83xr`x\xe3\xbf\xc0W\x8b:(\xb5\xa8?\xfcc\xe8\xf6\t\t\xe2?\x00\xbe,2oej?\x98\x9b\xe7\xf8-\xe7\xb3\xbf\x08Oms\x92\xac\xb8\xbf\')T\xb4\xb9M\xe2?f\xb9(\xdd?\xdb\xde\xbf\x00\x0b\x08M4\x8e\xb7\xbf\xcc\x86,b\x91"\xcf?\xf5P\x8ai\xa8\xe5\xe0?\xc8\xca\x04\x92\x1cC\xc0\xbf\x0cJ\x8e\xba\x90\xd0\xc9\xbf\xf0\xb5h\xd6\x10C\xb9\xbf\x8ay\xce\x9b\xbf\x9a\xd1?\x80L\xd0\xde\x0ei}\xbfR\xf2\x0c\xdc\x08Z\xd5?*\x9b+^1\xb1\xd1\xbf\xbcY\xedz\xce\x8f\xd8?v\xc9Ph\xa2\xf0\xdc?\x80\x19\xcf\x07\x0f\xd4\xb5\xbf*[%\x10\xe5\xcc\xdf\xbfXh@\xf8}\x1e\xca?\xae\xe3\xf6Fi\x8c\xd8\xbf\xa0(\xa7\xe6\x1d1\xab?\xe8\xffl\nl\x89\xba\xbf@\xf7\xa3\x8eN\x82\xaf?`\xb6{\xfb\x8d#\xab\xbf\x10#*\x15\xde\xdc\xc1?\xc2\xf0\x12\xdc$\xd1\xdd\xbfR\x18\xb7\xc4\xed\xa5\xda?\xa0V\xd7\xb7\x92_\xc5?P\xea\xe9d\x97\xd6\xb3\xbf"\x9a\x84\x94\xe2\xdc\xd6\xbf\x80t\xf5\xff\x1e!\x8e?x,\xba#\x82\xa6\xb4?\x00\x99\x06w\t\x81\xbf\xbf\x00C\xae\x0c\x054\x95?\x00\xa6x;)\xc3a?~)\x87\xc2\xd3\x1a\xe1\xbf\xb0\xa9\xdb\x8e?w\xd4\xbf \xd8\x83T\xda\x1c\xc5?cl&\xbey\x82\xe4?\xe8\x8c\xf4que\xc0\xbf\xe87\xe6\xff\xcco\xb7\xbff\x17N\xa8\xad\xbf\xe0\x1a+\xea\xe9\x03\xb2?\x98\x87B\x02\xce`\xc4\xbf\xe0R5K\xebP\x9b\xbfVDb\xb8\xdd;\xe5\xbf\xc6\xc7R\xb9\xcc\xbf\xd6?d\x01\x86\x82\xe8\xb8\xdb\xbf\\\x04\x94\x8b\xc5\x9d\xc5?\xfc\xdf\xd3\x0c\x81\xc5\xc7\xbfP1\x9a)\xa3\x9b\xaf\xbf\xe4\xda\xeayEi\xc4?\xb0\xf3\xc7\xc7\xdaL\xa2\xbfx\xd2\xc5/j\x03\xbf\xbf\xb2\x81\x04\xa2\xb8\xc9\xde?\x94^\xf3\xb0\xbdj\xca?Hn\xa6\xabv\xa7\xc3?\x001\xa7!\xb1\x1bs?\xe0\xaa7\xde>?\xac\xbf\x80AP\xdc\x86q\xaa\xbf\xe0\xb9\xfbG\xbe\xcf\x94\xbf`\x16\x0bguX\x95\xbf\xc03\xb8x\xdd^\x9d?\xfan\x91\x03\xf1p\xdf\xbf\xce\xe0i?\xef\x14\xd5\xbf\xd6$\xf9\xd5\xbac\xd1?\xb5687e\x98\xe1\xbf\x84\xb5\xac\xdez\xf7\xcf?@\xec\x03\x00\xb5\xa5\x9e?\x02\xe78\x0f\x1do\xd2?\xad\xe0\x98o\x0b\x18\xea?\xf0\xb0\x1d2\xccj\xb0\xbf\x08H\x7f\x1e\x9aX\xd6\xbf\xf0\xd4\xb1\x8c-7\xbb\xbf\xd0\xf1\x8e\x96)\x82\xcd?\xc4\xda"$\x12\x16\xcb\xbf\xdc\x04\xe5n\x06\x10\xcf?@\xf2C\xb1\x10\xa2\x90?\xf4G~\xf1l\xe2\xda\xbf\x88\x97\x92\xf9\xc5Z\xb0\xbf$\xdb8\xa15\xaf\xd8\xbf\x98\xf9\xb7p0{\xbd?\x88\xe6N\x943\x9e\xc4?\xe8\x19\x1b\xacB\x13\xb4\xbf\x14\xaf\xc7\xe9/\'\xd3\xbf\xdc\x9a\xbdK\'\x1f\xc7\xbf,\xb41\x07"\x1f\xc5?\xa0\x1a\xd4\x82}e\xc7?c\x7f\xee^R\xe0\xe2?`\x91>:e\x0f\xcf\xbf\xb0Pg\xc6\x86\xa3\xad\xbf\xfc\'s\xf70\x1b\xd3\xbf\xf4\xa6#~\x86\xa5\xcd?\x12\xef\x8c\xb1!\x08\xd7?\xae\x00\xaf\x89Y\x8d\xdc?\x10y\xe1\x19Y\xb0\xcf?\x90\xe7\xf2X\x16\x8d\xb1?\x08]\xc3\xd5\xfc\xe9\xb7?\x18E\x7f\xb0.}\xb3\xbf\x10\xc2.\x84{\xce\xb8? \xce\xcd\x8fs\xac\xc6\xbf,\xd1X\xad\x1e\xd5\xd4?\xdc\x9d\x91HE\x89\xc1?\xbaQ\x96Wg\xab\xd2?\x80\x15\x81 X\x03\xcb\xbf`,j\xb8 \xe0\xa1?(\xb2\x03\xfc\xb1\x9d\xcb?\xf4\x12\xcd\xcd\xa6\xe3\xd9\xbf\xd8d\xa0\xd0|%\xc4?\xf0\xf2\x0b\x9d\xfd^\xb4\xbf\x80\xc4\x88\xb9da\x7f?:\x7f\xce\xe5\xd0J\xd5\xbf\x00\xd9\x1bp\xd7\xce\x84\xbf\xa2b\xb3w\x08\x86\xd0?\xbc\xe8(-\',\xc4?\xe0?J3\xe4\x12\xd2?\xa8\\\xe1\xc53\xd2\xb1?\xd4I\xd2\xads\xc2\xc8?0\xba\xd5\xcc>t\xb9?rby7A\xda\xd9?\xd6\'\x17\x87^\xae\xd6?X\x07\xef\xff\xc2\xe8\xd7\xbf0h!e\xb8\x87\xc5\xbf(\xe3\xb2\x8e\x03\xa3\xc9?\x00\xcf\xd7\xeah\x8a~?\xd4R\xcdw\x97\x1e\xc6?\xe4\x96\xd94\xa3\xa1\xcc\xbf\x10\xde\xcb\xad\n\xe5\xd4?\x80\xcb\x19w\xaf\xca}\xbfx\xe9\xbe>r\x1f\xbb?' +tbtp11 +. \ No newline at end of file diff --git a/python/test/testdata/test_compute_rotations_from_accumulators_input.pkl b/python/test/testdata/test_compute_rotations_from_accumulators_input.pkl new file mode 100644 index 0000000..74c2d67 --- /dev/null +++ b/python/test/testdata/test_compute_rotations_from_accumulators_input.pkl @@ -0,0 +1,59 @@ +(cnumpy.core.multiarray +_reconstruct +p1 +(cnumpy +ndarray +p2 +(I0 +tS'b' +tRp3 +(I1 +(I10 +I8 +I8 +tcnumpy +dtype +p4 +(S'f8' +I0 +I1 +tRp5 +(I3 +S'<' +NNNI-1 +I-1 +I0 +tbI00 +S'\xd0\xa0\xea?\x02N\xdd\xbf\xa8\x8f\x9b8km\xd8\xbf\x14SFK\x88B\xd3\xbf\xea\xe5\xf6\xdf\xb2\xba\xd4\xbf\xb4;\xa8\xdeg\x8b\xd2\xbf$8\xf7\xc5\x98\x84\xd7\xbf\xfc\xc6\x8fw0\x1b\xd7\xbf\xa3/?\x0fA9\xd4\xbf\xb3S\x1b\xe0>x\xe5?(VU_\x83@\xa8\xbf8\xef_r\xf24\xc5?p\x17\x98\xb7\x9c\x86\xcf\xbfR\xc0\xeeU\x00\x8e\xdb\xbf\xd2\x19\x1e\xca\t6\xc5?jV\xd78\xcf\x98\xdf\xbf\xe0\x80\xc9"\xbc\xb9\xab\xbfl*\x80c\x9cd\xb7?\xbe\xb6z\xfc\xb3\x16\xd9?T/\xf0\x13\xc4@\xe1\xbf\xceTx\\\xb3T\xdf\xbf\x1e\x82(\xab\xd6+\xd5?>Z{\x9bf\x82\xd5?\x80B\xe9\x1c\xa0jy?\x04J\xe1 \xbb\xae\xd2\xbf\xd4h\xdf\xe0\xd4@\xc4\xbf\x95qEM\x94\xdb\xc3\xbf\xaa\xbc\x1c]\xa4\x12\xc8?+3\x8a\x9c8\xa8\xe2\xbf\x00p9\t\xc6\x18\xd0?d\xb4\xb7\xdaOa\xa5?$.\xc6\xb2\x81u\xc0\xbf"\xf9\x82c\x1b~\xe6?[p\xd9\x9a\xad6\xcc?\x0b\x87\xce\x81l[\xdd\xbf\x10\x1e\x93\x05\x99uq?\xe8\xe7\xab\x82\x07K\xd6\xbf\x02\xaa\xf8\xcb\x04\xc9\xcb\xbf\x8a\x13%\xb9\x9d\xc4\xae?\x18\xbdVV\x03\xc4\xe7?\x92\xad\x8aB}\xdc\xc0\xbf\x0ccJHj\x08\xbe?\xcdt\x9a\x00.\xa7\xe5\xbf\xae\x92\x8e\xcf\x02\xf4\xbd\xbfxM\xfe\x0b\xdc\xaa\xd0?Qi\x960\xc3q\xdf?\xba\xa4\xe5\x07\x16\x95\xd7?<\x02E\xadaP\xce\xbfO\xd3u\x85\xb8\xdb\xbd\xbf`\x18U\x97\xda\xe5\xbd\xbfbe\xe9s\xb1\x91\xa9\xbfm\x15\xef\x1ev@\xe2\xbfby\x8f\xd5p\xad\xce?\xec\xd6[\xa5Uh\xde\xbfK\x9f\xbat\x1a[\xd9?O\xffgu\xc8\x03\xa0?\x8d\x92\xf2"\xb3\xd7\xdd?\x8d\x1d~I\x84\xbf\xde?P\x90\xd5\xd1\xf1\x94\xae\xbf\xa4+\x83\x03s\xce\xdd\xbf\x04\xd5\x87\xf0}@\xb7?t\xd0G\xd7\xb7\xc4\xca?V\xe7\xa2\xf5\xb6\xe5\xe4\xbf\xf0\xc21\n\x98,\x9b?J\xfe\xf7\x16\x1b\xe1\xd0?Q(\x03$Z+\xda\xbf\xae~\n\xa3l\x06\xd4\xbfn\xc8sy\x1e\x84\xdd\xbf\xf9i\xd5\xc90F\xd4\xbfQ\xce1\x00\x0e\xeb\xd1\xbf\xc7Z\xb8\xdaTb\xd5\xbf\x05M\x9d\xab\xbe\x8a\xd8\xbf_\xe6h\x00\x9a\xa2\xd2\xbfm\xa6\x08D"$\xc6?@\x18\xb0\xd2\xd4\xa0\xc5\xbf8\xbe\x18wm\xb0\xd7?X\xf6\xb2\xe6\xb7\xd6\xce\xbf\xa3\x98\xaf|\x80\xed\xd6\xbf\x92[W\xa5\xc2\xd3\xc5\xbf.\xe4\xb5.`\xb7\xd8\xbfS\x06_\x13j9\xe5?Fi\x858\xa4\x9e\xe1?\xdeX*\xa7\xce\x1f\xe0?\x82(=\x92kD\xb5\xbf\xf0\\\x0c\x81*)\xb9?\xe0\xa2W\xeb\x18\xe5\xb2\xbf\x88\xdaqr\xab\x04\xe1\xbf\xc22Z\xb7\xfc\xd0\xd0\xbf\xc0\xb1(\x9f\x0f\xd1\xd0\xbfHx\xe5x\xbez\xc4?X0\xb1\xeb\xc5n\xe1\xbf\xf7\xab4W\xfe\xcb\xc5\xbf\x999\xa0\xb2\xbbw\xe5?\x0e\xeb\xb8\xe9\xe2\x1e\xbb\xbfG\xf0\x16\xb4d\xaf\xd6\xbf\xe8j%3\x86\xe9\xca?+L\x8e\xcf\x93\xee\xc0?~\x01"\x1eD\x96\xe2?}\xe8\'\x01P\xeb\xd9\xbf\xa43k\x84\x9e\x83\xa3?E\x19\xa4\xa2\x03J\xdb\xbf\x80\x9a@\x98\x84\xa3\xd0\xbf\x9fZ\x83L\xaf/\xc8?\x8e\xf8\x16\x10\x84\x81\xd3?[\xf0\x84\x9a\xa7\x17\xd6\xbf;`OS\xbf\x1f\xc5?\x84\x9e\xb8\xc8&\x06\xc0?\xa8\xd1w\xb8\xf4\xd8\xe4\xbf@\x19?\xfa\x04\xe2\xd4\xbf\x8az \xde\xcf\xd9\xcc?IL<\xd9\x08>\xbc\xbf\xfc\x9d\xfa\x86yS\xd4?\x87f7"\xf7c\xe0?2f\xb0\xe9n0w\xbf\x03\x07#\xd4\xf39\xd3\xbf\x10\xf8\x92\x85\x80H\xd5?,w!\xdb?\xf2\xd0\xbf\xc3\r\x88\xbd\xac\x13\xe7?\x18\xa2n,\xd6\xc2\xdc\xbf\x80 x\xe9*\xc2W\xbf\xa2\xebx9\xe0l\xb5\xbfER\xd3;\x9f,\xd5\xbf\x0e\xf1\xe7\xbags\xcd?F\xa5\x94\xd7\x9f\xdd\xd1?W\x05~]4\x9e\xc3\xbf\xe5Ko\x17\xb9\xc9\xd7\xbf\x06\x91\xfcO\x1b\xb1\xdc\xbf\x0eFYSR(\xe4?\xe0 ]\xd9\x15-\x90\xbf\x05du\x9b}\xa1\xdb\xbffO7u\xbdS\xd6\xbfs#\xf6\xbbG\x08\xd2\xbf-[!\xdf\xe1\xfe\xd4\xbf)\x16\x9d\x0b;>\xd6\xbf\xd5\xa8-9\x9da\xd7\xbfA\x14\x8e\xf5mv\xd4\xbfOxZ\xcd\xd2\xa7\xd8\xbf\xdb\x10{\x89\xba;\xa2\xbfH\xc9\xfd\xb4\xc0\xb5\xd3\xbf8\xe3\xcd\xb2\xf1\xa2\xbd?\xcav\x01\xdbVu\xc6\xbf\xbf\xd8\xa7\x18\xdc\x06\xe5\xbfP=l\x90(\x1b\xd1?\xd2\xf0i\xcc\x0e\xb6\xe0?\x9c(\xdciG\x86\xd2?@\xd3$\x03\xc3\x90v\xbf)\xbcB\xc8J\x9f\xb4?\xecj\x85\x99\xc8\xa4\xe9\xbfoWm\x06@\xf2\xca\xbf\x0f4\x16\x99J\xa2\xb7?G\x0c\x15\x947\xf8\xd1?\x9c\xa1xRd\xbd\xbf\xbf\x968\xb1\xcc\x88\xec\xdc?\xf7\x90\xc1t\x85\xa0\xc8?\t\x866\xc6n$\xd0?\x03\xb1n#\x0f\x7f\xc7\xbfB\xd5G\xc9\xe5j\xdf\xbf\'(\xcf\xf5/c\xca?\x12z\xce2\xc01\xd8\xbf\x00\x06\xe0v*\xd8\xe3?\x82?\xa7\x1eD\xf0\xcd\xbf\xd1\xfc\x17hq\x11\xe5\xbf\xeb\x98\xeb\x0fT\xa1\xce\xbf\x14\xbf\x9f\xd9P4\xd0?\xc0\x8az\x87|=\xce\xbf7G\x80\xb2\xe9B\xe1? )\xf4x\xb8\xc6\xbb?\x989g\x89\x7fs\xc5?@\\\x9b\xbe\xfe\xf6\xce?\xcc\xa9q\x82\xa3f\xd0?\xd9\xad\xc1\xa7E\\\xe6\xbf<\xfdJ\x8b\xe5\xa3\xd1\xbf\xcc\x8e v\xab\x90\xd2?n\xbc\xd7\xb8\xb7-\xd3?\xb4\x9f\xef\x8e\xd2y\xc6?*\xe0}:\\\x15\xce?n\x19\xbd\x8b\xb3;\xd5\xbfT6\x02\xce\x80\xaf\xd6?t\xed\x1c\xa4O3\xb8\xbf\xa4Q\xccG\xe8\xdc\xd0?\xf6o%.\x8d\xa0\xe4\xbf\xe8\xa7\xde\xad\xe6\xd1\xb5?\x85\xff]\xf5+A\xde?\x8f\xb1\xfc^\xf5\xb9\xd4\xbf\x95\xd27\xe7o\x1d\xcb\xbf+w\xdd\xb3[\xd4\xd8?\x9bx5\xe8\x82\x82\xd9\xbf\x9c\x97\xdb\x00H\xe9\xc2?p\xfd\xbe\xd7\xc1x\xc2\xbf\n\xb6\x0bJor\xb7?\x01Ua\xdf\xf9\xbc\xe1\xbfv\x04\x00\x91\x86\x97\xc8\xbf\xe5]\xc2\x9aRd\xe1?E\xb3\xfb\xef\x01\xb9\xd4\xbf\x84\x8e\x15c\xbc\xb9\xd7\xbf}\x9b\xaa~\xa6j\xcf\xbfQ7L/\xcb\x00\xd8\xbfX\xef8*\xd9C\xd8\xbfb\xb1\xe1t\x1a\x08\xd6\xbf\xe4\xff\xbdu4O\xda\xbfI\xca\xaf\x18.\xae\xd6\xbfc\xa6\x87\x8cR\xb3\xd5?\xfa\xef\x93\x83\x1a\x98\xd9?\xc4\xc6l\xc7\x04;\xc7\xbf\xb0\xadp\x08E\'\xd0\xbf\xf8"\xe5\xcd\x06\xa9\xc2?x\x80\xf7Pl\xa9\xe0\xbf\xd4\x93\x0f\xc5\x88\xae\xd8?r\xaaJH\x10\xb4\xdb\xbf&0\xc3\xcd\x8f\xc9\xcb?J\xd2\x81\xda\xb5\x06\xe0\xbfN\xc6\x8e\x96\xda\xff\xcc?\x8b\xa4\xc1\xd1\r\x8e\xd1?\xc0w\x88\xc3\xe0S\xdd?\x05\xffRDU\x91\xb7?D\xdf\xa3N\xe2\xa2\xb8\xbf\x96\x08\xbf>\xee\xde\xe2\xbf=\x9bR\x04\xf3&\xa2\xbf\x85\xa1\xf4\x8az\xfa\xdd?N\xc8\xe3\xe4\xab\x12\xb4?\xee v&\xe1]\xcd\xbfd1&\xe1rH\xc6\xbf\xa0[\xe1\xdc\xad\x94\xe4?\xe8\xf5\xd4\xe4\xf3\xca\xc7\xbfr\x88\xdel\xab|\xdf\xbfp\xc4\x98g_\xce\xdd?\x95%\x93)\x89\x94\xc5?G\x89\xa4\x98\xcb\x99\xd7?w\xf0\r\x81+\xb1\xd8?V\xd9<\x8b\xb6Y\xe0\xbf\xd3;I\x03kd\xd1\xbf\xe0|\x004\x88m\xd7\xbf\x18\x0c\xe5q\x07\x11\x9f\xbfQ\x05\xcf\xbb\xe7n\xdd?\xaf,\x19\xc7,Y\xb1?@$\x99\x9dU\x91\xe2\xbfy\xfc\xbc\x03\xf3\xd7\xb0\xbf\x8d\xff\xb0\xd2Za\xd3?\xcfH\xda\x82y(\xb9?NTh\xb8\xb1\x96\xe1\xbf\xea\x1c\xb37\xc8j\xc9?&(\xd0\xaeq\xfa\xc6\xbf\xf1\x0c\xe5\xc1\x12T\xa9?\xe8\xcd\xaf\n\xad\x12\xe3\xbf\xdf)\'\xf6\xf9u\xe5?\xb7\xed\x13u\xbb\x15\xcf\xbft\xd5\x8c_s:\xb4?}\xf6\x83\xc0\x03\xab\xc9?G\x97\x175\x91\xfb\xcd\xbf\x7f\xfb\x81\xdf\xfb\xa9\xe0?\x85\xf1nu\x17g\xdc\xbf\xc5T,U\x8d\xbc\xc2\xbf\x8c7\x94/\xdaJ\xd0\xbf\x82\xe4\xbbP\r`\xdb\xbf\xd6\xa2T\x1f\x8d\x16\xd4?\xb7l\xb9\xb0e\xc5\xd9?\xa4M\x8am\xd6\xf7\xa6?\xd7\x0b\x8a\xfe\t!\xcb?\x86\xd5\x8cp\xb2\x9f\xda?\xc1\xcf\x8dvMf\xd3?0\x89\x10\x04\xbe(\xd8?\x8f--\x16\xc1\x9a\xd9?\xd8\x1c\xd0\x8e\xe5`\xd5?\xc1#\xf6%\x02\xdc\xd6?\t\x13\x0b\xcaD\xa8\xd8?i\x90\xf9\xfeS\xe0\xb1?\x7f$\xb5\r~J\xe0\xbfN@\xa8\x96\r\x8f\xbd?0\xea=h\x99\x7f\xe3?a\x82\x05\x91\xf4U\xc1?\xac\x04J\xba\x8a{\xb4?$p\xee\xf4\xe5\xcf\xe1\xbf\xd5p\x81\x8e\xf0\x91\xc0?\x80\x85\xced\x0b\xce\xe1?\x04[\xda\x9a\x1dk\xdd?\xec~(\x1e\x8b\xd7\xb7\xbf\xca\x1b/\n\x94\x00\xb0\xbf3\xd4\x02\xa9\xb7\x15\xc9\xbfP\x1cz\x98mF\xd4?\xa0\x0c\xe6\xe1\xd7\x1e\xe0\xbf\xa1\x06\x17\xe4\xe6h\xd1\xbf\xa8?\xac\x06\xd1\x86\xd0\xbfp\xb8)\x00"\xa9\xac?L\x8az\xc2\x1f\xdb\xe5?\xe8\xdc\xbf\xe6\xbc\x1b\xc8?\x025N\x08\x13\x13\xe2\xbfR?1d/\xc8\xc4?\x7f\xde\r\xf8\xe1\x18\xb6? \x0b\xea\xc9Q\x92\xd1\xbf\xdf\xa0Dz\xa8\x84\xcf\xbf0\x16\xab;F\x15\xcc?\xea\x08\x1f+\x1e\x87\xbd\xbf\xd0&6^\xaf\x94\xd6?\x1d\xdcr]b\x04\xdc?u\xd1\x80\x0f\x9e\x84\xc1\xbf\xc8\xb2\x17\xcb\xf8\xd4\xa7?/\xcdM#\xe8\x88\xe7\xbf\x19L}\x11\xf1U\xd3?\xa6}_\x8d\x0c\xef\xb9\xbf\x00(\x89\xc7\x9f \xde\xbf0\xa2]\xba\xdf\n\xe0?\xbaT\xf3\xb3\xda\x05\xde\xbf\xac4\xb7eYR\xab\xbf\xa8\xbb\xdc\xf1i\x91\xdc?b\x9b]\xf2\x95\xbd\xad\xbf\xef\xa0\x1a^\xb0\xcb\xdb?\xcfT%I\x01L\xe1\xbf\x9e\x16\x15\\7\xf0\xc8?g\'d\x86\xea\xfc\xd0\xbfF\x99\xb0\xac@\xcc\xcc?\xfe\x9d\x00\x0b\x9d$\xd6?C\xc2\xe6D\xa18\xd4?\xb2x\xa3?R\x00\xd8\xbf\x9c:\x1a\x7fu\xeb\xde?,2\x11=\\\xfc\xaa?\xbf\xf4\x01\x18\xb7\x84\xd8?\xcfl\xbdn\xfa\x7f\x98?;\xe0\xf7\xaf\x19\xfc\xa5?\xb8J\x00\x15(\x13\xe9\xbf\x8a\xda\xb4vg\xc1\x85?PBy\xb9\xb6 \x97\xbf\x99\xac\x9b\x17Pj\xd2\xbf\xf0q\xe8\xc0#\x98\xdc\xbf\xf9KG\x03l\x08\xd3\xbf.;y\x82\x96$\xd7\xbf\xf9\x89\xccujn\xd3\xbf$$\xae>\x10j\xd8\xbf\xa9\x98\xbe\xdc\xb77\xd7\xbf\xac\xf0^\x04Z\xfe\xd6\xbfl&%\xdd\xa2\xe7\xaa?\xbc\xd5d\xad\x9d\x1e\xba?\x1a\xfa&\xa7\xe0D\xb3\xbf\x06\xe1\x94i\x99"\xe1\xbf#j\x17%\xb7%\x93?\x9c\x99\xa4\xeeh[\xdf\xbf\x86\x84\xcc\x86\xaa\xac\xd7?\xbfz\x1a\x8d\xb4\x05\xe2?\x19\xa3\x93@lG\xdc\xbf\n\x81S\xbb&P\xc5\xbf\x08\x16\x0f\x04\xffo\xe2?\x8b\x0c.@n\xdd\xa2?\xf6y\x8e?\x82/\xe0\xbf\xf8`5o\x84P\xb0?\xe6\xef\xf3\xfd\xf1c\xdb?V\xddm\x08/\x0c\x9a\xbf\xda\xd6@K^\xb9\xe1?\xd2m\xadp\x92\x1c\xe6\xbf\x85t\xa1\xff\\\x8b\xc2?\xfd\xe8\x82\x07\x1ee\xd1?&\xba\xcb\x97\x7f\xb5\xb6?\x06\xc9h\xc0\x80\xd9\xcf\xbft\xbe\x15\xbd\xd7\xdc\xcc?\xa0\xa5s\xac\x82\x9f\x90\xbf^\r\x13/\x0f\xdc\xe0?\xb5H\xb3\xc3\xfd\xb0\xde?\xceN\xea\xd2\xda\xc1\xb3?\xa0\xf9"k\xa0]\xab\xbf\x05\xed\xe6\xc3\x8d\xea\xde\xbfJ`\xb7\x15e\x13\xd0\xbf&\x9f\x86\xfa\x0b\x87\xb2?\xe3\xda\x84X\xa7L\xdb\xbf\x8a\x15\xa1&a\xa5\xbc?2\xf3NKD;\x80?\xd3U/t7\x0f\xe7?\xc8\xda(\x07\xdfw\xda\xbf2\x85F\xd5P\xb6\xd5?x\xf2c|\xf2\xad\xaa\xbfL\xbc\xb2S\x90k\xda\xbf\x04bg\xe7\x8f\x1f\xb8\xbf\xc6\xd08\xa3U\xf0\xba\xbf\x1eq\xdf/F\x94e?F\x03j8\x07\x1c\xbd\xbf\x83R>\x87\x86\x9e\xd2\xbf0N\x93\x07{\x83\xdd?1\xe0\xa6"(Q\xb7?/\x86\x12\xa8\x89#\xe2?\xf6;\xf9\xe2(\xe8\xe2\xbf\x1e7\x02\xce\xa3X\xd5?\nz\x17\xa9\xc5\xf2\xcd\xbf\x91\xd2Q3\xb4\x1c\xbf\xbf\xe2C\x94]i\xeb\xdf\xbfce\xb0\xf5\x92f\xd2\xbfH\x81}B\n\n\xe6?\xf8T\n\x95c\xdd\x98?\xca\xb4\xf9d\x00\x14\xbd?r\x1f\xd2\xe9\xc7\x0b\xd6\xbfrp\xbc\x8a\xb3V\xd6\xbf_"M+\xf4\xfd\xd6\xbf\xc8\xee\x85\x0e\x18\xd2\xd7\xbfD\x1b\xce\x8bz^\xd6\xbf-K\x87\xdf\x92\xf8\xd5\xbf\x8e\x9bC\x1f\x1a1\xd7\xbf\t\x9d[\x83\x8a9\xd6\xbfD?O\x14\x18\x89\xc8\xbf\xaf\xb7uu\xd6\xab\xce\xbf\x98\x83m\xc8\x0e\x8c\x95?\x10\x8b9\xe9\x08\xd9\xd3?\x0c\xdd\x9dN|\x84\xd3\xbfz\x97\x86 \xaf\x11\xc1\xbfB\xdd:\x97Q\xfb\xd0\xbf\xafg^\xb9\t\\\xe9?\x89a\xde\x99\x18\x9f\xdd?\x1ai\xcd\x13\xa8\x12\xd3?\xe3Q\xf5\x96\xd5\x13\xc0\xbf&\xae3\xcc\x90\x92\xaf?:\x17\x12\t\xcb\xfb\xe8\xbf\xc8~`\xd6\xc9s\xce?\x03X\x87\x96\xa1)\xa8\xbfO&o\xf1iN\xb8\xbf\x81~8\xc5/+\xc8\xbfU\xdd\xfc\xc6`\xf7\xe0?@\x98o? \xd0\xe0?\x90\xc1\x98\xe7+\xf3\xe0\xbf\x1c\xdf\xca\xee\xdc\x11\xb3\xbf\xd2WY\xce[\xf4\xbd\xbf\xdc%j\x9e\xb8 \xd2\xbf<\xb2\xfd%\x85\x17\xc5?ef;8\xfb\x0c\xc6\xbf\xd0\xd7\xf9%~\x9f\xa3?\x8d\x8d\x80\xfc\x1a\xa1\xd1\xbfa\xca\xc0\xfa\xecy\xb7\xbf=\xef#;b\x8e\xca?\xe7\xf4\x8d\x9d\xea\xb1\xe8?z\x9f\x89\xbb6\x86\xdf\xbfh\x12\xe8/t\xa4\xad?*\x81?z1"\xd8?\xa8\xe5\xebq#R\xe4\xbf\x9eB\xda\xa0\xdd\xf3\xd5?\x9c\xc8e\xecSb\xdf\xbf\xb8\x8a\xb7\x98u(\xae\xbfin$\x00h\x1f\xd0?\xba&m[\xe5\xa6\xba?\x15E\x9e\x9f*{\xc1?GdS$\r\xcc\xdf??\xa2\xa2\xd4\xc0(\xb5\xbf\xeb\xa9\xa8!\xfe,\xcb?\x86\xdbZ\xd8h\xb0\xd1?1]C\xac\xacT\xce?\x1e7n\xd7\x8b\x94\xd0\xbfrF~o\xfc\xc0\xe5\xbf\xbeT\x8f\x97\xf4l\xc9\xbf2J\xc9P\xd4\xd9\xda?\x82\xab\xd6^\xa8]\xc9?5,\xa5<#\xa9\xe2\xbf:2[\xe8\xfa?\xd9\xbf\xac\xb6\xcf\xdeG\x93\xd0?\x14\x9e@;\xba\xae\xd0\xbf$\xf1\xf6ZE\xcb\x8e?\xb1t\xa8\xc7K\x18\xd9?\xf8\xbc\xdd\xfc\x10\x8f\xd7\xbf\x94\x93\x8a\x8b}\xba\xd5\xbfr\x95\xdf/\xe5\x1a\xda\xbfh;\xb5 _\xae\xd9\xbf\xb5\xe45\xd3&\x85\xda\xbf\x85\x85\xd6F\xc3K\xd1\xbfZ\x08\x06\x95\xaci\xd0\xbfr\x00\n\xb4\xbcZ\xd5\xbf \xcc\x13T\xa4\xf5\xde?%\x16\xf6\x0b\xd2\xe6\xe2\xbf\xa0\xa0\xe8a7\x02\x84?0W.\x87\xf5\xe5\xc2\xbf\t\xcbN\xe9\xfd\x12\xdb?@\xc7\xcd}%o\xb9?\xe3\x7fH\xd2\x0c\xe3\xb7?%a\x0e\x87At\xdc\xbf \x9b\x86G\xf6\xeb\xdf\xbf\x00[v\x820P\xb7\xbf\x1b\xadF{\xe6\xd9\xb3\xbf6D,\xd16\xa1\xd2?\xea4\xa9Q\x86\x8f\xe5?\xb0\xa1G\xd5R$\xc4\xbf\xdd\x17zk\x88\xa7\xda\xbf`\xb7\xaaR\x16nY\xbfI\xd3\xd8\xff?\xdb\xbe\xbf\xa8;F_T\x0e\xdc?\x8f\x15\xef\xd0\xe1\xa2\xbb?\x18\xed\xc3\x89y\xce\x93?\xa3\x080\x8a\xb8W\xc1?\xa0\xe1Q\xea\xb9v\xdd\xbf\xee \x87\xac\x87\xfc\xdb?7q\xba\xf6\x7f1\xe3\xbf6\x13\xb5\x86#\x17\xd9\xbf7\xcb\x0b\xcd\xe6\xb0\xde\xbf4I\x97O\x1eM\xe2?H^z\xe8m\x90\xd7?\x97\xee\x98\xd5\x13P\xd2\xbf0Q\xfeM\\\x9a\xb2?\xf7\x0b\xe8\xae\xbe\x1c\xce?D\x01)"q\x15\xbb\xbfA\xa7`\x1b\x8eM\xd9\xbf\xba7\xea\xe7\xe7\x1d\xa8?\xbcTq\xdc\xdbM\xd0?\xe6z~z{\xf7\xe6\xbf\x87i \xe5&\x0c\xd2?yn\x8f\x8e\x8f\x18\xd4?\xa2\xd5\xdcK\xec\x12\xd0?\x9d\x88\x05C\x949\xc2?\x99\x89I`\xe9\xdb\xcc\xbf\xc1d\x8e\x87S\x9c\xbc?<\x94;\x84\x91>\xdf\xbf\xbfX\xbd\xaf*p\xd1?X!\xd5\x87\xc2\xd3\x93\xbf[k\xd2\xda\xe7\xf6\xe5?0\x02\x99\xa8Ic\xce?\x90\xaa\xb14\xa2\xda\xd3\xbfr\n\xe6`\xf6e\xb4\xbf\xc9y\xbb\x0f+F\xd3\xbf\x94l!yc\x91\xdb\xbfo4cl\xb3\xab\xb5?\xed\xc5u\x0e\xcf\x9a\xbf?Km\x02\xd7\xb7\xc6\xd4\xbfW\x08,\xd4\x10\xe2\xe3?\x93\xde\xca\x92\xdd\xdf\xdc?p\xe8\x80\x18S\xe1\xd8\xbf\x1cm\xb1PF\x8d\xd5\xbf\xcbj\x1e_tw\xd7\xbfRE&\xae\xc1:\xd4\xbf\xe6?\xea\xe6\x91\xfb\x7f\xa3\xc0\xbf\xc5\xb9\xf3of\xce\xc1?`\xa9\xe3\xd3^\xf9\xe4\xbf\xf0-\\\xf8k\xfc\x9f?0]\xce\x81\xc8>\xc0\xbf\xcd.\xe0\xe4\xaf\x80\xc5?@\xa0\xdb\x0fW\x10\xa5?|\x9a\xa1\xaa\xe7\x8d\xac\xbfj\xa3\x9bH\x18\xb3\xea\xbf\xaf\x81\xd2\xc1n\xa8\xd8?J\xdekR\xb6\x9a\xc6?\x1b\xccB\x80\xad\xfb\xb8\xbfY\x90(@\x14J\xd2?Ni\xcf\xa3.\x93\xd4\xbf%\xf5\x8f\xe8\x0f\x99\x99?\x8a:j\xd1E\x1e\xa2?\xfb\x1e\xc0_=\xf1\xa8?}\xdfe\x924\x17\xcf\xbfR!\xb5\xb4\x7fZ\xd1\xbflpF\x13\xc8\xd9\xba?\xef\xcf\xdd\x04+\xb1\xeb?j!\xc1\x9b\xaa\xd0\xbf\xbf\xe5\x1d\xae\xe8K\x7f\xe2?"-\xb4\xe3\x8a\x88\xc5\xbfed@\xae\xcdm\xd4\xbf\xa6\x19SO\x9e\x88\xd6\xbf\xaa\xfd\xd1u9\x15\xbb\xbf7\x04\xf5\xaf\nR\xe2?p\x10G\x82\x07\xa4\xce\xbf\x87H1=\x91\xdd\xe1\xbfr\xfb\xcd\xc0\xc4\x86\xde\xbf\x84\x0b"y]\xd9\xd4?A\x0cL3s\x87\xc0\xbf\xd8\xb5[\xf3\x97\x1d\xc4?\xf0\xf9;\xe9\x8e\xfa\xd0?\xffB\xea\x83\xf5l\xde?D9%\x0b\xeb\xa5\xc0\xbf\x97x\x8c\xcb/\xdf\x86?/\xdbf\x13\xb5\xb8\xc3\xbf\xdb\xd1\xe52\xf1@\xe1\xbf[\xba\xae\xd00A\xc3?\xc4}w\xa0\xf3-\xe2?\xf8\x04uh\\5\xde\xbfj\xd5}\xbb\xe7\xe8\xd5?\x8c\xaf\x08z)\xbd\x7f\xbf' +tbg1 +(g2 +(I0 +tS'b' +tRp6 +(I1 +(I10 +I8 +tg5 +I00 +S'\x94\xc5\xa9\x05\xd5e\x98?\xbb3a\x8dW\xab\xa9?\xd6_\xf3p\x0e\x14\xac?1$\x1ea\xd2\xcc\x9f?6E]\x8cn\xa6\xb0?\x93\xc4\x96!\xd2\xdb\xb1?V\xdfg\xf8e\x1e\xa8?,\n\xb9^\xec\xe7\xa2?*\xfa\t*\xe1\xa8\xa3?f\xa7\xde+l\'u?\x15hJ6\xb0O\xa2?\xca\x19\xfari\xcc\x94?\xb8\x95\xb1L\x14+\xae?%\ns_g\xfa\xa0?\n\xcc?\xbd\xceh\x98?zx\xe4^\xb0X\xa8?\xe7\xab\xd9q$h\x82?\x1c\x1d{u\x01q\xa0?\x97\x14\x03\xa5p\xee\xa2?\x9b\xc1%\xb1\xd9\xae\xa9?\xd0z\xee\xdcv\xd5\xa3?C[\x0b{\x0e\xd4\xaa?\xb3)\xcb0\\\x88\xa8?\xad\xfb\xca\xf1qo\xa3?\x1c\xac\xa0;\x9bM\xa1?\xbe\xcdv\xf0\xe1\r\xa1?Ol\xad\x94H\xcf8?b\'\xe4e\xab\xb3n?\xbf\'c\xc4\xa0\x89\xa1?\xbc\x1a\x10\x13\xe7\xc9\xb0?\x0b\x82\xd5\x84x\xd0\xa6?\x14\x82 \x923_\xa5?t\xa1S_\xc20\xb5?:\xf9\xc0*\x94\xc3\xb0?\xb9\xf23\xc9\xb8\xa5\xab?G\x02F\xec\xae\xf7\xb2?\xd1\x08\xc5\x92\x0b\x0ec?\\\xe3\xd7\x9c\x83\xa3\xb5?dC6|\xb8\x1d\xa4?3\xcf\xc1\x8f\xe3\x7f\xa7?\xf3\x94\x8d\xad\x04\xb6w?\xdb\xc8\xfd\xe8\xe8\xa1\x90? %Q\x97\xf9`\x9e?\xca\xc7\x99fHY\x80?X\xf0>E\xc7^\x8e?\x87\xf2\x0fS\xaf\x03 ?\xf8\xab\xb8{\xe8B\xa3? \x07-\x17\x0cg\xa3?\xe2\x14"\xf2\x1e\x08\x8f?\xbd\xa0\x80\x96\x93\xa9\xa5?T+Cs\xd2\xe2\xa9?/\xf7\rd\x02\xbf\x8a?\x0e\xaa`\xa0)\x98\xa9?\xdc\'\xad\xf0\xc0J\x94?;I\xd5\xeb\x17\x9d\xa6?Z\xe3\xfdW\xdcx\xae?%\x8d\xdf?I$T?e\xa4\xc3M-,\x86?@l1sv2\x9f?w\xa7v\x07\xa6\n\x9b?_\xe4\xf8\xe1Z;\xac?U\\]\xccA`\x9f?\xb34x7v\xc9\xa6?%[c\x98\xb9\xd9\xaf?p\xbb(\x80\x9b\x7f\xa3?wY\xa6\xf2\xd6\xed]?i\xcc\xc3\xa5m\xe6\xa3?\xcd\xe5\n?<\x1b\x9a?\x12dB\x11\xc4\xa4\xad?R\xda\x97\x8f\x9ez\xad?\xc2\x7fW\x02\xba\xd1\xa3?\xc7\xa2\xdd\xd1i\xf5\xa2?S\xc2\x97\xbc\xdf\x1a\x8e?\xc8$k\xdb\xdf\xdb\xae?\x8f\x14\x9e\xdf\x15M\xb4?h\xc6\xf4\xf1\xe8L\xb4?H\x8b\x8b \xaf\xc4\xb5?`\x05\xc8R\x0b\x89\x8d?%7\x16i\x7fy\xa8?\x00\x07\xe1qt\xc1Q?' +tbg1 +(g2 +(I0 +tS'b' +tRp7 +(I1 +(I10 +tg4 +(S'i8' +I0 +I1 +tRp8 +(I3 +S'<' +NNNI-1 +I-1 +I0 +tbI00 +S'\r\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\x00\x00\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x13\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00' +tbI2 +t. \ No newline at end of file diff --git a/python/test/testdata/test_compute_rotations_from_accumulators_output.pkl b/python/test/testdata/test_compute_rotations_from_accumulators_output.pkl new file mode 100644 index 0000000..6be9654 --- /dev/null +++ b/python/test/testdata/test_compute_rotations_from_accumulators_output.pkl @@ -0,0 +1,40 @@ +(cnumpy.core.multiarray +_reconstruct +p1 +(cnumpy +ndarray +p2 +(I0 +tS'b' +tRp3 +(I1 +(I10 +I8 +I8 +tcnumpy +dtype +p4 +(S'f8' +I0 +I1 +tRp5 +(I3 +S'<' +NNNI-1 +I-1 +I0 +tbI00 +S'\xb0g\xe2\x14\xe5~\xa4?\x82\x18!]#;\xcf?\x08\xe82\x1c\xb1\x81\xd6?j<\x0b\xb3\xf7v\xcf\xbf\x93\x98\xcb_\x9b\n\x9e\xbf\x8a\x96\xc1@v\xe8\xd3?\xb3@+\x1f\xbf\xff\xd8\xbf\xc7V\xffq\x0c\xbb\xe6\xbf\x07\xd2\xceX\xd4\x00\xe8\xbf(\xf0\x92J\x84\xfb\x8c?\xac\x89\xcf\xc9\xadH\xdb?\x97\xe4\xd5\x9d\xbb\xe5\xd6?\xf4\xcdo\x04\x9f\xaf\xc1?\\C\x12\x99\xc8\xfb\xd0\xbf\x03\xb2"?S\x8c\xc8\xbf\xe6\xe4\xa6\xd2\x12\x17\xa1?\x9b\xf0%:\xe0}\xbf\xbf\xaa\xf74\xb4\xe5\'\xdb?\xe0\x8e\xe9\xd6\x166\xe5\xbf\x14\x8cT<\xdfU\xd0?\xc8\xbc\x16\x1d\xf6h\xdd?\xce\xf4\xb9\x8e?\xd0"\xdf\x88\xb4b\xd3?4\xa5V*\xaa\xd0\xe6?p\xd4\xf6#\xd6\xa4\xa2?~\xcaT\x19\x1bZ\xc3\xbf\x1c9\xe21|w\xce?\x8d\xae7\x81u\xc6\xd0\xbfAc\x87\xb7\x00T\xca\xbf\xb6RPe*\xf2\xdf\xbf\xf8\xed&\xd0<\xf6\x98\xbf$\x1d>\xe9\xb3\xcd\xc0?\xd2\x97\xc1\xa88\xb3\xd8?\xd8C\x98\r\xe3\xf5\xe4?\xef\x17[\xdf\xbd\x96\xd3?\xdeq\x9c\x00\x82\xa8\xc1\xbf\xcd\xa9\x14\n\x99\xf1\xd5\xbf\xc0\xb0\x91\x8b$|\xd6?,\xe3\xf2V\x83\xe2\xba\xbfI\xec\x18\xe2\xfe\n\xb1?\xeai\xb7\xbf\xf4\xf7;{\x05\xb6\xb6?.\'\x15#?\xb5\xd9\xbf\xa7\x10\xc4\xb0(\x1e\xd8\xbf\rud\x05\xab\x04\xd7\xbf.\x0e\xe0\xb1\x1bZ\xdf?D\x89\x01\x12\xc2*\xd9?ic\xfdr\x9aL\xd1?\x89CP\xb7xb\xd3\xbf\xbc\x8c\xdb\x18#*\xd7\xbf\x00w\x96\x8e\xdcV\x8d?"\xc0\x9e\x99\xa6\xc4\xe0\xbfJZ3{e[\xca?R\xeb\x14f:\xa8\xce\xbf\t\x1f\xc8\x98H\xf6\xdb?\xf8\xcbT\x05\x1bE\xb7?\xd5\xed\x1e\x82\x99c\xe1?A\x0bl\x84v\x98\xe2\xbf\xb8\x13\xd84\x11y\xd6\xbf\xe6\x18\xf5\xf3\xa8\xf7\xc8\xbf\x1c`h\xf5 [\xd6\xbf\x7f\x1e\x11\xc7\xfc\xf8\xd6\xbfb\xa8\xc1\xf1\xa4T\xdc\xbf\x8c\xcd%&\xf41\xa0\xbf\xbe\xec\xa5\xe7?\x82\xcd\xbf\xedD\x83F\x9d\xb6\xb6\xbf\x16\xe8\xf9\xed3\xe2\xe1\xbf\x04\xe1 \x0c|e\xd4?\xfei\x12\xa3\xed\xce\xb4\xbf>\xd6\xb7\x92\x02\x86\xd2?:\xda\x87_\x0f\xdc\xa2?\xfaBp!\xffI\xe0\xbf\xd3\x1b`\xc0\xbb\x84\xde?\xd5\xc2N\x9f\xa8\xd1\xaa\xbfX\x87C\x8f^\x98\xa3\xbf\xe7\x8d}sn\x02\xc4?LN\xf9\x19\x92\xab\xbb\xbfc\x04\x0b\x8e\xd1\xca\xc6? \x84Y\xd8\x80u\xde\xbfL\x0f\x07\xbfL\xce\xcd\xbf\x85\x19\xb4\x7f\x90\xbe\xe9?Sr\xa0\xe4R\x97\xe0\xbf"\xafr\xb2!\xcc\xbb?\x80\x88U\x18\xfe\x14\xa5?\xcd4\xff\xdf\x1b\'\xc5?\xc2\xd3\xf2\xee\x8dA\xcf\xbf\xac\xf4\xc4M\x0b\x8e\xde?\xd7h\xca\xe9\x81\xc9\xe3\xbfr(\xbfWRQ\xc2?\xbb\x94C_\xde\x99\xd3\xbf\x08\xf6Zl?\x01\xe1\xbf\x08\xe0\xc3\xdc\xcb(\xe4?\x90\xf5\xe4n\x93\xe6\xc8?\xe8C\x07v\x8c\x15\xd4\xbf`\x96"\xaal\xae\xc4\xbf\\x*\xa1\x94\xe1\xcd?\x139\x10\x9d\xdd\x9c\xb9\xbf\xddN\xf1>u\x88\xe0?\x12\xe7\xd5\xd18\xfe\xd2?#\xa3\xdbGq\x93\xe4?\x0e\xa0$%\x7f&\xd1?P\x9d\xd0\xb7\xe21\xcb?!\x8f\x06\x8b\xa3%\xb9?z\xbc\xb1XoR\xd3\xbf\xa7;\x0ch&\x13\xbe\xbf\x0caMDU\xc6\xd1?\x1c\xfb\xd3\x99c\xcc\xd1?\xab\x89I\x18\xfc\x07\xb9\xbfzh\x83\xb2^\xab\x9d?!r<\x9e\x08\x9d\xea\xbf\xc5\x99\xbc\xcc\xa9\xbc\xd6\xbf\xa9\xa5\xd3(s\xc2\xc0\xbf\xa3\x15q\x85\xf9\x07\x84\xbf\x94z\xabR~I\xe0\xbf\x1a\xbd\xed\xe75\xc4\xe0?x\x91\xb5\xa52C\x9a?\x07\xf2\x08^\xe9N\xd1?\xec\xe5\xd2\xedq,\xcc?\xe0]_SH\xe2\xdf\xbft\xb3\x8bR\x84\xd3\xa9?\x16\x06yHj\x90\xd3\xbfS\x9f\xfa\xc5Y6\xc1?I\x88\xaa\xd1\x02n\xe0\xbf\x90T\xd2M\x96\xef\xd2\xbf\x98\x88kGd\xde\xce?\xb0u>\t\xcd\'\xc4?\xa9\xa6\x89\x88\xb17\xd6\xbfl\xd9\x12\x16N\x11\xe2\xbf\xbe\xeb\xf0\xc7\x1b\x15\xd5\xbf\xff\x7f\x17>\xc3\xad\xbd?\xb3&dE\xd6-\x9d\xbf\x03\x83\x0fKNj\xd0\xbf\x15k\x02\x0c[#\xeb?\x00\x01\xf9J\xd1e\x87\xbf\x7f\xcd\xea\xcaA?\xbf?CB6k\xd9\x05\xd2?<\xe2u\xcf\xa7\xe0\xd4?\xc6}S\xec\x80\xb0\xc5?\xdfJ*T\xdb\x1e\xe6\xbf\x96\x9aP\xdf\x91\xe2\xc1?\xd4\xb1Sg`7\xca\xbfX\xaf\x02EP\x12\x93\xbf>Z\xa21\xee\x8b\xd3?\xfe\xd3?\x03@|\xda\xbf\x96\x05WW\x0b)\xda?1\xa2L*s\xce\xa4?\xdd\xa7\x81\t\xa1\xdc\xcf?\xb5\xfa\x1aZM\x1c\xcc\xbf\x83_Jw]\xc2\xda?\x126\xaa\xd9U\xeb\xc1?\x90\xbe \xbaY#\xe9?\xff\xe0\x96\xc9\x98\xdc\xce\xbf\xdc6\xe8\x0fGh\xc1\xbf\x92\xf1\xa6\x9dr4\xe9?\xc8\x93\xa4\xfd<\xd8\xcf\xbf\xd4\xb1I\xd3\xbf\x84\xb9\xbf\x8a\xc4\xb7\x1f\xaf\x13\xc7? \x87\xb1\xafD\x97\xd1\xbf\x01\xdebE\xa0]\xa4\xbfLiC\x91\xcf\x07\xcb?\xbb\x16\x082\x08\x14\xd9\xbf\xf2\xd8U\x8d\x07\xba\xa5\xbf\xed\x03\xa9\xa3j\x06\xd3?\xcd\xcej\x06z\xc2\xde?Qa\xa5\x94\xc2\x90\xcc\xbf\xd9\x00\xba\xc64\xa9\xe7\xbf\xaa6\x04\xf1KP\xd2?0\x9d?\x17\x12J\x8a\xbf\x8a\xd6\x82\xcf\xb8\xaf\x90\xbf\xa5\x076\x85\xf2\xc0\xa1\xbfx\xac\xeeP\xffT\x8a?\x19\xc2\x1e9\x91\x96\xe5\xbf\x8fUc\xe8\xdc\xd5\xc2\xbf\x00;\xe1\xe9\x8e\x15\xd6\xbfG\x0e\nP\xda\x8f\xc3?\x00\x94\xf2\x87\xe6\x9c\xd9?\xdb\xf2\xba)\xf0\xf7\xdd?\xad\xa4\xc3E\xc0p\xda\xbf\x12\xf5\xd6K\xfc\xb2\xdd\xbf\xc3Og\x80V\xbc\xd0?\xc23\x9dD\xd2\xcc\xdf?\xe6&\xd0\xa7NB\xb8\xbf}u\xda\xbbz\x8d\xc2?\xe5\xe5\x8c\x9d\x9a\x87\xe0?\xca\xbaR\xa6\xe9O\xa6\xbf\xd0\xf1mh\x05\xa4\xda\xbf\x02\\@\xcfWH\xd2\xbf\xda\xe5\xa6`%(\xda\xbf\xf2\x1e\xd1\xb0:\xac\xb5\xbf\x86?\x16\xd2\x87\x98\xd6\xbf*\x86\xfdah\xc7\xba\xbf\x16\xb2\xe9\xe3\xfdr\xd8\xbf\xac\xdc\xdb(\xdd6\xe1\xbfr\x9f\xf8#\xcey\xa8\xbf\x12\xdf\x95~\xd4\xfa\xbc\xbf\xc7\x90\x90\xea8y\x9c?[\x9a\xc6\x08\xe6\xdd\xe4\xbf^Kx/7\xbf\xd4?\xe1Iaa-\xdb\xd8?eY6g\x03\x07\xd9?\xd2n#c\xd2\xca\xd8\xbf\x1e\x14\x16\xc6\xe0\xd1\xb1?\x80\xb5\x90j\xb6b\xd5\xbf\x80e+\xf1\xbd\x9a\xd1\xbf\x1a\xe8\x9f2\xc9g\xc3\xbf!\x9a\x7f\x18\x85\x18\xca?#\x0cu\xcf\xcb\xdb\xd5?9\xd7n\x17\x03\xf7\xe6?\x04P\x04Z\x9da\xd5\xbf\xdbD\x90\x7f\xc9.\xca\xbf\xbe\xb9q\xeb\x87\x11\xd4\xbf\xb1\x88A4\xee3\xc7\xbf\x9f\xc2\xad\xc9\xdf}\xcd\xbf\x07\xe3\xa6F\x9b\xde\xea\xbfy9\x15:\xa2V\xb6?fJ\x83\x02\xce_\xb0?q\x86x\xd9\x1d\x98\xce?$\x15\x9b\xee\xe5\xa2\xaf\xbf\xfa\x85\xaf\xb1\xfbG\xe6\xbf.;\xf9={m\xce?\x81\x07A\xc6\xe7\xfa\xae?\x0b\xda+7\x85\x8c\xd3?2\x1dd\xc1@\xbd\xd8?N\xaa7K\xc51\xd6\xbf8\xb8q\xcc!\x02\xd3?]\xb4w\xa04\xcd\xe0?~\xb5@\xb3\xa6\xdf\xdb\xbf\x9c\xcdO\x8f\x90p\xd0\xbf\xabE\rK\xea{\xc8?\x148d>\xcd\x93\xc2\xbf \xfd\x9e\xc0\xfc\xbb\xd8\xbfm9d\xaf(5\xd2\xbf\x0e\xfd\xf8p\xc24\xdb\xbf\'_\x9bI 2\xe3\xbf\x1c?\x90\xe0\xe2v\xc3\xbfb\x12\x00\xb8I\x8c\xd8\xbf\x7f\x81\xafx\xe9\xec\xd9\xbf\xc0\xa3\xd6bS-\xd3?ZnM\xf9[\xd2\xd8\xbf*4\x0b\x1d\x1f0\xcb\xbf/\xda\xf0f\x82S\xc2\xbf\x83\xbf\xf7\xd4E\x89\xde?\xca]\xdc+\xbf*\xb1\xbf!qM\xdb\x18\x99\xa0?\xdac\xd5\xddV\xe7\xe0\xbf{\x8b:\xe3\x88\xac\xc6?\xfa\xec\xe7\x1a.\x0c\xd7\xbf\xed\x97-\xae\x12\xe8\xc8?\xda@F\x9d\xac2\xe1?4p\xc9\x88\xf9\xca\xd0? \xe3?#\xe1\xd8\xfe\xae\xf3\xcc\xbf\\\xc9pC0\xab\xcf?]\xe4\xb2\xe1\x10C\xdd\xbf[\x8f\x93\xe6\x97\x8a\xd9\xbf\x15\xd7\x0e~\t\x0b\xd7\xbf\xcdv\xfa/l\xc8\xc2?\x16\xd9s\xe7i\xa4\xb4\xbf>\xfbak\xf3{?\xa4\x94\xf8J\xaf\xe4\xe4\xbf:<>dTK\xb5\xbf\xfa\x89\xf7\xe7\xad\xdb\xb0\xbf\x00\xb0\xd6T\xcf\x9e\xb6\xbf\x11#\xaf\xba\x13\xe2\xe3\xbf\x1d\xae\xa4f\xde\xe6\xd1?\x89\x14S\xd6\xe0\r\xe4?x\x89T\xe0h(\xd6?&\xb2\xa9C\xe34\xd3?>\xb9\x1b\xb3%\x8c\xd3?\xca\x1d\x8f\x8e\rB\xdc?\x0f\x18)*\xa7\xb9\xb9?8\xeaP\xe1+;\x9f?\xbb\xd38\xe2\x9d\n\xd4?\x06\xad\xd7\x86K?\xcb?\xe33|\x86"\xcc\xdb?\x82\x9a?\xb6\x06\xfa\xc2\xbf\x84]I\xbb\x84\x1e\xd1?\x91o\x17\x8e\x08\xd6\xe3\xbf\x98U\xca\x83\xa4\xd2\xdb?D*\\0\xfa\xb6\xb8?\x086V6\xe01\xd3\xbfn\x10\xdfI\x0c\x1f\xa5\xbf\xd3\x94xx\x82B\xdb?\xf0\x84\x1etV\xa3\xd0?\xdc\xa3\xf5\x89\xe3\xec\xd4?oL\xe4\xc7\xea\xe6\xe1\xbf\x8c\x12\xac\'\xc4C\xe2\xbf\x100\x12\xa0z>\xa1?\xf6W\xe2\xe3A\xa1\xa9?l\xeb\xdd?\x1a#\xa0\x16]\x0c\xe3\xbf\xb0\x8d:\x93\xbf\xe3\xb0\xbf|\xd4c\xdd\xe3\x12\xc9\xbf\x83/C\xd0\x9cp\xe6\xbf\xf3\x89\xb2\x12]}\xc1?\x89\x17\x0f\xf0\x11\x19\xc9?%L\xb8\x81d.\xd1\xbf2\xd1\x82\xebs6\xe0?p\xb0\xde\x1c\x1a\xfc\xc4\xbfr\x12\x16V\xee\x97\xce\xbf\xc5Ta=\xe3\xb7\x82\xbf\x8fO.\xc8@&\xd2\xbfp+\x87\xad\xaf\xab\x8a\xbfq\x1e\xda\xcd\xa2\xbf\xe6\xbfhr@C\x1b\xa3\x98?\x8e\x12\xd6\x8fi\x95\xb8?\x96(U\xd2\x1a\x90\xce\xbf\xf2\xa3\xac\x7f\xa4\xda\xe2?p\xc5\xed\xe3\x7fk\xe5?@u\xbfe=a\xa6\xbf0\x1b\xd8\x9f\x92\xf4\xd7?\x12\t[\xa2s\xdc\xda?C\x04\xcb\x95\xd7f\xb1\xbf\xec\x13`\xca\x1b\xad\xb8?P\xe5\xe7\x95\xa5\x00\xb1\xbfrE93\xed\xb4\xdd?\xe4\n\xa7\x03\xc92\xd7?\xbc\xe0?D\xac#\xc3?l-\xcc\xc2Q\x13\xa0\xbf\x1b\xfb\x8f\x85f\x88\xbf\xbf\xe4\x87\x06\x82\x01D\xe1\xbf8\xad\xea\xbaj\xf4\xbd?\x10\x01\x11a\xf4\xc5\xde\xbf\xbc\xa9\xb8_\x85Z\xe1\xbfqY\xd3x9L\xe5?\x14\xae>y\xe6I\xcb?\xc4\xfe(\xfc\xff2\xb0\xbf\xc8"c\xf8\xe9\x89\xcb\xbf\xd2\xb7\x08l\x12\xa4\xcd\xbfpP4\x13\xe2\xf3\xd2\xbf\x02\xc5\xa1\xa8\x8c\xe9\xde?\x05\xc6\xf6\x8f\xac\xd7\xd2?\x8a@}z\x92\xd0\x83\xbf\x85e\x9bz\xad\xe1\xe9\xbf\x15\xed\xd27P\xf9\xcf\xbf\xd7\x9a\xdb\x84\xf5\xad\xbd\xbfF\xe2r\xa93\xf7\xc0\xbf\xd13\'\xed\x85\x01\xd6\xbfh@\xfa\xc1\xafx\xc9?\xd8:\xc6e\t\xad\xd3\xbf\x07\xef\x11A\x0c\x90\xd9?\xa2\x000\xcc@\xb0\xd1\xbf`\xbbNv\xf4\x99\xb7?\xac\x04nf\xf4a\xe0\xbf)d\x08 \x88\x11\xe0?\xc8h\x85\xb0c\x8e\xde?\xa8\n\'\xaf\x04\x8e\xbd\xbf\xb0^#S\xc4\xb2~?R\xe0:\xf7\xfa\xd0\xce?L\xf5\x16\xcb\x92\x0f\xbe\xbf!\x85\xe1\xbf\x94>\xc4?>\xdbb<\x9e/\xbc?\x05c\x93\x84\xfe$\xcf?Q\x1c\xc0\xfd0\x08\xe3\xbf\x92z\x83\xc4\xaa/\xe4\xbf\xc2-\x05yT!\xd2?\x0e\x0f\xc8\x83\xa5\t\xaa\xbfz\xae\x17\xf8\x903\xa2\xbf\x14\x08\xf4\x98AB\xed?\x88\xff\xcbK*8\xb7\xbfJ\xe4h\xa0QT\x9e\xbf-\xcc\x01&,\xe9\xc4\xbf\x0c xn\xcc\r\xcc?c\xceK\xcd\xe5\xad\xd1\xbf*\xcbAML\xe3\xdc\xbfB\xa6\x89\xe0\x91\xa1\xc7?\xd3\x0f\xddX:\x8b\xb2\xbf\xb8\xaf\xb7\xb0\x1f\xa1\xe9\xbf\xa2\xf7\x02\tj]\xc2\xbf\xda\xf3\x06\t\xc2\x10\xcf\xbf\xade\x1f\xa1Pp\xc0\xbfHe\xe9$]\xdd\xc1?]\xb8\xc6y\xaaT\xa5\xbf\xfa=\xd3\x8eh\xfe\xd8\xbf7\xf0\xe9\x81|\xc8\xce?"\xc6Uj\x04\x95\xa1?~:wQM\xab\xe1\xbf\xb9\xad\x9c}b\x86\xd5?\x86r\x10w\xad\'\xc2\xbfT\xb5?9\xe5\xe4\xe2?' +tbg1 +(g2 +(I0 +tS'b' +tRp6 +(I1 +(I10 +I8 +tg5 +I00 +S',\xf3\xd0\x90-\x07^?I\x18\xda7\xce\x97o?\xabN3YkGq?\xcf\xb3\x88\x8a\xbc\x91c?~A\xd5\xe7\x11~t?fT\x08=\xdb\xfau?\xb9\x9c\xcelB\xafm?\x85\x0c\xd0%\xd4Dg?\x10]vv\x9bto?R\xec~\x89V\xec@?"@\xaa#MLm?\x08{.\x8f\x87\xa3`?\x93D\xc1\xa3v"x?\xa2v\x1e\xff>*k?\x08p\x99\xca\x0b\x87c?b`P\xb2&zs??\xb5O,\x92\\@?\xdc\xfa\xa1B\xad:]?j\xd9\xc9Y\xf2\xd3`?\x18\xac\xe8\xb9O\xd4f?\xb9P)6M\xa1a?\x91m_m\xf0\xd8g?-\xec\t\xd6\x8a\xcee?\xd3\xa6B,\x9eFa?q\xac\x8a\xbf\xd2Ke?%\xc2/\xd9d\xfdd?:J_\xde\xf6\x88\xfe>w\xc9\xb3>\xb8\xe42?u\xce\xc8,\xb2\x95e?"\xd2\x13\xf0\xb9\xa9t?\xbf\xb3\xcb\x8fE\x14l?@\x16cQ\xc9Mj?c\x19K\x16\x8f\xd2~?T\xb0\x18\xe1Kbx?)<\xf7\x1do\x1bt?\xf3\x8eN\xe3\xcf\x96{?\xd3\x0c\xf0\xa6V\xb7+?\xb4JQ\x9eyy\x7f?\xef\x04\xac\xb4\x80Bm?k\x7f\xbb"1\x17q?\x89G\\&\x98\xf73?-\x88um7\x03L?\x00\xa6\xa2\x13\x08\x95Y?\xf6]F\xba\xe5\x88;?\x07\x87\'\x8b.\x93I?j\xb3k~\xbb\xf8\xda>J\x9ee\xfcW8`?\r\xeb%\xd0\xc6V`?#\x0b\xf0M\xdd\x8cP?t\xab\x00\x18H\x1bg?\x15\x0c\xae6\x9c\x9ck?\xcc)S\x04z\x87L?\xfeN\x9a3\xf9Lk?\x1e\xc4\xc9\x11\x12\xa5U?r\xa3\x05s\x08\x1fh?\xec#C\x84u@p?s\xea\xda\xff\xe5\x04\x17?*NM4\x0fWI?nbef\xb1\xd3a?\xf6\xe3\x19-\x99\xe7^?\xc9\x14E\xca\xea!p?z\xa2\xc7t\xdc\xeda?\x83\xced?\xd0\nj?:}\xa6{E3r?3\x0c\xc5\x99a\xccd?\xa1\x92\xd3\x02\xa1\xec\x1f?\xe7\xb7\xae\x8e\x0e:e?\x85[\xc7\xcb\xc8\xd8[?5\xe2W\x01\xaf\x9eo?\x13\x1c\xc42\xbaqo?\xcf\x10\x19\x9c\xf9#e?\xd4\xe0ch\xf98d?\xdc\x01\x13\xca\x7f\x15X?\xd3\x83"I\xe6\xafx?\xa6CK\x19\xab=\x80? \x05\xf7\'\x87=\x80?\xd3\xd5\xa2\xb3%j\x81?\x807\xd3\xdb\xd5\xa0W?Q_\xab\xede\x94s?\x9aq\xceO\xbah\x1c?' +tbtp7 +. \ No newline at end of file diff --git a/python/test/testdata/test_eigenvalue_allocation_input.pkl b/python/test/testdata/test_eigenvalue_allocation_input.pkl new file mode 100644 index 0000000..f286585 --- /dev/null +++ b/python/test/testdata/test_eigenvalue_allocation_input.pkl @@ -0,0 +1,37 @@ +cnumpy.core.multiarray +_reconstruct +p0 +(cnumpy +ndarray +p1 +(I0 +tp2 +S'b' +p3 +tp4 +Rp5 +(I1 +(I64 +I64 +tp6 +cnumpy +dtype +p7 +(S'f8' +p8 +I0 +I1 +tp9 +Rp10 +(I3 +S'<' +p11 +NNNI-1 +I-1 +I0 +tp12 +bI01 +S'q\xc8\xdc\x8d\\\x08\xf6?\xacz\xec\xc8\x16B\xf9?.;\x93\xd7Y\r\xf1?}T\xe5G\xfd%\xf6?\x02\x8b@\xfd\x03+\xe2?`\x16.0\xf5\xac\xf2?JV$*\xaa\xfe\xf8?\x1c*\x9d\x9d0h\xc1?3yU\x8a\xf5C\xe4?\x98\xc5\x0b\xd7\xfd$\xf7?\xe00$\xd8l\xe6\xf3?\x94:\x93\x88\r\x87\xe9?F\xafT\\\x9d\x19\xd6?G\x95\xa5\x94\xc4\xac\xf0?\xd0\xc1\x94J5\xfb\xfe?x\xc9\xdb\xaaZP\xeb?\x13\x92{\xfbG\xd9\xe2?\xaa\x8d>~\xa1c\xf6?\xf2\xebg%\xf3x\xf5?v\xa7\xc7\xeby\\\xec?\x9cJ\xd4(\xed\xd5\xf0?]d\x93IcZ\xf0?\x1a\t\xf4\x91\xf1\xf8\xf3?\xfc\xfa\x88\x0f4\xe1\xc6?q\x03\xc3\xca]\x15\xf4?p9r\xac\x94<\xf2?\\\xbd\xd3s\xc6\x12\xf0?l/\xb0\x92\x0e_\xe8?\x8a\x9a\xae\x93\xe8\xe9\xf5?ov\x03\xad\xb6w\xfa?\xdf\xea\x8e\xefp\xcb\xef?\x99\xf3 \x83xN\xf7?\xfb\x03\xa9N\xda\x16\xec?f\xde\x13=\x1fE\xf0?X\x1f\x1c\xe4\x19\xdd\xf0?\x89i\x9b\xe8\x87\n\xe8?\xaf\x00\xc6,\xd8q\xf0?\x80\xf1:\xbb\xb5\xc5\xea?\xc0^7\x94\xfe\xe3\xf9?\xfa!Y^\x95\x8b\xf9?\xd0\xaaK\x8f\x17~\xf2?\x16A;ku\x1e\xea?|\xf41\x14\x89u\xe5?j\xdd\xdf\x112*\xf5?\x92\xec\xd0ZE\xaf\xf3?*\x96\x8c\x81\x9c\xcd\xe4?t6\x83\x06\xd7\xf0\xf7?u\x9cey\xe6\t\xe4?\xfd\xfb\xfdT\xdf\x91\xff?\xd4\xd7\xa3D\x81\xeb\xca?\x12\x83\x88nnk\xf0?\xc6\x85\x9ah\xba\xb5\xf7?\xc6\x90\xbc*\xfc\x1e\xfa?U\xcf\x1f\xf2\xbe\xd0\xee?\x15\xbd|\xe2:E\xf2?\x06\xe2\xd5OA\xab\xf8?\x995;\xa0\x02`\xe3?\xad:\xcct\'\xb7\xea?\xd6\xab\x93\x9e1X\xf1?W6\x92$e\xbb\xf5?\x8f\x94\x17\x1a\x13\xac\xea?\\\xa8\xfcA\\\x81\xf6?\xa4\xf8\xc8W$a\xed?z\xae\x04\xf1\xf4d\xf7?\xacz\xec\xc8\x16B\xf9?\xa0T\x0f\x86\xb0\x81\xed?\xb1\xad\xc2\x9c\x82\xa4\xe5?\xb5\xe36\xda\xf9\x92\xec?\xc1\xac&?"\xb8\xf3?GGk\xa5\xdaU\xeb?\'R\x81`\xedX\xea?R\xa6\xa2\x0e\xadd\xfd?X>r\xe8\x8c{\xd2?\xb0\xfbT\xa9\r\\\xdd?^/\xc9V\xfc&\xf0?\xfe\x9a\x90pb \xf2?U;N\xa0\xb5\xcb\xef?\xc4b\xd1r\x16\x13\xeb?\x1c\xddD/`@\xf1?\x84E\xb5c\x83\x1c\xf2?\x19B\xef\xa6\xf8\x0b\xf2?\xbc\xeej\xad\xe1\xa8\xf2?0\xe6\x10\xf0\xf2\xb3\xf0?\x8a\x96t\x82a\x8b\xf1?\x03b\\\x01i\xd5\xef?\xf3\xf2ZA!\xb9\xf6?\x9d\x08}\x1c\xa1\xf5\xec?\x9d\xc80\xc7s\n\xf7?J\x0b\xb8\x9cjj\xf8?a?p\xe04\xb1\xe5?B3\xa0\xc7\xa35\xd3?\xb0\xcbe\x96\xbd2\xf2?6{\x11@\xda\x82\xea?%\\\xc2o\xf1\xd2\xed?l\x06\xed\x82t\x08\xf7?\xc2\x83\xbc^\xcb\n\xeb?\x1d\xada\xde\xd3D\xf0?S\xeep(\xe9\xde\xe2?0\t\x82\x0eB\x85\xa4?0\xb2\xa5\xa1\xa3$\xe5?Z\\k\xa6\xac\xd2\xd6?\x0f\xb1^Y\x80N\xe0?A5\xfa\'\xb4[\xe8?\xa0h)\xe6B\xbc\xce?\nK\x1fX\xf6\xdc\xee?\xac>\xccMd\xc7\xc4?\xceW\x98\xe0g]\xe6?\xf6\xb5.S\xdc\x00\xfa?b\xe8\x8bI\xaf\xed\xe6?\xae?\xdc\'j\xde\xf0?Tt\xb3\x18D\xd2\xce?(\x03Px\xbe\xae\xe7?\xf40O\xb2?\x0e\xf3?\xa6\xeda\xb6)+\xf0?\x8cQ"\xfe\xc0\x8a\xef?\xb1)\x7f\xf0\x14<\xea?2\x1d\xab`\xa18\xdf?2t\xf3M\xad\xdf\xf5?\xe9\x15\xf6Px\x9c\xf4?0\xf8\x13\x04Z\x1f\xe8?\xb0\xae\x10\xa5\x8c\xff\xf4?\x07\rZ\xe3\x8cz\xe4?\xd6\x9e\x9a\x91nU\xdf?\x18\xd9\x0e\xd5\xa7\xa4\xd7?Y\xec\xff\xe2\x89\xd6\xe5?\x9c\x1e\x1f\x93\xec\xa6\xcb?\xa2\xf3\xa2\xf2\xcd\xd8\xf1?\xa6\xcfaX\xab\x03\xf4?.;\x93\xd7Y\r\xf1?\xb1\xad\xc2\x9c\x82\xa4\xe5?r\x8er7\\\xd3\xe8?$,\xc2(\xc4\x98\xfa?\xc25OZNo\xf8?\x9a\x00\x88\x96ST\xf5?\xf4\xe3\x14\x1a1\xd6\xd7?n\'\xf0!\xf9\x9b\xf1?\xa0u\xe8\xfe\x9b\xc3\xe0?\xae\xef\xf6\x10z\xc2\xf1?\x02\x1e\x86\xcd\x9f\xf0\xf4?\xfc\xc2\xba\xfd\xe2\x11\xf6?\xec\xfd-\xe9\xb9h\xd4?xn\x1f\xd9\xb4(\xb0?\x8f\xea\x98a\x8f-\xed?\xe4W\xa6\x19\xc4T\xe8?\t\x1b\x1c\x02\t\xe4\xee?6TrnI\xba\xf8?\xce\xd4T\x83\x18\xda\xe4?\xbb#,\xe8|k\xec?\xf2*\xfc\xf3jo\xfd?\xfd\'\x03\xcc\x0e\xd7\xe6?\x87\xfd\x88\x18\xbck\xec??8B\xf2J\x80\xee?\xbc\xe1B\x07\xae\xe9\xf1?s\x11\x07J\xf0\xb0\xe7?\x91\xbf\\g\x14\x82\xf7?\x1d\x8ac\xa8g\x94\xec?R\xcfu%\x1d\xec\xe2?,U\x8bs\xa3\x81\xf3?\xe2[d\xad\xbd]\xd2?\x8d\x88\xe3Y\xca\x84\xf5?x\xb6\x16\xa5\xac\x07\xe8?8\xc1d\x111\x0b\xed?e>,\xb5u\xfa\xeb?\xe6\x05\x1ar\x88\xea\xf7?\xb4\xc3r\xbd\xb2k\xde?\xa6Q?)\x97\xd6\xd7?\xb8*\xc9x\x8f\x1d\xf5?\xc6\xd2\xb4\n\x14f\xf2?y\x8b\xc0\xf4\xe7\x04\xfa?ae\x80\xb1\x84H\xf6?\xf7b\x84W\x891\xf6?\xcd(t[\xa26\xf3?\xe12 \x13\xd0>\xe8?\x90\x1f\xf1r\xad\x0e\xf0?\xb9\xc1\xd7\x94\x81\x02\xef?\x1d\x18\xec\xe9\xc9\x19\xef?\xd4\xf5+\xb1\xc6\xeb\xf6?4t(\x1bb\xea\xec?B\xbd\x01\xc6\xff\x90\xee?\xac\xce\x94ge\xc3\xf6?\x8e\xe3\xf8\x15F+\xf0?D\xca\xb4\x99\xe5\xb0\xf2?\x08\xc0\xde@\x13\xc7\xfa?\x11\xa8\xee\xdc\x1b\xf4\xf9?\xcc\xb1i\xc0\xe6\x88\xf4?\x1a=\xed[*B\xf6?x\x11e\xcf\x18@\xf5?\x8eQ3D\x1b\xd8\xf1?\xd0G\xcen\r+\xf1?[\xf9\xf1\xceL\xa4\xf3?$\xb3\xa9\xf8\xe1\x06\xe0?\x96\x94\x84\x86<\x9e\xf2?}T\xe5G\xfd%\xf6?\xb5\xe36\xda\xf9\x92\xec?$,\xc2(\xc4\x98\xfa?\xecq\x8a0\x06\xe9\xf4?\x84\x13\xbb2\xf8\xd5\xdb?\xe0\\M\xa5\xaci\xec?\x07\xd8\xd3MdI\xe7?\x92\xa4\x04Qn\t\xf1?\xca\xecz\xb2!\xb1\xf8?\x9a\x97\xeft\x97*\xf0?\x05}M"\xfe\xca\xf5?\xe0\xdaek\xcd\xe2\xf0?D\x0e\\\xa3\xd1\x06\xf2?\xf5\xdd0\x83\xc7v\xf3?b\xe9\xbbR_\x81\xec?L\xd3\xe8*\xba\xe6\xf6?\xd5\x94+\x82Y\x91\xf2?\xa6\xbdB)\x15}\xf9?+\xcbv\x93A\x9e\xed?\xcc3\xeaT\xf7\x1a\xe0?\xda\xfd}9{\xba\xfb?\xa2\x1c\x0c\x92\x01\n\xe1?h\xcf\xdd\xc2\xb7\xd7\xef?V\x1b\xcay\xa9\x06\xe1?\x06<\x83b\x88\xe8\xe6? \xdd\xd0YC\xc0\xe9?\xe3\t\xbe\x90\x1b\xc0\xfc?\xdeC\xca\x88\x86\x9e\xf4?T\xde,z\xf4\xe3\xee?\x8f\x1fz\x1e\x99-\xec?\x8c\xdbE\x00\xfe\x93\xdf?h^>y\xc2\x0c\xf7?\x8a\x18\xcd\x03$"\xed?\xe8\x15`\\\'y\xdd?$/4\xdf\xe1<\xf7?\x91; &\xbd\xdb\xf3? \xa8\xd1\xbd\xca"\xdd?\x8c\x95\xb3\x9aT9\xfb?VY\xa9\xa9\xf4\xd4\xf7?\x02\xd6\xab\t5\xf7\xf2?1\xa9T\x0cu\xc3\xe6?^\x97.0\xb8\xfa\xeb?\xe4\x18\x1b\xb7\x1f\xde\xf6?==\x87 \x0c\xe0\xe9?P\xfc\x84\x02\xad\x10\xeb?r\xbeZ\x8c\x1cq\xf5?N4\x10^\x96\xc9\xf0?w\xb5\xa2F\x0c\x8d\xf2?\x02\xd5\x83\xc1\xf8K\xd4?\xc5\x97\xc3\x9br\n\xee?\xa4#\xde\x12f\x8d\xcc?:\xaf\xa4\x04\xc4\x1a\xe4?\xff\xc6L\xe4\xf5\xb9\xff?\xfc=\xd0\xf8\xb1=\xf6?\x80\xbd\x07h\xf7\x12\xf5?2);(\x04@\xf9?\x13c\x06\xb4$&\xe4?`\x83\x92\xc76\x0f\xf0?p\x16T\xf3(\x9d\xe6?\x131\xf6\x0e\x87\x99\xed?~2s\xbd\x9c\xf8\xe9?,\xaf\x19ac\xfb\xf9?\x18R\xc2\xff\xe8\xa2\xe5?\xa6>\xfc;\x17\xe9\xe8?\x02\x8b@\xfd\x03+\xe2?\xc1\xac&?"\xb8\xf3?\xc25OZNo\xf8?\x84\x13\xbb2\xf8\xd5\xdb?`\xdb\xd6\xe1X@\xc3?\'\x00\x01\xaf\xd5\xc6\xe5?\xd6\x16\xf4\xc4\xa1\xd0\xe9?X\xe7sNpb\xf1?\xbc\xef\x01\xef.\x10\xf4?0o\xad\xf0@\xdd\xf0?6+\xeb]\xd9\x95\xf3?\xc2\xf0\xec\x16\x9a\x17\xd2?\x87\xe0\xd9\xb4\xb2B\xf2?\xf0\x92\x15\x03\x87\x1c\xf4?\xca\xd5\xbd\xaa\xce^\xe7?\x0eO\x89\xd25\x1d\xf2?\xe8`\x0ffw\xdc\xf0?4\xd7\xa5\x1a\xbf\x9f\xf2?\xf4\x9awGG\xff\xd2?\x88Jm\xf9\x0by\xf7?/\xdaq\xf2\x7f0\xe9?\xdd\xc0{HL\x87\xe6?\x02\x01\x96^I\xe1\xf0?Bc\x1d7\xa1\xd7\xf0?6I\xfb\t\xeb\xd4\xe3?B\xca\x06\x96F9\xe4?\xa2\xd7A$\xaa\xd8\xf3?9W\x9d\xb1Z\xf9\xeb?^\xd6\xfe%\x0c\xf1\xf1?\xd8\x03\x8a\xf8\x00!\xe3?\xef3Q\xf6\xc0\x14\xe5?\xd9\x0f-\xc6Wu\xe5?;\xc3J\xb4\t\xc6\xf4?"[T\x1d\x97\x07\xf5?]\xe06\x8c\x0b1\xed?z\x12l\x84\xb7?\xef?\xf6\xe2\x02\xebB\xb3\xef?21siC\xdf\xf7?\x1fBY:\xdd\xfc\xf1?\x07\x8a+\xda\xf5n\xed?\x92Q\xaf\xe9\xfb\xc2\xef?\x86\x93\xdd\x9d_\xf3\xf3?\xb0\xc4\xb6\x1d\xd3\x87\xed?gwy\x93\xbd\xdf\xe0?\x82\x89iA\x88\xce\xf2?\xf6+\x9c\xf4GL\xf1?F\x90\x8d\xa2\xb0G\xfa?\x16K3?\xe6V\xf0?H\xee\xce\x8d\xac\x01\xe7?\x12\x80\xff\xb8[\x03\xd4?\xabN\x02bU\xf4\xee?\xae\x13k\xb3\x89\xeb\xf8? \xd3\x85\x8c"\x1e\xf7?\x06E\x19G\x9f+\xf6?\xaf\xe2}\xbc\xe1\x8d\xf0?\xf2\xb6&>\x9ar\xec?t`\xbc\x10\x0c6\xfc?\xaf\xe9*I\xc5\xf4\xee?\xf4\xa5\xb6R\x14\xe1\xda?\x7fj"\xa1`\xe1\xe1?\xc5\x97\xff:^p\xe0?\xf2zk\x83\x1a\xa4\xfb?!\xb0;\xacp#\xf2?|\x14\xac\x07\x9bk\xf9?`\x16.0\xf5\xac\xf2?GGk\xa5\xdaU\xeb?\x9a\x00\x88\x96ST\xf5?\xe0\\M\xa5\xaci\xec?\'\x00\x01\xaf\xd5\xc6\xe5?`\xb7e\xc7\x80%\xb5?\xeb\x87\xa2\x97D\x99\xfb?r\xa9\xb5mYH\xe6?dl\x9b\x1a\xbd?\xd3?\xbd\x8d\xab>8\xb7\xef?\xb9\x18+\xe8\xe9\xd0\xe1?\xf3aF\xe1\xcc*\xe6?\xb2\xdf:\x98"\x86\xf8?J\x84R\xe5bR\xd7?03\x89\xc52N\xfb?W\x9d4:\xdf\x03\xf4?\xdd\xea\xf8\xab\xf1 \xe9?\xc4\x7f\xe6\x19"?\xee?x\xfa\xdf\x08\xf3\xe7\xcb?\xb5pX\xde\xb3\xee\xe7?\xcd\xf4(\xfa\xf3\xdd\xf0?\xcc\x9f\xcel\x8b\xd4\xf8?\xcc\x0f\xa21\xf5\'\xf7?\xc6\x8b\xd5h\xc3\xa1\xf2?\xc64\xcd\'\xcds\xf4?t|\xfeKa\xd1\xfb?\xb5U^l\xfbY\xe8?\x83@\xb6j\xb1:\xf1?V\xd2\xfc\xb7\x148\xf8?\xc0\x99-\x15\xab\xc9\xe7?\x96\xf2\x0bNB\x8a\xf0?\x86\x12K\xa8gu\xf4?F:4\xee\xb3.\xd1?r\xcc\xfe\xfb\t<\xfb?5\xe6B\xcfU\xcd\xe8?\xfb\xf6I\xc9\xe1\xc1\xe0?t.\xa7\x1f7\x8f\xed?\xf8\x9c\xb2\xb3-b\xe7?\xa2w\x0f\xee\xe57\xd9?\n\x15/7;\xcb\xf2?\xd6\x8b\x90\xce\xaf\'\xe3?\x9c\xcb\xe3!Q\xff\xe3?\x1c\x1c\xe9=\x8d<\xee?\xca\x94\xc9\xe8\x1b\xa5\xed?\xb0\r\xcab<\x01\xe6?\xcev\xba\x89\xf4\xeb\xf3?\xeb4\xe6\x05\x85\xc8\xfa?\xbbG\xee\xbc\xb5\xbb\xef?\xe0\x8d\xfb\xbf\x80\x0e\xdd?\xcb\xa5{\xb6\xaf\x8b\xe0?\xae\xac\xa6,\xbd\x0f\xea?\xac\xa6\xaa-e\xc4\xf1?~t\x9ba\x86 \xf7?\xa3,\x16\xb9\xa7P\xec?\xb1\xb5\xca\x96\xc3\xf7\xe9?z\x01x9G\xe2\xf3?\n\xd1\x12\x10\xdb\xd8\xf9?\xd0DR\x90\x85Q\xf3?\x8cI*\x0e\xb6c\xf7?(v\xee\x81CC\xcc?\xf0\xe9&\xe18K\xfa?\xcf\xe7\x91\x90\xe7\x15\xf7?\xfc\x8b\xa5T&H\xf0?\xab\xef.V* \xf0?JV$*\xaa\xfe\xf8?\'R\x81`\xedX\xea?\xf4\xe3\x14\x1a1\xd6\xd7?\x07\xd8\xd3MdI\xe7?\xd6\x16\xf4\xc4\xa1\xd0\xe9?\xeb\x87\xa2\x97D\x99\xfb?\x17W\xb9\xd0;\xd1\xff?m\xeb0\x10/\xfd\xf6? &\xfa\x15V\x1d\xea?\x0fQh\x8b\xb7\xc9\xf2?\x9c\xeb7f\xe1\xe0\xc0?\xdb\xd8M$\x9f\xbc\xe4?\xf7\xd5\xd7\xf6\xd7\x1d\xfd?\x0e\x8c\xed\x98\xcd\n\xee?\x8c!3K\x11\x1d\xde? m\x10Z\x8b\xae\xf4?\xf4\x13`?\xc5\xc6\xde?\x15y\x0f\x87\xc1\xda\xe5?\xc0\x93v\xfb\xd3l\xe1?:\xffC\xcc\xd2\x1d\xed?\xee\x0ce\xd6(\'\xe7?X\n\x96V\xcc\xff\xe4? d\xf1\xe7\x8f\xce\xcf?\x80\xe6\x15>\x17\xd3\xd1?!\xec\xbbdl\xeb\xed?\x0e\x00=D~\x7f\xf1?\x90"\xd75\xac4\xd0?\xe4\x8b\x87\x1a\xc0n\xeb?${7\xfe\x0b\xf5\xf3?{\xa9m\x12\xe7\xb9\xf0?T7E\x18\xb5m\xee?\xd8>\xa0\x00\x02]\xb6?;K\x11)\xe4\xf1\xe3?%\xb64\xba\xce\xd5\xeb?\x98\xb2@\xa6I\xef\xf5?\xec\xc6\x04W\xbe\xc7\xf6?wa\xf1y}\x1c\xea?\x08\xe8Q99\x01\xe3?\xb0\xc8\xd9\xad\xf5\xf7\xb6?\x1eA\x9a!+\x1e\xd6?:Oi[%\t\xe5?\xb2\x95\x8d\t\t\x9b\xf1?#\xd6~GS>\xe2?\xaa\x04mb\x99l\xe7?\xe0\xa1\x17\x80\xc3\xb6\xd4?\x18\x8e~\xf7\xf5+\xfa?\x03\x1f\xfe\xd9\x91\xa1\xf1?\xdc\x86(\r\x92T\xf5?\x9dQ\x97q\x1f\xee\xe9?\xc6\xd0\xae\x88\xf6\xe4\xde?\x1d\xa2\xe6\xaeI\x9c\xf0?\xd8\xdbZb\x18`\xf1?t\x1c\xc8\xcd\x02\xaa\xd4?\xba Bx\x19g\xee?^\xc7\xf0)\xd2\x81\xd8?\xf9\xc9F\x14\xb7\xdc\xed?ch\xcf7RR\xef?\xfa\xbd\x16\xd860\xe0?dF\x82\xad\x1a\x86\xdf?\xcf3h1\xbd\xd1\xed?\n\xfc\xe9K\xd0\x93\xf1?\xd9\xa5\xf3GG8\xf3?E`\xacg&\x9f\xef?\xcd\x9d\xbb6"\xe1\xea?\x1c*\x9d\x9d0h\xc1?R\xa6\xa2\x0e\xadd\xfd?n\'\xf0!\xf9\x9b\xf1?\x92\xa4\x04Qn\t\xf1?X\xe7sNpb\xf1?r\xa9\xb5mYH\xe6?m\xeb0\x10/\xfd\xf6?N\xe1\x824F\x1c\xe2?\xee\x86T\xef\x08p\xf5?i_m\x90S\x18\xf2?y\xc7V\xb4\x96\xf7\xed?>\xe0I&y\xfa\xfb?L>o\xae\x17\xe3\xce?9\xe8\x82\xcd\xb0\xde\xed?\xcb\xdd\x95/\x10\xd6\xe4?P\x8b\x00\xc3\xaen\xf2?\xb47s\x18\xa1^\xd9?\xea\xb4v\xf1\xfa\xbe\xf4?\xffY1\t+\xf4\xe5?\x0e\xb9\xe7\xb0?n\xe9?z\xb6\x932\xbb\xd7\xf9?Xowj\xea\x9b\xbb?\x02/=\x93\xfdV\xf9?BV>\x81\xf9\x0c\xf7?\xfak\x00\xcd\xb2H\xf4?\xaf\xeb\x96\x84k\xa5\xef?\xb4~\x80\xe4\xacD\xe6?~\xf0\xaa\xed\xcc\x9c\xf3?NdW\x95N\x91\xd8?\x0e\xeb\x1eM\xec\x07\xec?i\xd5\xfck\xcb\xac\xfb?Vk\xf7\\G\xba\xe5?\xd6B\x18\x83\x13\xe3\xf9?\x8c&,|\x87r\xd3?D\xf0\x7f`\xf4\x95\xeb?\xffY+\x0fh\xd1\xf6?\xf6\xc5\xebr>{\xe4?\xf6;\xa20\xf4u\xf1?\xb2\x01\xbd;\xf2\x94\xe7?U*\xcdM-(\xed?\xa3Q\xd1\xb5FJ\xf4?\x1c\xf5\xbe\x1d\xe5\x03\xf2?\xa2\x11\x19\x1e\xf3\xc1\xd3?\xc62]\xd8\x12\xc6\xf1?\xe4X.\xab\xe9\x15\xf1?@b\xf1\x05!\xc7\xeb?Gg\xd7\x8c*\x91\xf9?\x11\xc1\xb7\x9a\xb6\x05\xef?`\xed\x03\x8e\x8du\xfa?O\xab_r\xff#\xef?b\xc7\xd5\x127\x93\xf3?\xce\xfc\xfa\xa9\x88j\xe7?\x88\x8a\xea?\x9e-\xee?\xed\xca\x8b\x81\xb3G\xe1?\xc2\\\xd3kP\x83\xf1?\x1c\xe1\x01r\r\x98\xd4?~c\xec\xad\x0bw\xf3?\xfe\xe6\xed\xa8E\x95\xf2?\xfe\'f{Lm\xf9?[\x83[\xee\x8d\xde\xe2?\xa6\xdb\x88=\xdb\xed\xf2?\x06\xe4\xae\x1d\x92v\xfd?\xf2\xb6\xfe5^>\xe6?"r\xe8\x8c{\xd2?\xa0u\xe8\xfe\x9b\xc3\xe0?\xca\xecz\xb2!\xb1\xf8?\xbc\xef\x01\xef.\x10\xf4?dl\x9b\x1a\xbd?\xd3? &\xfa\x15V\x1d\xea?\xee\x86T\xef\x08p\xf5?`\tB\x84\x9d\x8c\xf0?\xf0T s\x14!\xf4?\x12\xf3Co\xdd:\xe0?\xd6\x9b\xcfe{\xe9\xf4?r]\x88\xf9\x92\xa4\xf0?Q\xa6@\xe0d\x04\xf1?(\x8a\xaf\xa6\x89 \xc5?^:\x9c\xe6\xce\xb9\xf1?\x97vc\xd6ST\xe6?(\xa9\x9c\xab\x8b&\xec?H\x01\xda=\x00\xdf\xf2?@=\x00\xaa\xf2\x12\xd5?\xc7\xfd?r|q\xe1?\xe7[A\xe5\xacp\xe6?\xd6\x89\xb5\x02:J\xee?\xfe>z\x04\xd50\xea?\xa5\x0cY\xcdqS\xe6?]\x10M\xf6pd\xf5?\xf1\xeb\xae`\xad\x1f\xe3?\xf6}i\x9a\xc3\xbb\xd1?\x949/\xfd\xd3\xd2\xd7?\xdd\xa3JttV\xe2?\x98K\x04\x07\xb40\xc3?\x89\xd2\x88\x08\xd4S\xf6?t\xed\xf0\xe1u\x87\xf3?\x06TsH\xac\xef\xec?c\xb8.\xf15\xc2\xe7?\xde\x82\xd7p\xf2\x89\xd1?\xc0\xc8\x07\xb3T\xf9\xf0?\x08\x07\xc9U\x83\xb8\xe2?]\x14L\xc6\xcd?\xf1?\xdcw\xe1\x19\x05\xba\xf3?\xb6\xd9\xf8\x8fw[\xde?J\xa1\xf3\xa8\xad9\xed?\x10B\x94\xf3G\xd6\xee?`,\xa1;\xebe\xec?\xc6hhA\x1d9\xfb?\xcc\x17\x1f\xcaZ\x89\xec?zN\xcb\x7f\xfa\xf8\xf4?\xe3\x92u\xd3\xef\xc2\xef?o\xfd\xc2\xfbp.\xf5?\t\xff\xac\xd1\xaa\xc9\xe1?q\xd4\xa2\xefl\xe3\xec?t\x04[mq\xc9\xf1?(04J\xbc0\xf6?g\xca\xabJ\x93\xc2\xf0?\xd8\x9d\x86\xea\x1e#\xe2?\xdd\xf1\xe3\x8a\xf4M\xeb?n\xad\xbc\x19\xa0\x12\xf1?T\x90+\'\x06l\xe7?\xb8v{\xf8\x1b\xfe\xce?\x9c%\x87\xce\x84\x01\xf0?\x96\xce\x05\xde\x92R\xf8?\x13\xf0\xbej\xbd\xbc\xf6?\x9c\xee\x17\xda\x9a\xa4\xf6?0\xc8\xaaj\x11\xc4\xdb?\x98\xc5\x0b\xd7\xfd$\xf7?\xb0\xfbT\xa9\r\\\xdd?\xae\xef\xf6\x10z\xc2\xf1?\x9a\x97\xeft\x97*\xf0?0o\xad\xf0@\xdd\xf0?\xbd\x8d\xab>8\xb7\xef?\x0fQh\x8b\xb7\xc9\xf2?i_m\x90S\x18\xf2?\xf0T s\x14!\xf4?\x9d2Us\xf9*\xf3?F\xccRX\xd1\xc0\xd1?\xbe\xb8\x0c\xba\xba0\xf9?\x18\x9ao\xf9[\x93\xf3?R9\xda<\x7f\xc8\xe5?^h\xea\x92\xbb:\xe6?\x97\xb33\x01\x98:\xf9?.\xfc\x1a\x1d\xcf\x95\xe8?\xca,\xa6IB\x19\xf0?\x14\xa8\xbb\xf0\x15\x95\xf2?|\xa9d\xfd\xf9\xc9\xee?\xa4Za\x0c=\xff\xe9?\xa0|\xdf\x83Mm\xa6?I;\xeb\xaa~\xce\xee?\xd9\x16j*\xa7W\xe6?B-\x12cg\xf5\xf6??\xeb\x9a;\xde\xe9\xef?\x1a\xdb\x90\x06\x0fL\xeb?\x08\x08\x17@|9\xfc?z`\xfe\x9b?S\xf2?h\xc8\xe0\x15\xde|\xc2?\xa0\xb2\xc3\x1b\xb2\xf8\xf5?\xea\x1c\xab\xae\xae\x9b\xf0?\xd1\xd8[\xe2\x8f\x9e\xf0?\xacc\x90\xa4v\xc0\xf0?b\x14k\xd5\xaaS\xfc?\x1f\x80\xe7^\xd4\xec\xed?\xba\xd7;`&\x97\xf1?\xc6\xa8\x1bg\xa6"\xf6?\xca\xb1\xd6j\x96\xc8\xf2?\x9cI\xa3\xf5\x8d\x9b\xf1?}\x91>\xf0\x80\xbf\xf6?\xb2;(2\x7f*\xfc?\x1d\xb2\xca\xff\x15&\xf0?\xf2\xe5\xb3\xe3M\x00\xed?4\x85_\x9d\xe2^\xca?\x0e\x82q\xde\x8a\xe7\xf2?\xe6Z{\x19\xc6,\xf2?\x0eg\xce\xcbK(\xf3?\xd6\xdb\x91\xd3\x12,\xf3?\xfeq\xdea0@\xf7?t^n\xdb\xadm\xd6?*\xab\xdfQir\xf7?\xee\xa8@\xabi\x1b\xf3?\xa9`\x89mOC\xfa?3\xf6\xacM\xf4\xda\xe3?\x91\x18\x04\x12\x1e\xec\xec?\xc0\x03\x15\xc9\xc6\xe6\xf1?\x10\x14\x0exg\xf9\xf8?\x04\xea\xa9CB\x18\xf1?BZ\xf9j\xc4\xf8\xec?zX\xc8sk\xb9\xe3?n\xaa\x08\x9eF\x88\xeb?\x96\xc9\xf7\'7\xb4\xef?\x8f\x84\xaa$\xaf$\xee?\xe00$\xd8l\xe6\xf3?^/\xc9V\xfc&\xf0?\x02\x1e\x86\xcd\x9f\xf0\xf4?\x05}M"\xfe\xca\xf5?6+\xeb]\xd9\x95\xf3?\xb9\x18+\xe8\xe9\xd0\xe1?\x9c\xeb7f\xe1\xe0\xc0?y\xc7V\xb4\x96\xf7\xed?\x12\xf3Co\xdd:\xe0?F\xccRX\xd1\xc0\xd1?\x01\x01\xcd\xab\x03\xd6\xf3?\x8e\x9b\x16^&\x98\xf0?\xca\x06Z\x81\xf2?=\xbe|k/y\xf1?\xf3O\x97N\xde\xa3\xef?\\\x88\x8a\xf2\xafS\xcd?\x82}\xf1< 9\xe9?\xd4~>%\x860\xe8?\xc4e5\xc6\xd2\xdd\xf3?\xba)\xec08\xa4\xf7?\x18i\x94T\x05\xdb\xe6?\x9d:~b\xb6\xab\xe2?\xc2\xb3\xf2\x84\xeb\x18\xe8?\x94:\x93\x88\r\x87\xe9?\xfe\x9a\x90pb \xf2?\xfc\xc2\xba\xfd\xe2\x11\xf6?\xe0\xdaek\xcd\xe2\xf0?\xc2\xf0\xec\x16\x9a\x17\xd2?\xf3aF\xe1\xcc*\xe6?\xdb\xd8M$\x9f\xbc\xe4?>\xe0I&y\xfa\xfb?\xd6\x9b\xcfe{\xe9\xf4?\xbe\xb8\x0c\xba\xba0\xf9?\x8e\x9b\x16^&\x98\xf0?b_u\xc6\x82-\xf3?r\xe6\xb2\xdd\xd9\xb9\xe4?\xc4\x8cm\r\xcc\x99\xf8?\xc6\xfa\xa5\xcb\xe5\xbd\xdb?\x86\xb7u\xc0\x1a\x8e\xf3?\x9dB\xfa\x88\xd70\xe8?09\x1b/\x86\xc1\xf5?\xed\xf0\xb6\xd9\x9b\x0f\xeb?+\x94+\x02\xe7)\xef?=q}\xba\xf5\xb9\xe0?\x14r\xb0N\xf87\xd9?\xde\xf5\xbf[\xe2\x1d\xf8?A\x99\xa4h\xbd\x81\xf4?\xfe\x82\xca\xd1\x97\x9a\xfc?e\x19/\xf8DW\xea?g3`\x9f\x17\'\xeb?\xd6yc\x8b\x9e\xa4\xe7?5^\xc9\xb6\xb2t\xf2?\xd0\x10\xeb\xba\xb43\xe5?\x17\xa1\xe5\x81i\xf8\xf1?\xfdRT\xbc7\xa0\xe1?b=\xd4C\xbf\x98\xea?\xc10C\xbb8\x87\xf2?nQ\x83\xd0P!\xf6?8R\x80\xbc\x01\x81\xf6?\x16\x9a*i.\x00\xed?J^\t\x81\xfc\x8f\xfb?\xb9\xbb\x0e\xc1\xb0\x84\xf6?\x9c\xd3\xacP\x06\xc9\xef?\xb7A\xb4\xb3Y\xde\xe5?\x0f8GRv\x10\xe0?KAM_\xc7\x9f\xf1?\x0cT&\xda\x1b\xbf\xf0?D2p,\xd6\xaf\xf2?\xa0s4\xc8\xa1\xe3\xd7?\xed\x1e\xe1/a\x18\xf6?\xa6\xaa\x06\xc2\xb8]\xd3?.Y\xde\x05W\x9c\xf8?P\xb9\xde\xca\xf5\xd3\xeb?\xe4\xc7\xcd\x8b!\xe6\xed?U~\xc8(\xce\xb2\xee?\x16J\x1d\xf8\xddu\xf2?\x88\'\x97\xc4b\xa2\xd0?u\x1ap\x7f\x96\xb2\xef?\x08\x8c\xef\xc4\x04l\xdb?ZO\x8d\'\xfaP\xf1?\xf4H\xea\xda\xe0R\xdc?0{\xd0{\x19\xf9\xf5?l\x9aW]\x846\xf2?\xb8Dv1\x17\xbb\xf7?\xebH\x98\x87\xe3\xb4\xea?\x9c\x1b;\xd3\x92|\xd3?$\x1b\xdb\x829\xd1\xe1?F\xafT\\\x9d\x19\xd6?U;N\xa0\xb5\xcb\xef?\xec\xfd-\xe9\xb9h\xd4?D\x0e\\\xa3\xd1\x06\xf2?\x87\xe0\xd9\xb4\xb2B\xf2?\xb2\xdf:\x98"\x86\xf8?\xf7\xd5\xd7\xf6\xd7\x1d\xfd?L>o\xae\x17\xe3\xce?r]\x88\xf9\x92\xa4\xf0?\x18\x9ao\xf9[\x93\xf3?\xca.\'7\xfa?\x16\x8a-\x983\x06\xf1?.\x1a\xc7\xe4\x7fL\xf2?\xaf\x88\xae\x00\xe2\xa3\xe5?\x93\x06qO\xb6\x93\xf2?D\x95O\xf4\x8c\xb7\xf7?\x15\x80\xd3\x8d\x7f\x06\xf2?v\x19\xf2\xb0q\x88\xe8?t\x18\xf3\x0fo\xa6\xd1?x\x98\xdbI\xf2\x83\xf3?\xde}\x86:\xe1\x14\xec?\xbaJQ1\x905\xf0?\nClE\x96:\xeb?8PwI\xfa\x15\xe2?E2\x10\x8d\xfa\xd2\xf0?zC\xa8\x92\x8e|\xe4?\xbb\xb1\xad}B^\xe2?\x1f\xc3\xe0\x00"2\xec?\x88^\xa6\xf8\x9d\xa4\xcd?f\x18C\\\x9cb\xe7?\xca6\xec\xd3(\x14\xf2?\x07\x14\xf8\xc7\xb8=\xe1?\xb3\xa0!*\xb3\xb2\xfa?`\xf3\x14\xc9\xa0\x90\xf3?H{\xa2$\x17\xbe\xfc?\x12\x1a\x18K\xd6U\xf3?\x96\x90WH\xac\xa8\xfa?\xd4^\xb0`}\xc2\xda?\xf8\xa5\xee\x8cw\x0b\xf5?\xed\x08\xfbK\xf8\x9e\xf9?\xd8\xbc\x0c\xb87e\xeb?`98w0[\xdf?\x1a\xce\xb3\xba|\x02\xfc?\xdc\x98a\xeb\xffO\xf4?G\x95\xa5\x94\xc4\xac\xf0?\xc4b\xd1r\x16\x13\xeb?xn\x1f\xd9\xb4(\xb0?\xf5\xdd0\x83\xc7v\xf3?\xf0\x92\x15\x03\x87\x1c\xf4?J\x84R\xe5bR\xd7?\x0e\x8c\xed\x98\xcd\n\xee?9\xe8\x82\xcd\xb0\xde\xed?Q\xa6@\xe0d\x04\xf1?R9\xda<\x7f\xc8\xe5?[\x0bkk{q\xed?\xc4\x8cm\r\xcc\x99\xf8?aZp\xb9DB\xe3?4\t2\xa7\x00\x18\xe7?X\xa7+\xcc\x92#\xfa?\xfcU\x84r\xa1\xfb\xe0?2\x0f\xd1o\xde\x9c\xf7?\xa6\xa9|\xed\x0c5\xe6?\xe9-\x8d\x924\xd8\xf0?\x81\xbaC*\xd9W\xf5?>\xf9\x9d\x96<\xe9\xf0?/\xb6\xb5\xc7\xdcc\xf8?\x0f4\x8eJdU\xfa?\x06v\xd8GN\xe0\xf2?\xb2DKI\xe5\x05\xf2?w\xb7C\xfd\xa5\x95\xef?\x8b\x99#\xb9c\xf8\xf8?B]8\xdb\xf5\xab\xf6?i]\xbeY\xc4G\xf1?\x90\xae\xb1F \xbf\xf0?[\xd3A\x00\x8b\xf3\xf5?>0\t\\@\xbc\xd4?\xb3\xdb^e\xc4J\xe7?.4\xc6=K\xaa\xd1?\xad\xc1\xbd9\x9ff\xef?\x81N\xe6\x91Dt\xe3?^x\xa0\t\x13\x0b\xfa?"\x87\':D7\xd9?\xeee\xc2\xa9\xf9\x82\xee?\x99\xca\x08\xf9Q\x82\xe8?>\xe0\xaa\xf1Oe\xf9?\x11\x9f\xfb\x9fm$\xeb?\x12\x1e\xcfN\xb8\x13\xfe?\xf4\x05\xacoz\x9a\xfa?\x16\xeb_\\\r*\xe3?"\xd3A\x04\xcdQ\xf0?\xbeG&\x1fXv\xda?\x1d=\x19\xa9cA\xf3?\x0eg\x14\xbf\x11\xdf\xf3?F\xd1\xb8\xf9\x83\xfc\xe6?\x18/#F\xe1#\xf1?\n\xc8\xaa\xae\xc1\xde\xe8?fv\x9c\x99\xdc\xfb\xef?P\xfc-YE\x93\xf0?\xcc\x07\xea\x85]\xd9\xec?m\x03\x12\xf8\xa6\xeb\xf3?i\xf2\x02Mk\x8c\xf2?2\x8e,\x1f\x02\xd7\xf4?\xe0\xaa\xd2\xc4\x12D\xc8?\xf0}\x82U\xc58\xee?\xf2taj\x8c\xef\xfa?8\x9d`\xfc[1\xf4?\xf6\xd1\xd1F\x08\x8b\xed?\xf8\xab\xd8["\x0e\xf0?\xd0\xc1\x94J5\xfb\xfe?\x1c\xddD/`@\xf1?\x8f\xea\x98a\x8f-\xed?b\xe9\xbbR_\x81\xec?\xca\xd5\xbd\xaa\xce^\xe7?03\x89\xc52N\xfb?\x8c!3K\x11\x1d\xde?\xcb\xdd\x95/\x10\xd6\xe4?(\x8a\xaf\xa6\x89 \xc5?^h\xea\x92\xbb:\xe6?\x9c"a\xd4A\xbf\xfa?\xc6\xfa\xa5\xcb\xe5\xbd\xdb?\x97G\x17RV\xd4\xf7?X\xa7+\xcc\x92#\xfa?\x19\x80\x13,+\xc4\xf7?\x87R\xbd/\xa0\x8a\xfb?\xc6-\x86\x9b\xa0\xcb\xfb?#Ng aI\xf4?d\xba\x1fP"\xae\xeb?\xb4@<^\x16\xfe\xfb?\x00\xf9\xc6;sT\xf0?\x1e\x80\xfe\xa2\x91\xe7\xed?\x98|\xc8\x1a\xa8\xa0\xe4?\x13[\xbd\x83(\xb6\xe8?\xb8C\xce\x0b\xe4\x84\xe6?\xc9\xd8\x82\xf1\x11\xa6\xf0?\xe7n\xc4\xf56\xf2\xf6?\x10\xbei%\x1dV\xe9?\x05\x89\xdd\xbd\xe6 \xf6?\xf4e \xa2e\xb1\xf9?\xe0\x8e\xc9W\x13Z\xba?\xe8\xff\x94\xa7\x94*\xe2?\x82\xfa\x03Y\xb7\xfb\xdb?\'\xb0\x06w\xea)\xe6?\x0b3\xf3t,p\xf1?h\xf0\xb9\xb8\xa5\x07\xbd?x\xa6\xf6\xb2\xe5\x95\xe4?\xe4\x80\xc6\xe6+\x8e\xe4?A`\r\xc3\x99\x82\xe8?k\x9eJ\n\xf9o\xe1?\x1c\xb3\x15t\x08&\xf8?\x11\x0c\xe5\xde\x8a\x00\xed?\xce\xeavufB\xe7?\x1f\xf3\xca\xd9\xbbA\xef?\x0ft5\xcf\xa0\xc7\xee?\xf9i\x9b\xd6b\xeb\xf7?\xf97\x85*\xe1\t\xeb?\x17\x08,\x96\xb2\xdb\xf2?\xde\xd0o\xa5\xab\x0c\xef?\xc5\xeb\x1f\xef\xd2\xf2\xef?\xd2\xaa\xf2\xa4\xb0\x06\xf3?\x14\xbeU\x03\xc31\xf5?BT\xc5\xddZ\xaa\xf9?\xbd7\xd9\\\x0b\xfd\xec?\xd77\r\xb6p\xe9\xea?p\x98\xb9\x95f=\xe8?\xb4\xb95`{\xfe\xf1?\xdcP\xa3\xcb\xa5\xa7\xce?\xa6\xf6g0\x1f\xd0\xf5?\xbami{\xc5\xdd\xf2?,\xccb\x02\xe1\xb9\xdb?\xbcXG\xc7\x01\x15\xf8?b\x91\xcf8g;\xfb?\x8d@h\xf9){\xf3?x\xc9\xdb\xaaZP\xeb?\x84E\xb5c\x83\x1c\xf2?\xe4W\xa6\x19\xc4T\xe8?L\xd3\xe8*\xba\xe6\xf6?\x0eO\x89\xd25\x1d\xf2?W\x9d4:\xdf\x03\xf4? m\x10Z\x8b\xae\xf4?P\x8b\x00\xc3\xaen\xf2?^:\x9c\xe6\xce\xb9\xf1?\x97\xb33\x01\x98:\xf9?\x18\xdd\r\xb7\xa3#\xf1?\x86\xb7u\xc0\x1a\x8e\xf3?<<\x9a\xf8\xdet\xe4?\xfcU\x84r\xa1\xfb\xe0?\x87R\xbd/\xa0\x8a\xfb?\xf8\xa9\x1c\xfbf\xb0\xce?]\xa9SJ\x90u\xea?\xe4\x8f\xea\x980}\xf4?\xb6T\x96&T\xcf\xf0?|>x\x08?T\xf5?=\xef\x87\xab[\'\xee?\x08\xdb\xca\x86\x0c\xb6\xee?\xb4\xd5[1\x16\x04\xe1?\xf8\x10\xcb\xcb}e\xf0?8\x18\x13b\x85~\xf4?D}{\xef?\xe2\xf5?\xab\xb1\x0c\xbcop\xe2?\xba\x1aN\x9d\xdd)\xf1?>\x95\xd0\x18U\x8e\xf6?\nK\xbc\xacd\xe0\xea?\xd0\x1eA\x1d\xdd\x8f\xd3?\xc13\xa0\xcf\x9b\x17\xf4?.\xc9\xd6`\xd4\xf8\xf4?$\xd1(L\xad\xea\xec?6K\x98\x88\xb7`\xd1?\xc1)\x97\x1d\x1f\xa2\xfa?\x18\xdc\xc2\xf1j8\xf6?\xa1(\xf0\x13j}\xf4?"Lo\x97\x03`\xf5?\xe7\x98\xad\xb5\xd1\xed\xec?\x15\xc8D!\xf9\xe8\xed?t\x94\x1e8h)\xf2?\xaa\x92~z\x95p\xf0?\x91\xdc\xdb\xa9k\x81\xe5?\xa2\x92\xd2\xec\xd5\n\xf0?\xa2\x95\xfb\xa5Q\xa0\xfa?\x14L\x98\xf2\xe9\xbe\xe7?pl>\x0c\xe6"\xf2?2\x93\x9e\x1c:n\xf4?\x7f\x0b\x95B\x0b,\xe3?\xab\x8b\xf1\xabvU\xf0?\x82\xf7\xa7\xa9\xad\x91\xdc?\xd9\xb1\xf9\xe5Q\x01\xe6?\xce\xbb\xbb\x91\xdf\xb8\xf2?\x00\xc2`\x11&j\xe2?8\xcb\xa4\xb1\x90+\xee?+\xa5\xf4\xbc\xbf\xb3\xf3?\xbca\x03eo\xc5\xef?\x86\xe7\x82\x90\x9e\xd2\xf3?\x0e\xd2\xa4(\x8a\xcb\xf8?m\xa8\x92Z \x1f\xed?\xcej\x7f\x88\xb1\xd4\xf3?RZ\xbb\xf3\xec\xe3\xf1?\xcf}\x89\xa9m\xc8\xfa?\x13\x92{\xfbG\xd9\xe2?\x19B\xef\xa6\xf8\x0b\xf2?\t\x1b\x1c\x02\t\xe4\xee?\xd5\x94+\x82Y\x91\xf2?\xe8`\x0ffw\xdc\xf0?\xdd\xea\xf8\xab\xf1 \xe9?\xf4\x13`?\xc5\xc6\xde?\xb47s\x18\xa1^\xd9?\x97vc\xd6ST\xe6?.\xfc\x1a\x1d\xcf\x95\xe8?l\x04^\xe9\x80\xc1\xcf?\x9dB\xfa\x88\xd70\xe8?8\xe1\xa8\xdf\xd7u\xf1?2\x0f\xd1o\xde\x9c\xf7?\xc6-\x86\x9b\xa0\xcb\xfb?]\xa9SJ\x90u\xea?\x00!\xc2\x0b-v\xf2?\xec\xbd@\xf4~\xb5\xc7??\xe6n\x81B}\xea?\n\xa2q\xbe\x99$\xf5?\x18>l\xadE\x95\xf0?"\x91|0\xe7\x05\xfa?\xdc\x94D\x02M3\xe8?\x07\xfb3G\x86\xff\xe4?Ao\xef\xf0\x92\x90\xe3?\x8c!O\xd9f\xbb\xd3?J\xd2@\x1c\xc9R\xf1?\xd8\x82{\xa5]\x04\xfe?\x12\xfdx`S\xea\xd8?R\xe2G\x02\xab\x16\xda?t\xe8\x83!\x8b\xd8\xf8?\x04g\xf6U|\x8f\xf0?\x97\xf0\x7f\x9arl\xfe?\xc3eL \x19b\xf1?\xa4\x17\x8e\xc3\x14\xe4\xf2?8\x8fZ\xa6\x8f\xb2\xf6?4\xcb\xa9~\xab"\xf0?j;\x16\xfa\xae\xe2\xf3?\xe3;\x91\xb8\xbe\x9f\xf0?b\x1bw\xad\x1a\x17\xdb?g\xdb\x16*\x1f\xef\xf7?\x16\xc6\x13\xa6\xd9\x81\xdc?\xc4\xb45/\xf3\xb3\xf4?5b/\xd0\x13\x08\xee?\x1f\xec\xfe|\r\xd8\xe9?\xe7K\xe31\xd2>\xe6?\xbe\x81\x937\xa2a\xf5?\x82V\xcd\xddXt\xdc?\x94\xfd\x075\x9a\x88\xe6?\xaa\xb2\x8bu\'b\xfb?\x9d\xfd\xc0WJ\xd2\xf0? F*\x10>z\xfb?z\x065\x9b\x81\xb5\xe2?l\xd8{\x98\x96\xfa\xd9?/\xc4\x15\x95\xa1F\xe6?X\x9a\x12(\xe8\x82\xf0?\xf6\x85bpW\xa0\xef?6\'\xcbd >\xfa?\x80d8U\x18\x8c\xee?\xc8h\xf1\x031G\xd8?\xbe\xfd\xc0\x1d\xb0\xdc\xe1?\x90\xbd"\xeeow\xc1?\x7fLv)\x00\xbf\xf3?)\x96\xdd\xf3W\xbe\xea?\xaa\x8d>~\xa1c\xf6?\xbc\xeej\xad\xe1\xa8\xf2?6TrnI\xba\xf8?\xa6\xbdB)\x15}\xf9?4\xd7\xa5\x1a\xbf\x9f\xf2?\xc4\x7f\xe6\x19"?\xee?\x15y\x0f\x87\xc1\xda\xe5?\xea\xb4v\xf1\xfa\xbe\xf4?(\xa9\x9c\xab\x8b&\xec?\xca,\xa6IB\x19\xf0?\x1a4~\xbf\x1a\x8b\xed?09\x1b/\x86\xc1\xf5?\x91B\x05\x89\x92.\xfd?\xa6\xa9|\xed\x0c5\xe6?#Ng aI\xf4?\xe4\x8f\xea\x980}\xf4?\xec\xbd@\xf4~\xb5\xc7?{f?=\xf3\xdc\xf9?va\xac\xc1w\x8d\xf6?9\x92\xa7\x10,|\xed?B\xb2E\xb3\t\xdc\xf2?X\xd0\x987\x8e\xde\xed?\xb2\xa5mS2\x9b\xfa?v8v%S\x9a\xfb?\t\xd8\x1c\xa8H\xe3\xeb?\x91\x9a\xf4\xc0-}\xe8?:_\xc0Y\x92\x85\xf0?\xa4\x9f\x90\xf1v\x89\xda?\xd5\xa1\xe1\xb2\x7f\x1f\xf0?\xc2\x96\x84\xfc4\x86\xe8?\xa0\xd3x\xddT\x19\xf2?\x05\xb5\xeeL(\x96\xf0?\x1b\n\x85\x0cY\xb6\xe5?\x03\xad8\x87\xd0\xa4\xf0?\xbc(\xd1\xb8&\xa2\xf4?\xd8\xed\xf4\xf8\x8a\x91\xf1?\xee\xf6FE\xa6\x82\xfd?\xda\xb9\x81"\xabk\xe5?\x0c&:\xd54\x08\xf5?I\x16\xe3\x9e\xecf\xf7?~j\xf1\x99\x17\x9e\xd3?\x04\xd9n=\xbfQ\xd3?J\xd1\xf2\x08\xa4]\xf4?\xb8T\xdd\xcc\x1f{\xf5?\x03O\xc3\x90\xe05\xf0?\x03\xfa\xb0_Zh\xef?ig\x9c[\xccx\xe8?\xf8[\xcf>\xd61\xf2?\xdc2R\xfd+\xc3\xe6?3X\x08W\xe9B\xea?Xan\xaf\x9cg\xf9?=\x7fef\xa6\xf7\xf3?\xb2\x97c-\x87]\xf4?\xe4\xca4_\x19\xf0\xf0?\x05\xdf\x91-\x8a\x98\xe2?*\xcf%b\xaa{\xe2?X\x9d\x80\x0c\x82|\xfc?\xe0\x12\x99\x06\xd8A\xf5?\xa2\xa0\xdd\xa9QT\xeb?J\x03B\xda\xd9\x0e\xf5?\x12\x99\xd9\x0f\x93\'\xf0?/\x0c\x92\x18\xf0U\xf4?x\x9b\xb6\xb0y\x14\xe9?\xd6\x1aR\xcb\xaa\xfb\xe0?\xf2\xebg%\xf3x\xf5?0\xe6\x10\xf0\xf2\xb3\xf0?\xce\xd4T\x83\x18\xda\xe4?+\xcbv\x93A\x9e\xed?\xf4\x9awGG\xff\xd2?x\xfa\xdf\x08\xf3\xe7\xcb?\xc0\x93v\xfb\xd3l\xe1?\xffY1\t+\xf4\xe5?H\x01\xda=\x00\xdf\xf2?\x14\xa8\xbb\xf0\x15\x95\xf2?\xc2\xb9g!iB\xf8?\xed\xf0\xb6\xd9\x9b\x0f\xeb?\x81v=)_\xad\xee?\xe9-\x8d\x924\xd8\xf0?d\xba\x1fP"\xae\xeb?\xb6T\x96&T\xcf\xf0??\xe6n\x81B}\xea?va\xac\xc1w\x8d\xf6?\xa4\xe6V\xbdu\xe5\xe9?t\x86\x16d\xbb\xf6\xec?\xeb\x02\xc8)b_\xf6?\xa6\xf0\xec\x95\x93\xa6\xf3?DL\xd0l\xa1\xaa\xec?m!\x9b\x15\'r\xe8?\xee\xee\xfc\xc2\xab)\xf0?\xb8Y\xb6\x1cjE\xfe?\xc6d#\xd8\xb4"\xdf?r\xa4]_\x81\x87\xf0?xW\x93:\x98\xd0\xd7?~It\x00\xd0\xb6\xf0?(\x98\x931\x8b\xb2\xfb?\x9e\xe2\xfe\x0e\xa06\xf5?n\x02\x0e\x1bF7\xd4?<\xcd\xe5 O\xf9\xec?bN/v\x8bW\xf5?\x95\xa1\x8c\x0f\x10\x1f\xf0?\x91\x9cH\xb4\ra\xee?N\x18P\xb1\xa2@\xf4?\xc4\x90U\x18\x84\x99\xd0?d\xba\xfb\x9c\xf1\xc9\xed?\xb7\x02j[0p\xfb?\x17\xeb=5\xe8\x06\xef?\xca\x8a\xa3\x9e\x91\xd2\xf2?\xe8G\x10\x16GS\xef?j\x14G?\xd7c\xf0?\x9es\xd1\xc6\xb6\x19\xf6?miJ~\xde\xb0\xe4?\x98x\xbf\x0c\x00\xaf\xfc?{8\xe7\xf6\x8e\x1a\xe7?n\xed`!\xc8\xf6\xf5?4~c,8\x07\xea?\xdeLr\xb2<\x15\xf3?\x1c\xa9?\xe9\xaa\x93\xf5?@\xba\xd4*\xd4\xd8\xf7?\xd3\x90l\x8ea\xad\xf9?\xearF]*\n\xed?\x88\xd1\xe7\x9e\xbd\xf5\xf3?\xca8=(\xa4O\xe9?\x1d\xfas\xe4A\xff\xe5?\x91\x96\xfaz\x9b\x81\xf8?\xe5\xa0Z\x94\xd8\xc6\xeb?}\xd3\x90\x8eZ\xc9\xf3?\x8a\xf3QP[\xe4\xf2?\xf8\x8d\x9eR"W\xf2?v\xa7\xc7\xeby\\\xec?\x8a\x96t\x82a\x8b\xf1?\xbb#,\xe8|k\xec?\xcc3\xeaT\xf7\x1a\xe0?\x88Jm\xf9\x0by\xf7?\xb5pX\xde\xb3\xee\xe7?:\xffC\xcc\xd2\x1d\xed?\x0e\xb9\xe7\xb0?n\xe9?@=\x00\xaa\xf2\x12\xd5?|\xa9d\xfd\xf9\xc9\xee?\xe2\xc1^8o\x11\xf6?+\x94+\x02\xe7)\xef?n\x81\x1a\xde\x80\xc4\xf2?\x81\xbaC*\xd9W\xf5?\xb4@<^\x16\xfe\xfb?|>x\x08?T\xf5?\n\xa2q\xbe\x99$\xf5?9\x92\xa7\x10,|\xed?t\x86\x16d\xbb\xf6\xec?\xbc-B\xe1d\xc6\xe1?o:\xe4h\x1a\xdd\xea?h\xa2\x93^\x0b\xa8\xd6?rS[8\x07\xc9\xf8?\x02\xb8Js\xbd\x8c\xf2?\xc6\xf4k\x88\xdf<\xea?\x1e?\xf0\xbf\n/\xf0?O\xb6$\xdf\x95t\xf9?A:\xeb\xcd\xc2\x93\xe5?\x80\x8b)\xcb\x85\x8b\xf0?\x84"*`b\x1c\xe4?\x9c\xa7\xf41\x0b\x00\xd9?"\x9a\xce\x1b\xa2;\xe4?\xd4*`\x0c\xe4\xb4\xc8?\xeb\xb8\xbf\xf5\xb8\xd0\xf2?\x06\xc8\xe9(u\xa1\xf0? l\xc0\x93:\x81\xf2?\xe5\x9fO=\x84\xdb\xe7?4\xabV\xf0\xc7>\xf9?\x95\xeb\xbb\xe2\x10y\xf7?N\xe0\xb7\xa6]\x94\xf4?\x1fU\x84\x8e\xd4T\xeb?N\xdaK[\xba\xd8\xf0?NL\x03v\xd7\xa5\xf7?{\xf4G\x94\xeb\xfc\xf1?\xb0\xc9\x1cyon\xf6?\xb9\xa2\x94\xea\x81\xed\xea?\x18\xa7b\x91AR\xfe?\x1c\xb6\x0c\x1f\xea\x8f\xe0?\x9f\x1di\xe0Ln\xed?&^\x95cA:\xde?m\x9d\xa4\xa0^#\xe4?3\x01\xf3\x17\xeb\x90\xf1?\x82\xa3Q\xffTD\xf8?P\xe0\x01\rSJ\xea?\xde\xe1\xde\xfe\xf8Z\xf6?\x0es\x92/!\x1f\xdc?7\xcb\x1d\x07\xa2n\xed?\xc0\x97\xc7\xb1H\xe4\xe6?\x845\x83\xe2\x11.\xf9?\xb6a\x1f\x1d~d\xe4?\xd2m\xa4\x1d\x81\xb8\xf6?\xdc\x965L\x86\x88\xf1?2{\\]TV\xf8?HjD\xcf@\xcf\xf3?\x9cJ\xd4(\xed\xd5\xf0?\x03b\\\x01i\xd5\xef?\xf2*\xfc\xf3jo\xfd?\xda\xfd}9{\xba\xfb?/\xdaq\xf2\x7f0\xe9?\xcd\xf4(\xfa\xf3\xdd\xf0?\xee\x0ce\xd6(\'\xe7?z\xb6\x932\xbb\xd7\xf9?\xc7\xfd?r|q\xe1?\xa4Za\x0c=\xff\xe9?\x8e(0\xfeQ\xdd\xda?=q}\xba\xf5\xb9\xe0?\xd0\x9f\x877\xe3\xd5\xf5?>\xf9\x9d\x96<\xe9\xf0?\x00\xf9\xc6;sT\xf0?=\xef\x87\xab[\'\xee?\x18>l\xadE\x95\xf0?B\xb2E\xb3\t\xdc\xf2?\xeb\x02\xc8)b_\xf6?o:\xe4h\x1a\xdd\xea?F)\x1ai\x9c\xf2\xfc?,\x81\xa3AS\xeb\xf7?\xd5\xc9 \xd8L\xc9\xe7?\xae\xad\xd7FpI\xf2?\xef\x19\xa6iJ\xec\xf3?\xa6\xed\x8f\xcbp\x89\xfa?\x18}\x0b\x95\xb2\xee\xf2?\x9bY\xd08~\x1c\xee?Y\x95gu\x90\x1b\xef?F0 J\xe9\xac\xf9?\x1bI\x8a\xdc@|\xfa?\xdc\x82\xcb~\x83[\xf7?\x829\xc9\xad\x10\xed\xea?\x86H\xa9\x16\x91\xf2\xef?\xc1\xbco\xc3\xf6\xc0\xf0?\x9d\xf3\xd8\xc2\x10{\xf0?\x92-;\x81\xd4\xfc\xf6?jg"\x14\xffp\xf3?\x84\xd3\x90~X\x9c\xf3?\x84\xdd\xe4\r\xa1\xc6\xe4?\xfdxs\x7f\x9a\x1a\xfc?\x94\xdbFj\xa4\xf4\xf0?l\x93|\x8b,\r\xe4?h0\xf9\xfd\xf1\xbb\xf9?\xbc\x9a\xc4[T\xc5\xfc?\x90)*\x98\xf4\x0b\xf2?\xf6s\xd4\x1c\x10\xbf\xe0?S2\xefE\xd5\x9d\xe8?\x9f\xa7\xdb\xc9\x04i\xe0?.W=\xa8\x19\xdc\xfb?\x06\x13*\x8bC\x8d\xf5? CEK\x98;\xf1?\x95\x8e\xf0\x90\xa1\x82\xf8?\xadvW3D\x12\xe8?$Y\x00B\xdf\x11\xe2?i\x1a\xccac\x18\xe3?.4/O\xf2\xd0\xf8?AI7Ux\xa1\xe7?\xf4,+\xa0>\x1c\xe7?\xc3*\xbc\x1bk|\xef?\x92\x99\x8e7\xa2\xf2\xe8?/\xb0\xdfT\x86\x08\xe5?fL\xf0\r\x1b)\xf1?\x187i\xef}\xb2\xec?]d\x93IcZ\xf0?\xf3\xf2ZA!\xb9\xf6?\xfd\'\x03\xcc\x0e\xd7\xe6?\xa2\x1c\x0c\x92\x01\n\xe1?\xdd\xc0{HL\x87\xe6?\xcc\x9f\xcel\x8b\xd4\xf8?X\n\x96V\xcc\xff\xe4?Xowj\xea\x9b\xbb?\xe7[A\xe5\xacp\xe6?\xa0|\xdf\x83Mm\xa6?\x06UC\xc8\xc1\x0f\xeb?\x14r\xb0N\xf87\xd9?}n\x88m\xef\x88\xf6?/\xb6\xb5\xc7\xdcc\xf8?\x1e\x80\xfe\xa2\x91\xe7\xed?\x08\xdb\xca\x86\x0c\xb6\xee?"\x91|0\xe7\x05\xfa?X\xd0\x987\x8e\xde\xed?\xa6\xf0\xec\x95\x93\xa6\xf3?h\xa2\x93^\x0b\xa8\xd6?,\x81\xa3AS\xeb\xf7?\x14C$\xa1Q\xcf\xe3?B~,%\xef\xd2\xf3?\xcck\xdch\xfb\x84\xef?\xb6\x80NR\xfa9\xf0?:p\xdc\xce\xe4\x12\xf2?<\x03c3\x86\x05\xf6?\xf1\xb9\xa9\x98\x83u\xe0?r\x99\x02\x11\x0b\x8f\xf3?\xdc\xe5L\x9a\xf2\xec\xe3?\xf4\x1b\xba\x8dM1\xeb?\x1f)t\xd1W\xa2\xe8?:\x1ft\x95\xf0\x00\xfd?\xf4\xf3\x8e\xa26\xea\xcd?\x8b\xb8h\x0f\x1a\x9d\xf1?\xa2\xeb$\xcbT\x88\xf0?~V\x93\xe1\xc8\xf8\xff?\xfby\xd2\xce\x19\xf5\xe9?\x04%\xe5\xca\xe6\xb6\xf3?\xaco\xa7?\x99y\xe0?\x8c]\x18\x87e\xb0\xe3?\xc5\xa3D\x1d\xf0\xd4\xee?T\xbb:x\xfa\x81\xf0?\xfb\x82j~t\xdc\xf0?\x17\x98\xa4L\xd9\xe5\xf5?\x02Q\x01\xb9\xb4\xe2\xf0?=z\xc8\xed_K\xec?+K\xc2\xd5\xf4\'\xf2?\xfa\x14\x06\xb8O\x14\xf3?\x90\'iH\x99o\xf5?V\xac\xd3\xcdQ\x9b\xd0?\x0f\xd3LXr\x04\xe3?\x1a\xb9\xff\xe7\xfb\xa1\xd6?(l\xe4\xbd\xf4\xb2\xf3?^\xdc@\xcba\x8e\xf6?\xdf\xc6\xacnR\x04\xea?\xafs\xbdF\xdf\x82\xe7?$ \x9c\xcc\x88\xe2\xf8?|G\xe3\xf0UF\xe5?\x7f\nc\x19\xa4\xcd\xf0?O\x99\xd9/L\\\xf7?\xdc\xf3}\xa0\xe5\xe0\xf4?\x1b\x04_G\x13\x1d\xe8?\xb6\xc73\xfe\xcfm\xe7?\x1a\t\xf4\x91\xf1\xf8\xf3?\x9d\x08}\x1c\xa1\xf5\xec?\x87\xfd\x88\x18\xbck\xec?h\xcf\xdd\xc2\xb7\xd7\xef?\x02\x01\x96^I\xe1\xf0?\xcc\x0f\xa21\xf5\'\xf7? d\xf1\xe7\x8f\xce\xcf?\x02/=\x93\xfdV\xf9?\xd6\x89\xb5\x02:J\xee?I;\xeb\xaa~\xce\xee?h\x7f\x8a\x15<\xcb\xcc?\xde\xf5\xbf[\xe2\x1d\xf8?\xc6\xc4pH\xad\xe8\xf4?\x0f4\x8eJdU\xfa?\x98|\xc8\x1a\xa8\xa0\xe4?\xb4\xd5[1\x16\x04\xe1?\xdc\x94D\x02M3\xe8?\xb2\xa5mS2\x9b\xfa?DL\xd0l\xa1\xaa\xec?rS[8\x07\xc9\xf8?\xd5\xc9 \xd8L\xc9\xe7?B~,%\xef\xd2\xf3?P\xac/;\x88\x10\xc2?G\xf9\xecz\xb8y\xf3?\xab\xa6+\xe5&\xe1\xeb?\xa8\x12\x05\x86)\xf1\xe3?\r\x1e\t\xf6\x1a4\xef?<\x84\xaf\xba2&\xe5?0(\x1dy\x11\x1f\xf4?\xc1\x7f\x94\xe597\xed?~=f\x14\xc2\x92\xee?\x06\x8c\x7f\xad\xce\x90\xe8?\x86_\xeb\xa4\x18\x12\xf6?\x81\r\xd1SG\xf8\xeb?V\xba m\xfeP\xf3?\xe4fQ\xd2\x9d\xe1\xf7?\x94\r5\xca\x07\xf4\xe4?\xc93\x9f8B\xb6\xf8?\x01\x82\xc7\x92W\x7f\xe7?\xfb\xe9\x17+\xa1\xbc\xf1?\xee&\xcaH=$\xf2?^\\\xee\xdf5#\xf4?."k8\x04/\xe8? \xc9\xa9\xd51\xb6\xdc?]\xfe\x05}\x86C\xef?*\xd6\x05[\x0eD\xeb?\xd4\xec\x19S\xbb\xd7\xfc?E\xe4UJL\x0b\xed?#\xe8\xc2\xad\xf4E\xf7?3\x16)\x9cw\xd7\xfb?\xd8\xd3\xf1\xbf`\xe3\xf6?\xe7o\x9d\x89\xaa/\xf8?\xe3*#\x97\xad\x86\xe4?|eY\x04\x17T\xd3?u\xe2s\xd1A\x82\xef?\x0c\xb2\x08^\xe0\x1c\xf6?}\x97\x12\x87\xabK\xe5?\xd5\x8fI \xcf\x8c\xeb?\xcf\xd22x\xbe\xfb\xf1?\x89\xaf\xfc\xdf\xd2J\xef?\x93\xe5E\x8a \x1b\xe6?\xbc\xbc\xb0\xaa\xe5\xc2\xfa?\xe0[\xd2\xd8\xd0\xfa\xf3?J,\\8\xe2\x87\xe6?\xfc\xfa\x88\x0f4\xe1\xc6?\x9d\xc80\xc7s\n\xf7??8B\xf2J\x80\xee?V\x1b\xcay\xa9\x06\xe1?Bc\x1d7\xa1\xd7\xf0?\xc6\x8b\xd5h\xc3\xa1\xf2?\x80\xe6\x15>\x17\xd3\xd1?BV>\x81\xf9\x0c\xf7?\xfe>z\x04\xd50\xea?\xd9\x16j*\xa7W\xe6?\xb6\xabyjE\xf5\xf2?A\x99\xa4h\xbd\x81\xf4?\x8f\xa2\x97\xa6b\xa2\xe8?\x06v\xd8GN\xe0\xf2?\x13[\xbd\x83(\xb6\xe8?\xf8\x10\xcb\xcb}e\xf0?\x07\xfb3G\x86\xff\xe4?v8v%S\x9a\xfb?m!\x9b\x15\'r\xe8?\x02\xb8Js\xbd\x8c\xf2?\xae\xad\xd7FpI\xf2?\xcck\xdch\xfb\x84\xef?G\xf9\xecz\xb8y\xf3?\xb8\xf8R\xe7\x92\xdb\xe8?\x90\x84WR*f\xdd?;\x99wp\x08E\xe5?\xaf<+D\xa7@\xe9?\x8d/H\xde\xf3\x9b\xe8?\xae\x168#]$\xee?\x84\xc8\xa3\x94\x99\x05\xef?2S\xf30?A\xf1?>\x9e\x9bU\xa36\xec?\x8e\xbf\x10\xf6\x17\x18\xed?\xff\x1e\xf9=\x97\xb9\xe7?\x01\xe3.\x86l1\xe3?X\x15\x9b\x11\xc6\x97\xd7?JPH\xb3g_\xd8?\xbaX\x18f\xd4\xe3\xe7?\x90?(\xd2\xe6\xc9\xdd?\xda:\xb0\xc8\xcb\xe3\xfb?\x92\t\xa66\xe9\x98\xd9?f\x81\x83\xb1\xc7\xe5\xd3?\xe0\xc9\x05\xd5\xaf\x06\xd8?|t@\x88F\xc8\xf1?\xcb\xc73\xff\xacN\xef?\x8bC\xcd\x13\xadD\xf5?\x86\x89\xbf0\x7f\xd7\xf0?\xb4z\xf1\xf9\x0f\xe9\xf8?l\xbb)\x08\xa9\xfb\xf2?\xfe\xd7\x97\x14l\xf7\xe0?\xc5\x87\xfcO:2\xf0??+\xf9[\xd8>\xe2?t\x81#\xb8\x87\xad\xfb?\xeaVq\x1c\x04\x0e\xea?fWP+\x04\xe3\xf3?\xc6Vn\x87\xca\x84\xf3?\xd4\xf01\xf1\xa3w\xf1?\r\x16\xcaS\xa5(\xe9?l\xacC\xe1\x17\xc9\xd5?\x84\xd5\xc2$1\x86\xfa?\xcc\xbf/\x94\xbd\x8b\xce?5\x89\xb0\x94\x00<\xe1?\xea\x93\x8d\xb1^\xca\xfa?\x08gp,F\xbc\xe3?q\x03\xc3\xca]\x15\xf4?J\x0b\xb8\x9cjj\xf8?\xbc\xe1B\x07\xae\xe9\xf1?\x06<\x83b\x88\xe8\xe6?6I\xfb\t\xeb\xd4\xe3?\xc64\xcd\'\xcds\xf4?!\xec\xbbdl\xeb\xed?\xfak\x00\xcd\xb2H\xf4?\xa5\x0cY\xcdqS\xe6?B-\x12cg\xf5\xf6?L\x96\x93\x08\\~\xed?\xfe\x82\xca\xd1\x97\x9a\xfc?\x16\xa3\xed\xbd\xeb\xe7\xf3?\xb2DKI\xe5\x05\xf2?\xb8C\xce\x0b\xe4\x84\xe6?8\x18\x13b\x85~\xf4?Ao\xef\xf0\x92\x90\xe3?\t\xd8\x1c\xa8H\xe3\xeb?\xee\xee\xfc\xc2\xab)\xf0?\xc6\xf4k\x88\xdf<\xea?\xef\x19\xa6iJ\xec\xf3?\xb6\x80NR\xfa9\xf0?\xab\xa6+\xe5&\xe1\xeb?\x90\x84WR*f\xdd?\x129|>\n\x18\xef?\x0eJV\x1c\xda\x10\xe0?\x14x\xa0\x07\x8fl\xef?j#\xdf\\g\xdc\xe0?0\xe7\x04\xdc\x96\xe8\xcb?\xd9\xcd\x9d\x95RD\xe8?\xcb\n\xacf\xab\x14\xfd?\xafJI\xcb`)\xf1?T_b\x9f\x7fm\xe9?\xaa]\xeb!\xf7n\xd8?\xceo\x06\t\x04[\xee?\\o0X\xcd`\xf3?\xf4\x05\xadt>Y\xed?$\x94\x1cZ\x93\xec\xf1?,\xf6E\x85\xc1|\xf0?2\x8b6\xfafh\xdd?\xf6$M\x93\xa0\x05\xf3?\x86]\xe6\xed\x8e\x80\xf1?\xef\xe6-\x81\x87f\xfa?\xc6\xb6)\x90-*\xf0?\x1f(Q\xdc\xf4*\xec?H\xdcd\x08\x99\x07\xf8?6\xb4\xe8l\x1f\x1e\xf3?\xc2\xbf\x85!\xb2^\xf1?\x16v\xd7\x98h3\xf4?P\xa7ES\x01\xae\xb9?FO\x9e\xab\x00\x85\xf4?S`\x9d\xb2\xf1}\xe8?J\xe52\x8c\x90\xf4\xf7?5\x08\\Y\x9e\x0b\xe9?\xd2\x0bb\xd0\x088\xf2?jz-\x89\xd6\x94\xf3?\n:L\x02\x82\xf6\xe6?\x8a\x80\x0f\x04\xd1\xa2\xf0?\x02\xa4\x9b3\x1e\xf2\xf6?\x16/\x87\x91y\xcf\xf2?\x9e\xd9\xd7R\xad\x90\xe1?\xc0\xe1\xa2\x17\xb6\xbf\xe7?\xfe\xf0\xab\xbd\x14+\xe4?\x10\xeb>\x1cE\xad\xd3?p9r\xac\x94<\xf2?a?p\xe04\xb1\xe5?s\x11\x07J\xf0\xb0\xe7? \xdd\xd0YC\xc0\xe9?B\xca\x06\x96F9\xe4?t|\xfeKa\xd1\xfb?\x0e\x00=D~\x7f\xf1?\xaf\xeb\x96\x84k\xa5\xef?]\x10M\xf6pd\xf5??\xeb\x9a;\xde\xe9\xef?\xc6EW\xe2x\x88\xe1?e\x19/\xf8DW\xea?\x08\x86\xf5\x82Z\x15\xf1?w\xb7C\xfd\xa5\x95\xef?\xc9\xd8\x82\xf1\x11\xa6\xf0?D}{\xef?\xe2\xf5?\x8c!O\xd9f\xbb\xd3?\x91\x9a\xf4\xc0-}\xe8?\xb8Y\xb6\x1cjE\xfe?\x1e?\xf0\xbf\n/\xf0?\xa6\xed\x8f\xcbp\x89\xfa?:p\xdc\xce\xe4\x12\xf2?\xa8\x12\x05\x86)\xf1\xe3?;\x99wp\x08E\xe5?\x0eJV\x1c\xda\x10\xe0?\x1c\xc4\xd6\xcb1\x14\xf9?\xaa\xfaR\xde0X\xf0?\xda\x05Z2\xe6V\xd5?\n\xfd\xee\x00\xbd\x1d\xf9?|\x1c=(\x8f.\xcf?\x18\xd5n\x80z\xda\xfc?\xca\xd8\x10\x1d\xd3\xce\xed?>\x80\x88(\xce\xc7\xf5?\xa8LP\x1bz\xb0\xf0?\nd`\x1eo\xbb\xe4?N\xda\x9f\xfd\xaed\xf4?\xc9\xaf)%F\xe7\xe4?\xf3\n[\xcf\x15\xf6\xe0?\xa6\x18\xc3\xcdK\xbf\xf4?S\x94\xdd\xa0\xd6\xfe\xfc?n~\xc0~\n\x0e\xf7?\x80\xbf\xbb4\xb0\xad\xf2?8\xdf\rs\xd5d\xf4?\x91\x07\xae\xd9\xd6\xb7\xeb?\xb9\x00\r;\xf2\x8c\xe5?\xcd\x99\x01\xca\xe6\xf8\xee?8\x10<(\xaa%\xe4?\x80G\x04\xe9\xb3\x1c\xf2?\x8c\xb8\x94\x1f\xed\xd3\xf7?\x951\xaf*\x87\xa9\xe8?t\x9a\x94\xfc2j\xdb?\xfc\xc9g\xe9\x05\xc4\xf6?\xc7\xa1\xd46]\xea\xf8?\xe6Ot\xc2\x8c\xec\xf2?J\x08\x89\xc4*0\xee?\x85\xd9Q\xf5X\xd8\xe4?\xc4+NK\x00f\xec?\xfa\rtZ\x02=\xfe?\xf7I\xfc5\x95U\xed?\x1ch\x11p\xa3\t\xf3?]+\xdc\xe3\x02H\xf2?\x84\xb3\xe7(\xc2\xde\xe9?i W\xfa?\x96\xe3?/\xce|gv\xe0\xf7?\\\xbd\xd3s\xc6\x12\xf0?B3\xa0\xc7\xa35\xd3?\x91\xbf\\g\x14\x82\xf7?\xe3\t\xbe\x90\x1b\xc0\xfc?\xa2\xd7A$\xaa\xd8\xf3?\xb5U^l\xfbY\xe8?\x90"\xd75\xac4\xd0?\xb4~\x80\xe4\xacD\xe6?\xf1\xeb\xae`\xad\x1f\xe3?\x1a\xdb\x90\x06\x0fL\xeb?\t\x91}q\xc5v\xf8?g3`\x9f\x17\'\xeb?\xb8$\xae1*\x97\xf2?\x8b\x99#\xb9c\xf8\xf8?\xe7n\xc4\xf56\xf2\xf6?\xab\xb1\x0c\xbcop\xe2?J\xd2@\x1c\xc9R\xf1?:_\xc0Y\x92\x85\xf0?\xc6d#\xd8\xb4"\xdf?O\xb6$\xdf\x95t\xf9?\x18}\x0b\x95\xb2\xee\xf2?<\x03c3\x86\x05\xf6?\r\x1e\t\xf6\x1a4\xef?\xaf<+D\xa7@\xe9?\x14x\xa0\x07\x8fl\xef?\xaa\xfaR\xde0X\xf0?\x9a\x97hS\xdf\x90\xf9?\x18kla\xbd\xd7\xc9?\x90\xff\xbby\xa2\x04\xf2?X\xbd\x85\xe0V\x8e\xce?\x80$\xc6\xe5\xf1\xb6\xbd?\x8dVk\xbe\xf9.\xe0?\xc1o\x88\x07\xb4J\xe8?B\xe0.\xe6\xce`\xd7?\x92\x83+\x1bV\xa5\xe7?\xfa\x88\xb7\xfc\xd3\x96\xe2?\xc1\re\xa6\x0fx\xf0?\x1cn\x16\xf3\x9d\x8f\xee?)\xbcI\xcelX\xee?\xb0\xde\xbbY\xae\xd4\xfb?\xb4eS~\xa8t\xcb?\x0e\xdd\x8a\x1b4!\xea?\xef6\xae\xc7\xd5\x81\xed?\xdc\xa4\x9b\x84\xef\xc9\xec?\xce\xedz\xca\x19\xc8\xe9?J\x1a_\xabq\xd4\xf5?\xfdF\x95\xc7$\xd4\xef?\xcca9?t\xb7\xee?0\xff\tN3\r\xf3?\x9c\x9d\xa1Q\xa6\xd1\xe1?\xe4p\xba\x903\xb6\xea?\xada*\xf6\xd5\x7f\xfe?\xffjIt\xfaN\xef?\xf0\xef?\x99C"c\xb5J\xf2?9-U\xa5\x00\xe1\xef?\x16^\xe1\x14\xac\x1d\xef?\x966\x7f\xec\xdd\x8e\xee?\x08^~\r\x86\x0c\xc6?\xe8Rz\xd5\x0e\x8b\xf0?\x99u\xbc"\xca\xb6\xeb?\xc8\xd1\xeb\xed\xd8\xa9\xf2?:0\xb2\x14T!\xec?\x00Cy\x80\x94B\xf2?\xe5\xc8\x94\xeet`\xe6?\xa6v\xcc\xadt=\xf3?\x81@\xf5W\xaf\xd6\xe3?\x06?.\xac\xc77\xd4?>\xe2{-\xcb\xc4\xf2?\x84\xcc \xdf%\xf3\xee?0\x8b:\xec+\xe5\xf2?\xfa\x15\x9a3\xc0\xb2\xf6?@l\xf6\xbb\xbcV\xf6?\x8a\x81J\xd9P\x8e\xf0?T\x87\xe8\x0c\t\xf7\xea?^\xeb\x88\xb6\xec\xbd\xea?\x8a\x9a\xae\x93\xe8\xe9\xf5?6{\x11@\xda\x82\xea?R\xcfu%\x1d\xec\xe2?T\xde,z\xf4\xe3\xee?^\xd6\xfe%\x0c\xf1\xf1?V\xd2\xfc\xb7\x148\xf8?${7\xfe\x0b\xf5\xf3?NdW\x95N\x91\xd8?\x949/\xfd\xd3\xd2\xd7?z`\xfe\x9b?S\xf2?u\xa9{\xef\xe0\xbc\xf4?5^\xc9\xb6\xb2t\xf2?\xad(}\xd6t\xcc\xf0?i]\xbeY\xc4G\xf1?\x05\x89\xdd\xbd\xe6 \xf6?>\x95\xd0\x18U\x8e\xf6?\x12\xfdx`S\xea\xd8?\xd5\xa1\xe1\xb2\x7f\x1f\xf0?xW\x93:\x98\xd0\xd7?\x80\x8b)\xcb\x85\x8b\xf0?Y\x95gu\x90\x1b\xef?r\x99\x02\x11\x0b\x8f\xf3?0(\x1dy\x11\x1f\xf4?\xae\x168#]$\xee?0\xe7\x04\xdc\x96\xe8\xcb?\n\xfd\xee\x00\xbd\x1d\xf9?\x90\xff\xbby\xa2\x04\xf2?)K\xb1\x9f\x90|\xea?4!\xda\x86\x7f\x0e\xde?\xc4]\xd6\'K*\xed?"\xecl5D\xe6\xf1?>\x87\xd1\xc1\n.\xe9?\x95\x99_\x15\xc8\x96\xf5?W\xdd\x06\xe8\xde9\xf4?\xfa{\x98N\x8cj\xf2?=\xea\xe0\x0f\xeb\x08\xe6?\xe4\x00\xd4sm\x9d\xf3?\xac\x95\x9f3\x85\x9f\xf0?/\xc8\xe3\xf8\xa1\xfe\xe2?qh M\xdab\xf1?,|\x97D1f\xe7?\xb2\xa6\x1c\x18\x7f&\xf0?\xd8\xab\xd8\xfe?\xb0\xf1?Iy\xe7\xef\x17\x9c\xec?\x07bO$W[\xec?\xdey\x0f-W\xe9\xd6?\x94\xa7<1\xd4\x91\xf3?\xfe~\x03df\x00\xf9?\xb8T\xc7Da-\xde?l\x0b\n\xeaRy\xef?KK\xc1\x94\xb9\xb8\xf4?\xf2\x83\x93J\xc9\x9a\xf9?\xb0\xcf\xcb\xf3\xf3X\xf0?`\xcd\xf2?\xfb\xa9\xf4?`\xb9/9\x89\x93\xf1?\xbb\x9e\xa1\xe5\xfcK\xee?\xaf\rD\xcc\x99K\xf0?P\x19\x92\xf2W\xc3\xf2?*`\xb6\xd5\xa7\xac\xf8?\xc4\xe6nq(K\xea?\xc9S\xf8\xbavI\xfd?P\x9a\xe3\xcfC\x8f\xe3?\xfb\xef\x8c\x04\xa6\x88\xe9?r\x1f\xbc\xceC\x0b\xdd?ov\x03\xad\xb6w\xfa?%\\\xc2o\xf1\xd2\xed?,U\x8bs\xa3\x81\xf3?\x8f\x1fz\x1e\x99-\xec?\xd8\x03\x8a\xf8\x00!\xe3?\xc0\x99-\x15\xab\xc9\xe7?{\xa9m\x12\xe7\xb9\xf0?\x0e\xeb\x1eM\xec\x07\xec?\xdd\xa3JttV\xe2?h\xc8\xe0\x15\xde|\xc2?A\xa2CU\xfdZ\xf2?\xd0\x10\xeb\xba\xb43\xe5?\x81\xf6\x92\xbc\x93\x9b\xe4?\x90\xae\xb1F \xbf\xf0?\xf4e \xa2e\xb1\xf9?\nK\xbc\xacd\xe0\xea?R\xe2G\x02\xab\x16\xda?\xc2\x96\x84\xfc4\x86\xe8?~It\x00\xd0\xb6\xf0?\x84"*`b\x1c\xe4?F0 J\xe9\xac\xf9?\xdc\xe5L\x9a\xf2\xec\xe3?\xc1\x7f\x94\xe597\xed?\x84\xc8\xa3\x94\x99\x05\xef?\xd9\xcd\x9d\x95RD\xe8?|\x1c=(\x8f.\xcf?X\xbd\x85\xe0V\x8e\xce?\x96\xa6|H=\xf0\xf2?\xc4]\xd6\'K*\xed?hz/\x9cv\xc2\xe8?/Q\xdd\x91c\xa8\xe5?\xfe\xfb9\xcc\xd20\xf0?\xdb\x18\x9d\x0bf\xf2\xef?\x85\x8c\xa70\x1f\x9f\xeb?\xd6\xacx\xd9[O\xf2?\xd6U\x8f\xdd\xea\x94\xe5?\xc7-p\xb1\x0c+\xef?\xf6I\xf5\xc2t|\xf7?\x1czY\x1b\xb4\xd1\xc4?\x12d\x8b#\x92\x88\xe7?\x8b3\x93V\xbe\xc8\xed?\xe7\xd3\xb3\x05\xabE\xf1?\xe6\x1fAJ\x81Z\xd5?\xa6\xe6\xc1\xaa\t\x83\xf2?\x85\x90\x85\x00\xa3\xcf\xe8?\xf4N8`\xe7\x17\xf1?\x1aJ\xe9\x92S\xd4\xf1?$\x05\xc4\x98\xcd\x94\xce?l\xc6M\x81\x95y\xe8?\x00&\x04\xbbU\xe4\xab?;iK*kT\xe4?C\x03\xbfe/i\xed?\xe8\x19\xbbm\x07 \xe3?\xd4\x08Eh\x9es\xc7?f\xf5\xe2\x11\x02l\xe4?<\xa7L\x1c\xff\xa1\xf4?\xe0c;b\x1dq\xe3?\xac\xbdn\xe6\xb9\x86\xf8?\xcd(\xa5}\xe8\xf3\xec?VJ\x9a\xc3\xb2!\xfe?\x10\xf6\xecPqi\xf3?dO\xea\xdd\xc76\xe7?\x9ct\xf7\x9f\x9a\x11\xd1?\xb9k$\x0c\xf4\xb5\xe3?\xdf\xea\x8e\xefp\xcb\xef?l\x06\xed\x82t\x08\xf7?\xe2[d\xad\xbd]\xd2?\x8c\xdbE\x00\xfe\x93\xdf?\xef3Q\xf6\xc0\x14\xe5?\x96\xf2\x0bNB\x8a\xf0?T7E\x18\xb5m\xee?i\xd5\xfck\xcb\xac\xfb?\x98K\x04\x07\xb40\xc3?\xa0\xb2\xc3\x1b\xb2\xf8\xf5?\xc4\xc3=\x81Y5\xed?\x17\xa1\xe5\x81i\xf8\xf1?D\x17>.\'7\xfa?[\xd3A\x00\x8b\xf3\xf5?\xe0\x8e\xc9W\x13Z\xba?\xd0\x1eA\x1d\xdd\x8f\xd3?t\xe8\x83!\x8b\xd8\xf8?\xa0\xd3x\xddT\x19\xf2?(\x98\x931\x8b\xb2\xfb?\x9c\xa7\xf41\x0b\x00\xd9?\x1bI\x8a\xdc@|\xfa?\xf4\x1b\xba\x8dM1\xeb?~=f\x14\xc2\x92\xee?2S\xf30?A\xf1?\xcb\n\xacf\xab\x14\xfd?\x18\xd5n\x80z\xda\xfc?\x80$\xc6\xe5\xf1\xb6\xbd?\x00k\x87M\xc0\xdb\xf2?"\xecl5D\xe6\xf1?/Q\xdd\x91c\xa8\xe5?\x7f\xb5}[\x8fT\xf7?\x18\x86\xbb\xefm\xf0\xeb?\xad\xd4*\x1e\xe6\xc3\xf8?\xd0\xda(\xf4\x9eY\xf6?\x90/\x1f@\xe3\xb6\xf2?\xab\x07\x81\xaa\xe3\x11\xed?X\x0f\xd1\xf6\x8b;\xf6?\x0b.34\'U\xf6?\xd0\xdf\xbe\x11\xe8}\xf5?\x11\x04\xce\xbd\x0bm\xea?-v\x81\xb4\xbaw\xf2?8\x08\\\xa5\xb7p\xf2? \x1d\xc9n\x97\xc5\xdc?\x15\xf6\x8f\x98\xd2c\xfb?\x06\x8f\x7f\xa2,\xb3\xf5?\xbc\xc4ed8\n\xd1?\xceQ;2\xbf\xc0\xf7?4\x0e\xaa\xa8\xc1\x9c\xf4?\xd8\x00.\xef\xdd)\xf4?z\x91U\xf67S\xf4?\xda\xd16\n7\x9b\xf2?\x8a\xadP\xfb2\xf0\xf4?\x9a\x10\xae\xe6\xc3?\xed?\xc0\xa1gBy\xe5\xe4?0\xd0\xdc\xc1\xabo\xea?I\x11\xb50%G\xf5?J\xe3\x1b\xe9\x8e\xdb\xec?>`\x90\xdf\xd5G\xf1?u/7\x80\xae\xff\xea?~[\xa1\x0f\x9d\x99\xf4?\xc85\xb25j\xe7\xfc?\xdc\xabX@#J\xea?\x966\x88\x0f=\xf3\xf8?\x1e"\x8d\xfd\x16\x10\xf0?\x99\xf3 \x83xN\xf7?\xc2\x83\xbc^\xcb\n\xeb?\x8d\x88\xe3Y\xca\x84\xf5?h^>y\xc2\x0c\xf7?\xd9\x0f-\xc6Wu\xe5?\x86\x12K\xa8gu\xf4?\xd8>\xa0\x00\x02]\xb6?Vk\xf7\\G\xba\xe5?\x89\xd2\x88\x08\xd4S\xf6?\xea\x1c\xab\xae\xae\x9b\xf0?j\x98:\xd2\xf5m\xed?\xfdRT\xbc7\xa0\xe1?\x16\x8a-\x983\x06\xf1?>0\t\\@\xbc\xd4?\xe8\xff\x94\xa7\x94*\xe2?\xc13\xa0\xcf\x9b\x17\xf4?\x04g\xf6U|\x8f\xf0?\x05\xb5\xeeL(\x96\xf0?\x9e\xe2\xfe\x0e\xa06\xf5?"\x9a\xce\x1b\xa2;\xe4?\xdc\x82\xcb~\x83[\xf7?\x1f)t\xd1W\xa2\xe8?\x06\x8c\x7f\xad\xce\x90\xe8?>\x9e\x9bU\xa36\xec?\xafJI\xcb`)\xf1?\xca\xd8\x10\x1d\xd3\xce\xed?\x8dVk\xbe\xf9.\xe0?\xce.\xda\t\n\x92\xf3?>\x87\xd1\xc1\n.\xe9?\xfe\xfb9\xcc\xd20\xf0?\x18\x86\xbb\xefm\xf0\xeb?\xe9\x81x}`\x93\xf9?\xcd\x04!\xf7\xc9{\xf5?\x8c\x1d.\xe7\xad\xc3\xf1?%\xe6-\xd1\xd2R\xe4?\\\x06\x97\xb3\x17\x9b\xe9?@\xf2\xa1\xde@\x0b\xf8?\x18\x1dv\xc9\xc9\xfb\xed?\x87\xf2\xff\x86\xc5\xd7\xe7?\xc4\x9b\xe7\xac\x8fW\xef? \xb4_\xd5D?\xad?W\xe1\xde=\xaf\x1d\xf1?\xa5\xca0\xfc\r\x94\xfd?\x18\x91\x91\x00\xbd-\xdb?"\xe6\xdd_k\x93\xef?\x18 \xd11%\x18\xe6?\x10faW\x8am\xf7?\xa6\xcb\xfd\x01y\x88\xd3?\x86\x17c\x17 e\xf3?\xc3\xf7\xdf\x0f\xeaf\xeb?\xc7\xe2\xf5\xfb]\xc5\xeb?9\x1c\xa6 6\x9c\xf1?\xf9{\xb6\xb5{\xc2\xe5?\xa0\xa0LA\xa4\xb0\xf0?\xdepq"\x8a>\xf5?5\xbf\x1f\x8e\x81]\xea?\x1arM\xd5\xc9\x9a\xf8?d\n\xe7\x07W\x97\xdd?\xa2#\x110\x9cC\xfd?_\xe6\xbb\xe2S\xc2\xe1?\xc4O\x8ek+\xb4\xcf?\xcc\xaa\xf0\x8d\x83=\xd5?\xdc\x81\xa5\xfc_\xcd\xf9?\xb2\xdb\x9e\x19\x1c\xf0\xee?\xfb\x03\xa9N\xda\x16\xec?\x1d\xada\xde\xd3D\xf0?x\xb6\x16\xa5\xac\x07\xe8?\x8a\x18\xcd\x03$"\xed?;\xc3J\xb4\t\xc6\xf4?F:4\xee\xb3.\xd1?;K\x11)\xe4\xf1\xe3?\xd6B\x18\x83\x13\xe3\xf9?t\xed\xf0\xe1u\x87\xf3?\xd1\xd8[\xe2\x8f\x9e\xf0?\xd5:\xaa\xd7\xb2s\xef?b=\xd4C\xbf\x98\xea?.\x1a\xc7\xe4\x7fL\xf2?\xb3\xdb^e\xc4J\xe7?\x82\xfa\x03Y\xb7\xfb\xdb?.\xc9\xd6`\xd4\xf8\xf4?\x97\xf0\x7f\x9arl\xfe?\x1b\n\x85\x0cY\xb6\xe5?n\x02\x0e\x1bF7\xd4?\xd4*`\x0c\xe4\xb4\xc8?\x829\xc9\xad\x10\xed\xea?:\x1ft\x95\xf0\x00\xfd?\x86_\xeb\xa4\x18\x12\xf6?\x8e\xbf\x10\xf6\x17\x18\xed?T_b\x9f\x7fm\xe9?>\x80\x88(\xce\xc7\xf5?\xc1o\x88\x07\xb4J\xe8?\x92u\xd6\xd1\x0e6\xf1?\x95\x99_\x15\xc8\x96\xf5?\xdb\x18\x9d\x0bf\xf2\xef?\xad\xd4*\x1e\xe6\xc3\xf8?\xcd\x04!\xf7\xc9{\xf5?2\xf1%A\x86\x9e\xf3?P\x91\xdef\x1a\x0c\xe5?\xbe\xefEP\xd1|\xd4?,>"\xe8\xa1\'\xf0?H\xbfj\xd2\r\xc2\xdb?q\xd13\xe4\x11\x16\xed?\x06*n\xa1?\x94\xd9?0\x92Lh\x98-\xdd?\xee!\x83D\xb6>\xef?c\xc6\x0e(\xed\xff\xea?\xbee\x1f7\x1c\xec\xfb?\xf8\xa9H\xeeB*\xd0?\xbf\x1fpk\xfe\x00\xf1?q\x9bc\xfe3\xa3\xec?8\xe0L\xf6\xf8+\xf4?\xb2\xe0\xfc\xa0 Y\xf2?b\xba9\xdd\x1c\xad\xfb?\x12\xa4c\x1f\x904\xf3?H]u+\x1a\xc0\xf0?,\x03\xf6\xd1\xac\xc4\xe0?\x08\x8b$\xcd\xe2|\xf8?\x03\xb2=\xa3C\t\xec?\xe5\x95\xf8\xba])\xe3?\x0e;#\xb4\x10\xd0\xf2?|9S\x03\n\xaa\xf3?\x17\x06\x954\x8b\xb5\xf4?I\xfd\xba\xed\x84\xd2\xe4?JX\xb9\x92dN\xf0?\xb8\x90\xecD\xaa7\xf0?\xbas\xd8\xfe\x9d\xb9\xf3?\x9b\xd8@\x06\x1bi\xf3?\xb41>\xb9E&\xf2?f\xde\x13=\x1fE\xf0?S\xeep(\xe9\xde\xe2?8\xc1d\x111\x0b\xed?\xe8\x15`\\\'y\xdd?"[T\x1d\x97\x07\xf5?r\xcc\xfe\xfb\t<\xfb?%\xb64\xba\xce\xd5\xeb?\x8c&,|\x87r\xd3?\x06TsH\xac\xef\xec?\xacc\x90\xa4v\xc0\xf0?L\xc1\xaa4\xa1a\xe1?\xc10C\xbb8\x87\xf2?\xaf\x88\xae\x00\xe2\xa3\xe5?.4\xc6=K\xaa\xd1?\'\xb0\x06w\xea)\xe6?$\xd1(L\xad\xea\xec?\xc3eL \x19b\xf1?\x03\xad8\x87\xd0\xa4\xf0?<\xcd\xe5 O\xf9\xec?\xeb\xb8\xbf\xf5\xb8\xd0\xf2?\x86H\xa9\x16\x91\xf2\xef?\xf4\xf3\x8e\xa26\xea\xcd?\x81\r\xd1SG\xf8\xeb?\xff\x1e\xf9=\x97\xb9\xe7?\xaa]\xeb!\xf7n\xd8?\xa8LP\x1bz\xb0\xf0?B\xe0.\xe6\xce`\xd7?\x12O\x1b\xda\n\xc2\xda?W\xdd\x06\xe8\xde9\xf4?\x85\x8c\xa70\x1f\x9f\xeb?\xd0\xda(\xf4\x9eY\xf6?\x8c\x1d.\xe7\xad\xc3\xf1?P\x91\xdef\x1a\x0c\xe5?YS\'\x12\x19\xbf\xf0?\x04b\x1c/\xf4}\xc6?\x87i\xc7\xb0\xb1\x13\xef?9r\x89\xe3\xe2\xf7\xf2?\xbf\xa3\x86\xe7\x1a\xdc\xe8?\x02\x88(\xbc\xe5\x19\xe7?\xfd\x94\x99\x88\xa9B\xf6?\t\xd4V\x15jq\xf1?\xb0\x979p\xb0W\xf4?>\xc8P \x82\xc8\xf1?j9S\xf8\xa3(\xe1?\xb1U%\n\xfa\xdd\xf7?h\x9ejA X\xe6?\xaa\xe3X$\x1fv\xf0?,\x95\xce\xa9u\x93\xee?\x98\x95\xb8\xcbL\xda\xf7?I]\x98X\xa2\xc1\xf3?\xe6\xac]\x9d\x88H\xfa?\x19\xa1\x0c\x93|7\xe6?%Z\xdd\x89W\xa1\xe6?\xa2\xb7X\xb9\x9b%\xf3?\xdeg\xcda\xd2\xa7\xf2?lD0\xad\xabT\xee?n\r\x04\xb7/\xb7\xe8?\\iv\xa1\xbai\xf3?$\x14@I&\xc2\xf5?\xd3\xfdP]N\xeb\xe3?\xc85\xae>\'\x00\xbf?\xb0\x04\x8d&\xff\xfc\xf1?\x84L\xe6O\xf6\xde\xd8?\xc4>\x90\xcd\xe1i\xe8?X\x1f\x1c\xe4\x19\xdd\xf0?0\t\x82\x0eB\x85\xa4?e>,\xb5u\xfa\xeb?$/4\xdf\xe1<\xf7?]\xe06\x8c\x0b1\xed?5\xe6B\xcfU\xcd\xe8?\x98\xb2@\xa6I\xef\xf5?D\xf0\x7f`\xf4\x95\xeb?c\xb8.\xf15\xc2\xe7?b\x14k\xd5\xaaS\xfc?\\2\x1d\xa4O\x0c\xed?nQ\x83\xd0P!\xf6?\x93\x06qO\xb6\x93\xf2?\xad\xc1\xbd9\x9ff\xef?\x0b3\xf3t,p\xf1?6K\x98\x88\xb7`\xd1?\xa4\x17\x8e\xc3\x14\xe4\xf2?\xbc(\xd1\xb8&\xa2\xf4?bN/v\x8bW\xf5?\x06\xc8\xe9(u\xa1\xf0?\xc1\xbco\xc3\xf6\xc0\xf0?\x8b\xb8h\x0f\x1a\x9d\xf1?V\xba m\xfeP\xf3?\x01\xe3.\x86l1\xe3?\xceo\x06\t\x04[\xee?\nd`\x1eo\xbb\xe4?\x92\x83+\x1bV\xa5\xe7?\xb1\xb4\xf5\x02u\xc1\xe4?\xfa{\x98N\x8cj\xf2?\xd6\xacx\xd9[O\xf2?\x90/\x1f@\xe3\xb6\xf2?%\xe6-\xd1\xd2R\xe4?\xbe\xefEP\xd1|\xd4?\x04b\x1c/\xf4}\xc6?\xecC\xaaer\xf7\xf1?\xce\xa8;\x9fN\x90\xd7?R\xdb\xa9\xa0U\x8b\xf1?d\x147M\x00o\xde?j|+\xe4\xf5\xc5\xef?e\xb5\xfd\xe8b\xd7\xe8?s\x8c\xc2\xee\x10\x9b\xe0?\xb86\xf1S\xde\xe7\xdc?\xd0\xa4\xf7\x8b\xd1]\xf0?\x148K_\xb5\x92\xd9?\xae\x002c\t\xcc\xfd?\x86\x06_\x8c^&\xed?\x1c\xffc\x98h\xd1\xe6?8O\xae\xe5\x1a\x91\xe3?\x0c\x95H\xd35\xdc\xef?\xfe\xf04\xdf\xf0\x12\xde?n\x7f:\x15\r7\xf2?\x88:\x1e)ns\xfa?\x99\xde\xde\x11\xec\x11\xf6?\x9b\xf9\xc2\x85\xc7\xad\xf4?\xd4\xb0V\xed\x11\x1d\xdf?y\x15V\x02~\xfd\xe1?\xca\xa1\xdb\\\x9f\xa4\xf1?\xe8\xa4\x83\xf7\xd5\xc7\xd5?\xe3}\x97P\xef\x90\xfb?v\x86L\xcb2l\xf0?5\xf6_\x0eR;\xf2?\x03\xf5"\xccNE\xec?u\xcd\x82<\xe7\xbb\xee?&\xa2\x00\xd0v\x83\xed?\x89i\x9b\xe8\x87\n\xe8?0\xb2\xa5\xa1\xa3$\xe5?\xe6\x05\x1ar\x88\xea\xf7?\x91; &\xbd\xdb\xf3?z\x12l\x84\xb7?\xef?\xfb\xf6I\xc9\xe1\xc1\xe0?\xec\xc6\x04W\xbe\xc7\xf6?\xffY+\x0fh\xd1\xf6?\xde\x82\xd7p\xf2\x89\xd1?\x1f\x80\xe7^\xd4\xec\xed?\xdf\xd3\xadL\x8e\xaf\xe8?8R\x80\xbc\x01\x81\xf6?D\x95O\xf4\x8c\xb7\xf7?\x81N\xe6\x91Dt\xe3?h\xf0\xb9\xb8\xa5\x07\xbd?\xc1)\x97\x1d\x1f\xa2\xfa?8\x8fZ\xa6\x8f\xb2\xf6?\xd8\xed\xf4\xf8\x8a\x91\xf1?\x95\xa1\x8c\x0f\x10\x1f\xf0? l\xc0\x93:\x81\xf2?\x9d\xf3\xd8\xc2\x10{\xf0?\xa2\xeb$\xcbT\x88\xf0?\xe4fQ\xd2\x9d\xe1\xf7?X\x15\x9b\x11\xc6\x97\xd7?\\o0X\xcd`\xf3?N\xda\x9f\xfd\xaed\xf4?\xfa\x88\xb7\xfc\xd3\x96\xe2?\xb5&\xc3{\xee\xb2\xec?=\xea\xe0\x0f\xeb\x08\xe6?\xd6U\x8f\xdd\xea\x94\xe5?\xab\x07\x81\xaa\xe3\x11\xed?\\\x06\x97\xb3\x17\x9b\xe9?,>"\xe8\xa1\'\xf0?\x87i\xc7\xb0\xb1\x13\xef?\xce\xa8;\x9fN\x90\xd7?\xe0O#\x9aO9\xd9?\xa2\xe0h[\xb7\x83\xfc?\x12\x1b\xd1\xba\x00"\xfa?\xd1\xd69C5\xca\xef?\x13x\xa6G;;\xf7?\xff\xa8.\xae\x8e\xa6\xe7?\xfe\xfd\x84\xe6\xe4\x97\xeb?\x89\x0f\x92\x19;\x02\xea?\xe7\xc8Z];\xbb\xe0?Qi\xe3\xe6%M\xe1?W\xb7\xbb"\x85\x8c\xf0?\xe8\xffQ?\xb9\n\xe7?*|"Jd\xe4\xe5?~g\xe0\x11\x80\xe9\xeb?\xdb\xaa\xad\xc3o^\xe9?\xd3\xf1\x85Z\xcc\xff\xf0?\x03\xa8\x9a\xf2\x9b\xf5\xe2?\x86\xcd\xb5\xc0]S\xf7?\x1aE\x07\x167\xc2\xed?t\xb9\x82\xffw\xae\xc8?\xb0 \xc6\x9b\xb15\xf9?%\x15\x8b\xdf-}\xf2?$yV\x9cZ\xbd\xc8?\xdc\x106SD*\xef?\xc8\x08\x85\x83\xea\xfd\xfe?\xd2\xdaS\x94+\xbb\xf4?\xe4\xd3iU\xea\x8c\xef?\xafOM\t[\xaf\xf1?\xf8#\x84)\x8c\xe1\xd1?\xaf\x00\xc6,\xd8q\xf0?Z\\k\xa6\xac\xd2\xd6?\xb4\xc3r\xbd\xb2k\xde? \xa8\xd1\xbd\xca"\xdd?\xf6\xe2\x02\xebB\xb3\xef?t.\xa7\x1f7\x8f\xed?wa\xf1y}\x1c\xea?\xf6\xc5\xebr>{\xe4?\xc0\xc8\x07\xb3T\xf9\xf0?\xba\xd7;`&\x97\xf1?\xe2\xa3\x1f\xaf\xcd0\xf4?\x16\x9a*i.\x00\xed?\x15\x80\xd3\x8d\x7f\x06\xf2?^x\xa0\t\x13\x0b\xfa?x\xa6\xf6\xb2\xe5\x95\xe4?\x18\xdc\xc2\xf1j8\xf6?4\xcb\xa9~\xab"\xf0?\xee\xf6FE\xa6\x82\xfd?\x91\x9cH\xb4\ra\xee?\xe5\x9fO=\x84\xdb\xe7?\x92-;\x81\xd4\xfc\xf6?~V\x93\xe1\xc8\xf8\xff?\x94\r5\xca\x07\xf4\xe4?JPH\xb3g_\xd8?\xf4\x05\xadt>Y\xed?\xc9\xaf)%F\xe7\xe4?\xc1\re\xa6\x0fx\xf0?\xe7\x82ls\xf9\xa5\xfa?\xe4\x00\xd4sm\x9d\xf3?\xc7-p\xb1\x0c+\xef?X\x0f\xd1\xf6\x8b;\xf6?@\xf2\xa1\xde@\x0b\xf8?H\xbfj\xd2\r\xc2\xdb?9r\x89\xe3\xe2\xf7\xf2?R\xdb\xa9\xa0U\x8b\xf1?\xa2\xe0h[\xb7\x83\xfc?m\xb32\x92\x98\x14\xf8?\xde\xdd\xf0\x9a/\xc0\xfa?\xf6\xd4ZA\xc1\xb9\xf5?_H@\xa0J\n\xe4?\xb8\xd4\x92e\x80\x18\xd2?X\xd7\xc2c\rg\xe4?"\xc2\x1f\xec{\xcb\xf0?\xe2\xa0\xdd\xf9z\xb3\xf0?p~\x9d\xd9\x88\x86\xd2?du\xb2\x11\xfd\xfa\xd0?`V!0\xdf\x8c\xe1?\x1a\xa4\x7f\xc3\x9d\xbb\xfe?\x07\r\x9b"\xb9\xd6\xf6?\x00\xa3\x0b\xf4\xaa\xb1\xe6?\x82;c\x06\x9f\xfe\xe0?]\xa5\x90H\xa5\xb4\xee?\xe4)\xc8\xea!\x08\xf1?\xf0\x81\xbc\xf2X\x10\xb3?n\xf2\x1b.\x92\xa7\xe2?W\xea\x08L\xceq\xef?\xf9\xca\x7fa\xd2\xce\xe2?G\xe5\xee\x97ht\xf0?$Uo\xe0\xcc\xbc\xf7?[\xa0G\xe0E\xe2\xf0?+\xb1O\xb7\xb4\xfa\xea?\x10\xc8\xf4\x82\xb30\xed?\x01\xdcCa\xfa,\xed?\x15!\xa3Tk\x1f\xe2?\x80\xf1:\xbb\xb5\xc5\xea?\x0f\xb1^Y\x80N\xe0?\xa6Q?)\x97\xd6\xd7?\x8c\x95\xb3\x9aT9\xfb?21siC\xdf\xf7?\xf8\x9c\xb2\xb3-b\xe7?\x08\xe8Q99\x01\xe3?\xf6;\xa20\xf4u\xf1?\x08\x07\xc9U\x83\xb8\xe2?\xc6\xa8\x1bg\xa6"\xf6?,\x93\xbaZ\xac\xf4\xd8?J^\t\x81\xfc\x8f\xfb?v\x19\xf2\xb0q\x88\xe8?"\x87\':D7\xd9?\xe4\x80\xc6\xe6+\x8e\xe4?\xa1(\xf0\x13j}\xf4?j;\x16\xfa\xae\xe2\xf3?\xda\xb9\x81"\xabk\xe5?N\x18P\xb1\xa2@\xf4?4\xabV\xf0\xc7>\xf9?jg"\x14\xffp\xf3?\xfby\xd2\xce\x19\xf5\xe9?\xc93\x9f8B\xb6\xf8?\xbaX\x18f\xd4\xe3\xe7?$\x94\x1cZ\x93\xec\xf1?\xf3\n[\xcf\x15\xf6\xe0?\x1cn\x16\xf3\x9d\x8f\xee?\xdf\r\x07,\xfc\x92\xfd?\xac\x95\x9f3\x85\x9f\xf0?\xf6I\xf5\xc2t|\xf7?\x0b.34\'U\xf6?\x18\x1dv\xc9\xc9\xfb\xed?q\xd13\xe4\x11\x16\xed?\xbf\xa3\x86\xe7\x1a\xdc\xe8?d\x147M\x00o\xde?\x12\x1b\xd1\xba\x00"\xfa?\xde\xdd\xf0\x9a/\xc0\xfa?\x10\xcar\x95\xf9\x98\xbc?\xc0\xb9\x1bZ(\x08\xf4?\xc3\xcf\'\x89\xc8\n\xf4?\xf4\xa5\xf4\x04\x13;\xc8?\xa76\xea\x85\xe5\xf9\xe8?R\x8eO\xcc\x9d5\xf4?\x89\xb1:\x98r\x9b\xf1?Z\x0e\xcf\xd1\xc4\xb9\xd6?\xc5qR\xa6\x10\x9f\xf7?\xaa\xf7P\xacgp\xe7?Z\xe5\xb2x\xd6d\xe2?\xe8\xea\x087v\x89\xee?\xeb_\x064Z\x85\xe6?\xac\x1f\xec\xc0\xca8\xed?\xe6Z\xd0\xf1+\x90\xf2?\xa4\\\x10\x90\xec0\xf3?\x87\xf2\x7f\x1eL\x93\xe5?\xce\x8cg[#<\xf1?\xba\x7f`\xf1F*\xf2?\xf9\xe0\xfd\xc0e\xc9\xe8?\x9a\xbdQC=F\xf7?;\x0f(\xaf\xb3\xb9\xf6?4\xb7\xa2\xc72C\xf9?z\xa5}\x80\xc1\x0c\xdf?\xb8Y\x88/g@\xf0?\x1b\xa5a\xcc\xe1\xc3\xec?\x04-G\xc1\x85u\xe8?\xc0^7\x94\xfe\xe3\xf9?A5\xfa\'\xb4[\xe8?\xb8*\xc9x\x8f\x1d\xf5?VY\xa9\xa9\xf4\xd4\xf7?\x1fBY:\xdd\xfc\xf1?\xa2w\x0f\xee\xe57\xd9?\xb0\xc8\xd9\xad\xf5\xf7\xb6?\xb2\x01\xbd;\xf2\x94\xe7?]\x14L\xc6\xcd?\xf1?\xca\xb1\xd6j\x96\xc8\xf2?\xaa\x12;2\x83\xa9\xe6?\xb9\xbb\x0e\xc1\xb0\x84\xf6?t\x18\xf3\x0fo\xa6\xd1?\xeee\xc2\xa9\xf9\x82\xee?A`\r\xc3\x99\x82\xe8?"Lo\x97\x03`\xf5?\xe3;\x91\xb8\xbe\x9f\xf0?\x0c&:\xd54\x08\xf5?\xc4\x90U\x18\x84\x99\xd0?\x95\xeb\xbb\xe2\x10y\xf7?\x84\xd3\x90~X\x9c\xf3?\x04%\xe5\xca\xe6\xb6\xf3?\x01\x82\xc7\x92W\x7f\xe7?\x90?(\xd2\xe6\xc9\xdd?,\xf6E\x85\xc1|\xf0?\xa6\x18\xc3\xcdK\xbf\xf4?)\xbcI\xcelX\xee?\xaf\x96mX\x1fm\xe6?/\xc8\xe3\xf8\xa1\xfe\xe2?\x1czY\x1b\xb4\xd1\xc4?\xd0\xdf\xbe\x11\xe8}\xf5?\x87\xf2\xff\x86\xc5\xd7\xe7?\x06*n\xa1?\x94\xd9?\x02\x88(\xbc\xe5\x19\xe7?j|+\xe4\xf5\xc5\xef?\xd1\xd69C5\xca\xef?\xf6\xd4ZA\xc1\xb9\xf5?\xc0\xb9\x1bZ(\x08\xf4?\x86\xc1\xd4do\x10\xf2?\x0fk\xf2\xa5\x83\x1b\xfc?8g\xfc\x1bd\xff\xd3?0[8=\x13e\xeb?!wd\xe8\xd9\xfb\xe2?\x90\xbfQ84X\xf1?d\xbf\x90\xc1;7\xf5?\x89\xf6l\xd3\xcf&\xf8?\xf0\x0ej\xad0\'\xee?\x1cU\x8e\xff\xdc\xf9\xc5?K*o\x17&\xbf\xf7?\x0f\xaf\xef\xad\xd2v\xeb?\x97\xf8\xa1d\xbc\x15\xf1?\x85\x8f>O\xc8Y\xec?d&h\xbd\xb79\xe9?e\x8b\xf5=h\xd6\xe2?\xb5\xe5\x92\xfa\xecL\xe2?\xc9\xf4P\xde\xac\x97\xe2?\x8fZ"\x91C\xc8\xec?\xae\x83\x8a\xc4\xee}\xf2?\xebx\xfdA\xb8\xbe\xf7?\x9a\x00.av-\xfb?\xa2P^X\x96\x8b\xf2?\x08DEcB\xa6\xd5?\xb8b\x1c\x13$\xb3\xf7?G\xf4\x8b>\x84\xc7\xef?\xfa!Y^\x95\x8b\xf9?\xa0h)\xe6B\xbc\xce?\xc6\xd2\xb4\n\x14f\xf2?\x02\xd6\xab\t5\xf7\xf2?\x07\x8a+\xda\xf5n\xed?\n\x15/7;\xcb\xf2?\x1eA\x9a!+\x1e\xd6?U*\xcdM-(\xed?\xdcw\xe1\x19\x05\xba\xf3?\x9cI\xa3\xf5\x8d\x9b\xf1?\x18P\x08\x9e\xbdPU\xf3?\x87\xdb\xcf\x13Q\xb8\xed?5\x8c\xaf\x87,\x11\xe9?\x06\xee"\xb9\\\xd7\xd8?\x10JD\xc3}\xe1\xf5?M%\xd3\xed\x1cC\xe2?n7"\x1c\xd0\xee\xfa?V\xa5<\x97\xf5\xac\xe2?\xac*v\x98\xf0\xf1\xc0?\xd0\xaaK\x8f\x17~\xf2?\nK\x1fX\xf6\xdc\xee?y\x8b\xc0\xf4\xe7\x04\xfa?1\xa9T\x0cu\xc3\xe6?\x92Q\xaf\xe9\xfb\xc2\xef?\xd6\x8b\x90\xce\xaf\'\xe3?:Oi[%\t\xe5?\xa3Q\xd1\xb5FJ\xf4?\xb6\xd9\xf8\x8fw[\xde?}\x91>\xf0\x80\xbf\xf6?\x88!{\x8eYx\xe2?\xb7A\xb4\xb3Y\xde\xe5?\xde}\x86:\xe1\x14\xec?>\xe0\xaa\xf1Oe\xf9?\x1c\xb3\x15t\x08&\xf8?\x15\xc8D!\xf9\xe8\xed?g\xdb\x16*\x1f\xef\xf7?~j\xf1\x99\x17\x9e\xd3?\xb7\x02j[0p\xfb?\x1fU\x84\x8e\xd4T\xeb?\xfdxs\x7f\x9a\x1a\xfc?\x8c]\x18\x87e\xb0\xe3?\xee&\xcaH=$\xf2?\x92\t\xa66\xe9\x98\xd9?\xf6$M\x93\xa0\x05\xf3?n~\xc0~\n\x0e\xf7?\xb4eS~\xa8t\xcb?\xaej\x01*LK\xf3?,|\x97D1f\xe7?\x8b3\x93V\xbe\xc8\xed?-v\x81\xb4\xbaw\xf2? \xb4_\xd5D?\xad?\xee!\x83D\xb6>\xef?\t\xd4V\x15jq\xf1?s\x8c\xc2\xee\x10\x9b\xe0?\xff\xa8.\xae\x8e\xa6\xe7?\xb8\xd4\x92e\x80\x18\xd2?\xf4\xa5\xf4\x04\x13;\xc8?8g\xfc\x1bd\xff\xd3?\xdfL\xaf\x15\xcbz\xe0?h\x89\xd3\x85\xda\xbd\xec?\x91>dH\xe3t\xeb?S\x19)4\x88\xe8\xf5?\x88\xfds\x9c;v\xf0?TQ+\xf1\x1d\x05\xf4?\xcb\x10tj\x04\t\xe8?\xac\xe6\xe2d\x1f\xe0\xf7?\xce\x89\xdcT\xe3\x14\xf9?\x12\xddX\x90\xf0\xba\xe5?\x0fR\n`\x1cf\xf7?\xe1\x84F\x0fa\x89\xfe?\xf7gy\xf0\xeb\xbd\xf1?\x8c\xed5\xf3\xba\x8c\xea?F[kAgO\xe6?\xeaV\x8dy\xfb\xc5\xf3?\x05\x1b\x99\xe4\xbd\xda\xe7?\xcer\xbc/kN\xe3?\xba\xf3\xf2\xd3l+\xe7?\xd2\x86\x88\xb2\x81\x1c\xef?t\x12\xf3\xdc\x9b\xac\xf5?\x86r\xaf\x02\x13~\xdb?\xe2\xe64\xd4*#\xec?\x8a*\xdc\xe6\xb0\xab\xf3?%u\xf9r\x99\x10\xec?\x16A;ku\x1e\xea?\xac>\xccMd\xc7\xc4?ae\x80\xb1\x84H\xf6?^\x97.0\xb8\xfa\xeb?\x86\x93\xdd\x9d_\xf3\xf3?\x9c\xcb\xe3!Q\xff\xe3?\xb2\x95\x8d\t\t\x9b\xf1?\x1c\xf5\xbe\x1d\xe5\x03\xf2?J\xa1\xf3\xa8\xad9\xed?\xb2;(2\x7f*\xfc?\xae\x8b\xf5]\x94\xa1\xf0?\x0f8GRv\x10\xe0?\xbaJQ1\x905\xf0?\x11\x9f\xfb\x9fm$\xeb?\x11\x0c\xe5\xde\x8a\x00\xed?t\x94\x1e8h)\xf2?\x16\xc6\x13\xa6\xd9\x81\xdc?\x04\xd9n=\xbfQ\xd3?\x17\xeb=5\xe8\x06\xef?N\xdaK[\xba\xd8\xf0?\x94\xdbFj\xa4\xf4\xf0?\xc5\xa3D\x1d\xf0\xd4\xee?^\\\xee\xdf5#\xf4?f\x81\x83\xb1\xc7\xe5\xd3?\x86]\xe6\xed\x8e\x80\xf1?\x80\xbf\xbb4\xb0\xad\xf2?\x0e\xdd\x8a\x1b4!\xea?\xf5\x95z\xad>\xf0\xef?\xb2\xa6\x1c\x18\x7f&\xf0?\xe7\xd3\xb3\x05\xabE\xf1?8\x08\\\xa5\xb7p\xf2?W\xe1\xde=\xaf\x1d\xf1?c\xc6\x0e(\xed\xff\xea?\xb0\x979p\xb0W\xf4?\xb86\xf1S\xde\xe7\xdc?\xfe\xfd\x84\xe6\xe4\x97\xeb?X\xd7\xc2c\rg\xe4?\xa76\xea\x85\xe5\xf9\xe8?0[8=\x13e\xeb?p\xcf\xa7\xe9\x05\x83\xe5?\x91>dH\xe3t\xeb?8\xdb\xd8\xf90~\xed?7\xe8)n\xf4\x83\xf3?B\xb3\xf2\x1b\xda|\xe2?\x98\x86P\x92O\x89\xf2?\xd8bm\xc1\x91\xe7\xed?\xcc\xa2\xdai"\xd7\xc7?\x06\xd2|\xc5\x9b=\xfe?p\x151y\xa6\xf7\xf3?\xdb\xdeD\x84\xbc\x0b\xed?\x9a\xb8\xbb\xf4\xdf\x12\xfc?P\xde|\xde\xfd\xef\xfa?VAq0\xf98\xf5?\x043\x9f\xac\x93\xf7\xc1?%\x8b_\xd1\xa3\x87\xf4?\xd9\xe7{\xa7e\xe6\xe9?\xd5\xb4\xc1\xacU\x01\xef?\xa45\xed\x9b\x1b\xe9\xdf?\xcc\x13T\x00/E\xf0?>d\xa0x\x15\x8f\xdb?/\x9c\xe2\x98(F\xed?\x80\xd5\xee\xc4\x19\xa5\xea?\x01\x8c\xdd\x9e\xb1\x99\xfb?\x9e\x94\x89\x8c&\x08\xf7?|\xf41\x14\x89u\xe5?\xceW\x98\xe0g]\xe6?\xf7b\x84W\x891\xf6?\xe4\x18\x1b\xb7\x1f\xde\xf6?\xb0\xc4\xb6\x1d\xd3\x87\xed?\x1c\x1c\xe9=\x8d<\xee?#\xd6~GS>\xe2?\xa2\x11\x19\x1e\xf3\xc1\xd3?\x10B\x94\xf3G\xd6\xee?\x1d\xb2\xca\xff\x15&\xf0?\xafS\xd0tt\x19\xf2?KAM_\xc7\x9f\xf1?\nClE\x96:\xeb?\x12\x1e\xcfN\xb8\x13\xfe?\xce\xeavufB\xe7?\xaa\x92~z\x95p\xf0?\xc4\xb45/\xf3\xb3\xf4?J\xd1\xf2\x08\xa4]\xf4?\xca\x8a\xa3\x9e\x91\xd2\xf2?NL\x03v\xd7\xa5\xf7?l\x93|\x8b,\r\xe4?T\xbb:x\xfa\x81\xf0?."k8\x04/\xe8?\xe0\xc9\x05\xd5\xaf\x06\xd8?\xef\xe6-\x81\x87f\xfa?8\xdf\rs\xd5d\xf4?\xef6\xae\xc7\xd5\x81\xed?\x99C"c\xb5J\xf2?\xd8\xab\xd8\xfe?\xb0\xf1?\xe6\x1fAJ\x81Z\xd5? \x1d\xc9n\x97\xc5\xdc?\xa5\xca0\xfc\r\x94\xfd?\xbee\x1f7\x1c\xec\xfb?>\xc8P \x82\xc8\xf1?\xd0\xa4\xf7\x8b\xd1]\xf0?\x89\x0f\x92\x19;\x02\xea?"\xc2\x1f\xec{\xcb\xf0?R\x8eO\xcc\x9d5\xf4?!wd\xe8\xd9\xfb\xe2?^\x83j|\xb4\xbd\xf1?S\x19)4\x88\xe8\xf5?7\xe8)n\xf4\x83\xf3?P\\\xfe6\xac\x1e\xdb?)(\xcd\'\x94\xcd\xe9?2\\a\xd8\xa26\xf8?~\xdbo|\xc4\x80\xef?\xa6Q\xd3\xbb\x9dN\xf6?\xf6W\x06\xf9}9\xdf?\xa4\xe2G\x81\xddu\xea?\xd4\xd7\x16\xe5\xcc\x1c\xfc?\x93\x83\x11\xec<\xa6\xed?\xa9\xc5\xb6\x12N\xf4\xf0?{RM\xf9\xfc\xca\xf7?\xc3\\\xc20?_\xe7?\x18\x06\xed\xf1e\xeb\xfc?\xb1\xbf\x0c\x81\x1bp\xf5?\x14\x999L\xaf|\xc5?\x00\x8d\xf0\x06\xdc\x12\xf3?\xab\x9c#\xe1r\xd7\xe7?\x19\xaa\t\x83\\\x10\xf3?\x90\x83\x1a\xb93p\xf0?WM\xc0\xc6e\xef\xf8?\x86\xe69\x87\xc5\x14\xef?\xab\xa8\xc7\xca:\x10\xec?j\xdd\xdf\x112*\xf5?\xf6\xb5.S\xdc\x00\xfa?\xcd(t[\xa26\xf3?==\x87 \x0c\xe0\xe9?gwy\x93\xbd\xdf\xe0?\xca\x94\xc9\xe8\x1b\xa5\xed?\xaa\x04mb\x99l\xe7?\xc62]\xd8\x12\xc6\xf1?`,\xa1;\xebe\xec?\xf2\xe5\xb3\xe3M\x00\xed?N\xb8\xab\xb9\rT\xe9?\x0cT&\xda\x1b\xbf\xf0?8PwI\xfa\x15\xe2?\xf4\x05\xacoz\x9a\xfa?\x1f\xf3\xca\xd9\xbbA\xef?\x91\xdc\xdb\xa9k\x81\xe5?5b/\xd0\x13\x08\xee?\xb8T\xdd\xcc\x1f{\xf5?\xe8G\x10\x16GS\xef?{\xf4G\x94\xeb\xfc\xf1?h0\xf9\xfd\xf1\xbb\xf9?\xfb\x82j~t\xdc\xf0? \xc9\xa9\xd51\xb6\xdc?|t@\x88F\xc8\xf1?\xc6\xb6)\x90-*\xf0?\x91\x07\xae\xd9\xd6\xb7\xeb?\xdc\xa4\x9b\x84\xef\xc9\xec?9-U\xa5\x00\xe1\xef?Iy\xe7\xef\x17\x9c\xec?\xa6\xe6\xc1\xaa\t\x83\xf2?\x15\xf6\x8f\x98\xd2c\xfb?\x18\x91\x91\x00\xbd-\xdb?\xf8\xa9H\xeeB*\xd0?j9S\xf8\xa3(\xe1?\x148K_\xb5\x92\xd9?\xe7\xc8Z];\xbb\xe0?\xe2\xa0\xdd\xf9z\xb3\xf0?\x89\xb1:\x98r\x9b\xf1?\x90\xbfQ84X\xf1?\x96\xb1w\x05\xf4\x8c\xeb?\x88\xfds\x9c;v\xf0?B\xb3\xf2\x1b\xda|\xe2?)(\xcd\'\x94\xcd\xe9?$#\x80\xd1\xd3\x16\xeb?\xc4\t\xfb\x05G\x85\xe7?\xa0\x93\xd5g\xb2\xf3\xd2?\x82\xe94\xc5\x9c\x13\xf1?\xaf\x83\xc7\x01\xd2\xdc\xed?\x10p\xb9\x08\x97\x0c\xf2?\xf0kd\xcew\x91\xf5? \xe6s+\x94 \xfb?\x8e\xf4\xe1\x0b+c\xf0?\xc3\x07\x8a2\x15,\xf6?V\xddY\xbfm\x9e\xf4?\xae\xa2\xb4Dq\xb0\xd7?\x02s/\xb5}\xa4\xe2?\xda\xc5,\x8b\xe68\xf3?\xa85\x19\xc8V \xf9?*\xab7\xb6Q\x03\xf2?\x06T!\xb8!\xb6\xf1?B\xa3\xdcZ\nw\xf4?\x82\x0f\xa5h\x1d\xe4\xf7?\xb8\x16\xc3\x0f\x9fn\xcb?4\xaf\xfd\xc2\x01\xeb\xfa?\x92\xec\xd0ZE\xaf\xf3?b\xe8\x8bI\xaf\xed\xe6?\xe12 \x13\xd0>\xe8?P\xfc\x84\x02\xad\x10\xeb?\x82\x89iA\x88\xce\xf2?\xb0\r\xcab<\x01\xe6?\xe0\xa1\x17\x80\xc3\xb6\xd4?\xe4X.\xab\xe9\x15\xf1?\xc6hhA\x1d9\xfb?4\x85_\x9d\xe2^\xca?\x0b\x1b\xb2?\']\xe8?D2p,\xd6\xaf\xf2?E2\x10\x8d\xfa\xd2\xf0?\x16\xeb_\\\r*\xe3?\x0ft5\xcf\xa0\xc7\xee?\xa2\x92\xd2\xec\xd5\n\xf0?\x1f\xec\xfe|\r\xd8\xe9?\x03O\xc3\x90\xe05\xf0?j\x14G?\xd7c\xf0?\xb0\xc9\x1cyon\xf6?\xbc\x9a\xc4[T\xc5\xfc?\x17\x98\xa4L\xd9\xe5\xf5?]\xfe\x05}\x86C\xef?\xcb\xc73\xff\xacN\xef?\x1f(Q\xdc\xf4*\xec?\xb9\x00\r;\xf2\x8c\xe5?\xce\xedz\xca\x19\xc8\xe9?\x16^\xe1\x14\xac\x1d\xef?\x07bO$W[\xec?\x85\x90\x85\x00\xa3\xcf\xe8?\x06\x8f\x7f\xa2,\xb3\xf5?"\xe6\xdd_k\x93\xef?\xbf\x1fpk\xfe\x00\xf1?\xb1U%\n\xfa\xdd\xf7?\xae\x002c\t\xcc\xfd?Qi\xe3\xe6%M\xe1?p~\x9d\xd9\x88\x86\xd2?Z\x0e\xcf\xd1\xc4\xb9\xd6?d\xbf\x90\xc1;7\xf5?\xde\x1d\x06p\xf2\xa3\xdc?TQ+\xf1\x1d\x05\xf4?\x98\x86P\x92O\x89\xf2?2\\a\xd8\xa26\xf8?\xc4\t\xfb\x05G\x85\xe7?\x9d\xc8\xd57\x134\xfd?7\xcb+\x11.\xac\xf2?J)\xcc\xcd\x14\xab\xf1?~\x1bRo\xa6O\xf9?\xb8\x14z\x0eM\x04\xea?\x98\xa7i\x06\x1b\xaf\xe6?\xa1\xf7\xf6\xa1^\xe3\xf8?{\xad\xcau\xd8\xd6\xe1?\xa4\xa4\x8b\xc8\xcf\xe1\xf6?vv\x97\xa3YJ\xf5?b9jn\x95\xdd\xf1?\x0f\x92v\xd0Z\x14\xe0?\xec\x13\x15\x8a\xf0t\xf9?6-\xec~a\xfe\xf1?8amCUj\xf2?\x1a\x7f\x8c\xc1T\x81\xf2?\xb6\xa4\xde\xc8sY\xeb?\xac\x88\x12\xb7C\xe2\xea?\x82J\xaf\x91w\xee\xf3?\xb5ynT\x90G\xf3?*\x96\x8c\x81\x9c\xcd\xe4?\xae?\xdc\'j\xde\xf0?\x90\x1f\xf1r\xad\x0e\xf0?r\xbeZ\x8c\x1cq\xf5?\xf6+\x9c\xf4GL\xf1?\xcev\xba\x89\xf4\xeb\xf3?\x18\x8e~\xf7\xf5+\xfa?@b\xf1\x05!\xc7\xeb?\xcc\x17\x1f\xcaZ\x89\xec?\x0e\x82q\xde\x8a\xe7\xf2?~7:VX\x03\xee?\xa0s4\xc8\xa1\xe3\xd7?zC\xa8\x92\x8e|\xe4?"\xd3A\x04\xcdQ\xf0?\xf9i\x9b\xd6b\xeb\xf7?\xa2\x95\xfb\xa5Q\xa0\xfa?\xe7K\xe31\xd2>\xe6?\x03\xfa\xb0_Zh\xef?\x9es\xd1\xc6\xb6\x19\xf6?\xb9\xa2\x94\xea\x81\xed\xea?\x90)*\x98\xf4\x0b\xf2?\x02Q\x01\xb9\xb4\xe2\xf0?*\xd6\x05[\x0eD\xeb?\x8bC\xcd\x13\xadD\xf5?H\xdcd\x08\x99\x07\xf8?\xcd\x99\x01\xca\xe6\xf8\xee?J\x1a_\xabq\xd4\xf5?\x966\x7f\xec\xdd\x8e\xee?\xdey\x0f-W\xe9\xd6?\xf4N8`\xe7\x17\xf1?\xbc\xc4ed8\n\xd1?\x18 \xd11%\x18\xe6?q\x9bc\xfe3\xa3\xec?h\x9ejA X\xe6?\x86\x06_\x8c^&\xed?W\xb7\xbb"\x85\x8c\xf0?du\xb2\x11\xfd\xfa\xd0?\xc5qR\xa6\x10\x9f\xf7?\x89\xf6l\xd3\xcf&\xf8?\xebJx\xf6\xaeg\xe6?\xcb\x10tj\x04\t\xe8?\xd8bm\xc1\x91\xe7\xed?~\xdbo|\xc4\x80\xef?\xa0\x93\xd5g\xb2\xf3\xd2?7\xcb+\x11.\xac\xf2?Hh2\xb4\xef_\xd2?\x841(\xfd2\x9f\xe8?\xe1\xfc\x1aAd\xdc\xe4?\x84\xd8.|\xab\xec\xf3?\x89b\xf1,!\xb2\xec?qB\xc7\xf44\xd9\xe0?f\xb8\x8b\xbc\'\x83\xf6?\x14F%\xab\xcb\xfd\xe4?\xd7\xebq\xd2z\x0c\xea?j\xb8J\xcfY\x06\xf6?\xdb\'!\x1e\x00\x99\xfb?>\xb0\x91\xee\xf2=\xf3?\x0eT\x07\x0e@\x8f\xde?\x9e\xaa\xa3I\x1b\x01\xf3?+\x9e(\xd6\x9f:\xf4?\x1e%\xf0\xbe\x0c\xd0\xf4?x\xfaH\xb100\xfb?(\x1e[2{\xa6\xf4?r3\x0c\x18\xb5|\xf7?t6\x83\x06\xd7\xf0\xf7?Tt\xb3\x18D\xd2\xce?\xb9\xc1\xd7\x94\x81\x02\xef?N4\x10^\x96\xc9\xf0?F\x90\x8d\xa2\xb0G\xfa?\xeb4\xe6\x05\x85\xc8\xfa?\x03\x1f\xfe\xd9\x91\xa1\xf1?Gg\xd7\x8c*\x91\xf9?zN\xcb\x7f\xfa\xf8\xf4?\xe6Z{\x19\xc6,\xf2?%`\x96\xda\x9d2\xe8?\xed\x1e\xe1/a\x18\xf6?\xbb\xb1\xad}B^\xe2?\xbeG&\x1fXv\xda?\xf97\x85*\xe1\t\xeb?\x14L\x98\xf2\xe9\xbe\xe7?\xbe\x81\x937\xa2a\xf5?ig\x9c[\xccx\xe8?miJ~\xde\xb0\xe4?\x18\xa7b\x91AR\xfe?\xf6s\xd4\x1c\x10\xbf\xe0?=z\xc8\xed_K\xec?\xd4\xec\x19S\xbb\xd7\xfc?\x86\x89\xbf0\x7f\xd7\xf0?6\xb4\xe8l\x1f\x1e\xf3?8\x10<(\xaa%\xe4?\xfdF\x95\xc7$\xd4\xef?\x08^~\r\x86\x0c\xc6?\x94\xa7<1\xd4\x91\xf3?\x1aJ\xe9\x92S\xd4\xf1?\xceQ;2\xbf\xc0\xf7?\x10faW\x8am\xf7?8\xe0L\xf6\xf8+\xf4?\xaa\xe3X$\x1fv\xf0?\x1c\xffc\x98h\xd1\xe6?\xe8\xffQ?\xb9\n\xe7?`V!0\xdf\x8c\xe1?\xaa\xf7P\xacgp\xe7?\xf0\x0ej\xad0\'\xee?\xcc?\xee\x0e\x06\x97\xf2?\xac\xe6\xe2d\x1f\xe0\xf7?\xcc\xa2\xdai"\xd7\xc7?\xa6Q\xd3\xbb\x9dN\xf6?\x82\xe94\xc5\x9c\x13\xf1?J)\xcc\xcd\x14\xab\xf1?\x841(\xfd2\x9f\xe8?\\\xd0$\xcc\xa0\x99\xe5?\xf4\xa4#_\xeb\xb1\xed?u\x12\x97\xd0\xe2^\xed?\x01\xf1\x9d\xb5\xef\n\xf8?\xee\x96\xd8\xd7\x91\xf5\xf4?u\xf9\xac\x90\xabn\xef?\xc3\x99\xb2\xfd\x1eO\xe4?\xd0@\xc0D\xbf,\xe1?#s\x86\xaa#\xdf\xe9?\xa2\xa7"\xbd\xfc\xdb\xe9?V\xc5\xf1;\xdcR\xf0?\x86ja\x9c0\x17\xf2?\xb9\xbek\xe8wC\xf3?\xecT\xcf\xee\xb2\xb2\xfa?<\xbfbI\x12\x17\xea? 3s\x13t\xb2\xf1?j*n\xc8"2\xd2?\xa3\xa5\xe9\xb8\xdb\xe0\xf9?u\x9cey\xe6\t\xe4?(\x03Px\xbe\xae\xe7?\x1d\x18\xec\xe9\xc9\x19\xef?w\xb5\xa2F\x0c\x8d\xf2?\x16K3?\xe6V\xf0?\xbbG\xee\xbc\xb5\xbb\xef?\xdc\x86(\r\x92T\xf5?\x11\xc1\xb7\x9a\xb6\x05\xef?\xe3\x92u\xd3\xef\xc2\xef?\x0eg\xce\xcbK(\xf3?\xa2\xb7\x82L\xbf\x91\xf7?\xa6\xaa\x06\xc2\xb8]\xd3?\x1f\xc3\xe0\x00"2\xec?\x1d=\x19\xa9cA\xf3?\x17\x08,\x96\xb2\xdb\xf2?pl>\x0c\xe6"\xf2?\x82V\xcd\xddXt\xdc?\xf8[\xcf>\xd61\xf2?\x98x\xbf\x0c\x00\xaf\xfc?\x1c\xb6\x0c\x1f\xea\x8f\xe0?S2\xefE\xd5\x9d\xe8?+K\xc2\xd5\xf4\'\xf2?E\xe4UJL\x0b\xed?\xb4z\xf1\xf9\x0f\xe9\xf8?\xc2\xbf\x85!\xb2^\xf1?\x80G\x04\xe9\xb3\x1c\xf2?\xcca9?t\xb7\xee?\xe8Rz\xd5\x0e\x8b\xf0?\xfe~\x03df\x00\xf9?$\x05\xc4\x98\xcd\x94\xce?4\x0e\xaa\xa8\xc1\x9c\xf4?\xa6\xcb\xfd\x01y\x88\xd3?\xb2\xe0\xfc\xa0 Y\xf2?,\x95\xce\xa9u\x93\xee?8O\xae\xe5\x1a\x91\xe3?*|"Jd\xe4\xe5?\x1a\xa4\x7f\xc3\x9d\xbb\xfe?Z\xe5\xb2x\xd6d\xe2?\x1cU\x8e\xff\xdc\xf9\xc5?\x802aFLS\xf1?\xce\x89\xdcT\xe3\x14\xf9?\x06\xd2|\xc5\x9b=\xfe?\xf6W\x06\xf9}9\xdf?\xaf\x83\xc7\x01\xd2\xdc\xed?~\x1bRo\xa6O\xf9?\xe1\xfc\x1aAd\xdc\xe4?\xf4\xa4#_\xeb\xb1\xed?\x10w\xbdf\x9b\x90\xe1?z$\xd7k\xbdA\xd5?\x0eH\xdb\x8e,\xeb\xe7? \x8b\xbb\xe7-\xe2\xf7?\xf5\xe66S\xa3\xd6\xf2?\xa8o\r\xc8e;\xf2?D\x99{\x0f%e\xf5?\xfaG\x82[\x97\xba\xe9?\xc5"7\xaf\xb4\x96\xf4?\xc2f\x0f\x1a\x9b$\xd1?.\x8c)\xae\xaa\xe3\xe8?\xfb\xcd\xc2\xe7\xc5=\xef?\xb9\xd5\xe5\xdc_r\xe6?\xa5`\x8d\x11q\xab\xe2?r\x168\x8a\xe9?\xea?X\x10pQ3\xd7\xe8?\xd0\x14e\xa7d\xcd\xf3?\xfd\xfb\xfdT\xdf\x91\xff?\xf40O\xb2?\x0e\xf3?\xd4\xf5+\xb1\xc6\xeb\xf6?\x02\xd5\x83\xc1\xf8K\xd4?H\xee\xce\x8d\xac\x01\xe7?\xe0\x8d\xfb\xbf\x80\x0e\xdd?\x9dQ\x97q\x1f\xee\xe9?`\xed\x03\x8e\x8du\xfa?o\xfd\xc2\xfbp.\xf5?\xd6\xdb\x91\xd3\x12,\xf3?"\xc3X\xd5\xb0\xa7\xf1?.Y\xde\x05W\x9c\xf8?\x88^\xa6\xf8\x9d\xa4\xcd?\x0eg\x14\xbf\x11\xdf\xf3?\xde\xd0o\xa5\xab\x0c\xef?2\x93\x9e\x1c:n\xf4?\x94\xfd\x075\x9a\x88\xe6?\xdc2R\xfd+\xc3\xe6?{8\xe7\xf6\x8e\x1a\xe7?\x9f\x1di\xe0Ln\xed?\x9f\xa7\xdb\xc9\x04i\xe0?\xfa\x14\x06\xb8O\x14\xf3?#\xe8\xc2\xad\xf4E\xf7?l\xbb)\x08\xa9\xfb\xf2?\x16v\xd7\x98h3\xf4?\x8c\xb8\x94\x1f\xed\xd3\xf7?0\xff\tN3\r\xf3?\x99u\xbc"\xca\xb6\xeb?\xb8T\xc7Da-\xde?l\xc6M\x81\x95y\xe8?\xd8\x00.\xef\xdd)\xf4?\x86\x17c\x17 e\xf3?b\xba9\xdd\x1c\xad\xfb?\x98\x95\xb8\xcbL\xda\xf7?\x0c\x95H\xd35\xdc\xef?~g\xe0\x11\x80\xe9\xeb?\x07\r\x9b"\xb9\xd6\xf6?\xe8\xea\x087v\x89\xee?K*o\x17&\xbf\xf7?M\x08=7t=\xf4?\x12\xddX\x90\xf0\xba\xe5?p\x151y\xa6\xf7\xf3?\xa4\xe2G\x81\xddu\xea?\x10p\xb9\x08\x97\x0c\xf2?\xb8\x14z\x0eM\x04\xea?\x84\xd8.|\xab\xec\xf3?u\x12\x97\xd0\xe2^\xed?z$\xd7k\xbdA\xd5?\x8a5\x17#\xc8\x9f\xf1?\xf3j\xda\x03\x02\x10\xe0?\xa1`\x1f\x0f\xd6\x03\xe9?\xa6W\xa3f\x02\xf4\xf1?\xc8\x8e\x033\xf4\xc9\xfa?\xe2\xe1\xac-\x0c\x1c\xeb?\xf6\xd8\xab\x0f\xf2b\xfe?\xba\x9e-\x9e\x16\xae\xf1?Q\x0b\xf6\x072K\xf1?Is\xacxI7\xf5?\x88\xf9\xd9\x14\xcdd\xf2?\xe4\x1f\x9d\xd5\xa3\x02\xf7?\xcb\x92%\x1f\xca\xba\xef?\xcajR"\x1a\x03\xf5?T\x90?O\'\x95\xf3?\xea\x00\x8f\xa5\xa6\n\xdf?\xd4\xd7\xa3D\x81\xeb\xca?\xa6\xeda\xb6)+\xf0?4t(\x1bb\xea\xec?\xc5\x97\xc3\x9br\n\xee?\x12\x80\xff\xb8[\x03\xd4?\xcb\xa5{\xb6\xaf\x8b\xe0?\xc6\xd0\xae\x88\xf6\xe4\xde?O\xab_r\xff#\xef?\t\xff\xac\xd1\xaa\xc9\xe1?\xfeq\xdea0@\xf7?\x9e\\\xa5\xbc\x0f\x01\xf2?P\xb9\xde\xca\xf5\xd3\xeb?f\x18C\\\x9cb\xe7?F\xd1\xb8\xf9\x83\xfc\xe6?\xc5\xeb\x1f\xef\xd2\xf2\xef?\x7f\x0b\x95B\x0b,\xe3?\xaa\xb2\x8bu\'b\xfb?3X\x08W\xe9B\xea?n\xed`!\xc8\xf6\xf5?&^\x95cA:\xde?.W=\xa8\x19\xdc\xfb?\x90\'iH\x99o\xf5?3\x16)\x9cw\xd7\xfb?\xfe\xd7\x97\x14l\xf7\xe0?P\xa7ES\x01\xae\xb9?\x951\xaf*\x87\xa9\xe8?\x9c\x9d\xa1Q\xa6\xd1\xe1?\xc8\xd1\xeb\xed\xd8\xa9\xf2?l\x0b\n\xeaRy\xef?\x00&\x04\xbbU\xe4\xab?z\x91U\xf67S\xf4?\xc3\xf7\xdf\x0f\xeaf\xeb?\x12\xa4c\x1f\x904\xf3?I]\x98X\xa2\xc1\xf3?\xfe\xf04\xdf\xf0\x12\xde?\xdb\xaa\xad\xc3o^\xe9?\x00\xa3\x0b\xf4\xaa\xb1\xe6?\xeb_\x064Z\x85\xe6?\x0f\xaf\xef\xad\xd2v\xeb?cH\xf1$\x99F\xfa?\x0fR\n`\x1cf\xf7?\xdb\xdeD\x84\xbc\x0b\xed?\xd4\xd7\x16\xe5\xcc\x1c\xfc?\xf0kd\xcew\x91\xf5?\x98\xa7i\x06\x1b\xaf\xe6?\x89b\xf1,!\xb2\xec?\x01\xf1\x9d\xb5\xef\n\xf8?\x0eH\xdb\x8e,\xeb\xe7?\xf3j\xda\x03\x02\x10\xe0?\xacX\xf1v\xc4\x94\xf6?:\x04\xaf~\x9b/\xeb?\xbc\xb4S\xca\xb6e\xf4?\xff\xaeht\xac\x9a\xf6?(\xc2\x9b\x82\x98\x86\xf9?\xc8\x1a\xfds\xbf\x8d\xf4?,\x9e\xce\x92\xa4\xde\xf0?H\xbcL\xe6\xca\xa8\xef?\xce\x0e\xcd\xd99\x83\xfc?\xcb9\xd8\xc9k\xf6\xe2?\xbe\xedf@\xe3\n\xf6?\xddr\xd8-[\x9a\xf7?\xef\x0e\xf2\x93M\xf5\xf4?\xa0\x9c^n\x19\xe0\xce?\x9f?\x13\x07\x07\xab\xe7?\x12\x83\x88nnk\xf0?\x8cQ"\xfe\xc0\x8a\xef?B\xbd\x01\xc6\xff\x90\xee?\xa4#\xde\x12f\x8d\xcc?\xabN\x02bU\xf4\xee?\xae\xac\xa6,\xbd\x0f\xea?\x1d\xa2\xe6\xaeI\x9c\xf0?b\xc7\xd5\x127\x93\xf3?q\xd4\xa2\xefl\xe3\xec?t^n\xdb\xadm\xd6?8\xfc|\xfel1\xf4?\xe4\xc7\xcd\x8b!\xe6\xed?\xca6\xec\xd3(\x14\xf2?\x18/#F\xe1#\xf1?\xd2\xaa\xf2\xa4\xb0\x06\xf3?\xab\x8b\xf1\xabvU\xf0?\x9d\xfd\xc0WJ\xd2\xf0?Xan\xaf\x9cg\xf9?4~c,8\x07\xea?m\x9d\xa4\xa0^#\xe4?\x06\x13*\x8bC\x8d\xf5?V\xac\xd3\xcdQ\x9b\xd0?\xd8\xd3\xf1\xbf`\xe3\xf6?\xc5\x87\xfcO:2\xf0?FO\x9e\xab\x00\x85\xf4?t\x9a\x94\xfc2j\xdb?\xe4p\xba\x903\xb6\xea?:0\xb2\x14T!\xec?KK\xc1\x94\xb9\xb8\xf4?;iK*kT\xe4?\xda\xd16\n7\x9b\xf2?\xc7\xe2\xf5\xfb]\xc5\xeb?H]u+\x1a\xc0\xf0?\xe6\xac]\x9d\x88H\xfa?n\x7f:\x15\r7\xf2?\xd3\xf1\x85Z\xcc\xff\xf0?\x82;c\x06\x9f\xfe\xe0?\xac\x1f\xec\xc0\xca8\xed?\x97\xf8\xa1d\xbc\x15\xf1?\xfc"c\xba/.\xf3?\xe1\x84F\x0fa\x89\xfe?\x9a\xb8\xbb\xf4\xdf\x12\xfc?\x93\x83\x11\xec<\xa6\xed? \xe6s+\x94 \xfb?\xa1\xf7\xf6\xa1^\xe3\xf8?qB\xc7\xf44\xd9\xe0?\xee\x96\xd8\xd7\x91\xf5\xf4? \x8b\xbb\xe7-\xe2\xf7?\xa1`\x1f\x0f\xd6\x03\xe9?:\x04\xaf~\x9b/\xeb?{\x9c\x12\xa1\xa6\x89\xf4?\x88\xfb\x08d\x99\xcd\xd7?\x8e\xcd\xb1\xe1 7\xe1?]\xfe,\xcb\xd2\x85\xfd?\x00\xd4g\xd7\x9cn\xf5?r\xcc\xc8\xc13\x83\xdb?\xbc\xe1\xa4\xee\x9e\x8b\xea?_\x11\x97Zt\xf5\xed?\x92[[\x92(\'\xe0?\x1b\xe2\xf9=\xca&\xe8?\n\xdc!T\xed\x04\xf0?\xde\xeecS\xb3\xbe\xee?\xf5\xeb9)bm\xf7?\xd2et\xd7\x0eF\xf9?\xc6\x85\x9ah\xba\xb5\xf7?\xb1)\x7f\xf0\x14<\xea?\xac\xce\x94ge\xc3\xf6?:\xaf\xa4\x04\xc4\x1a\xe4?\xae\x13k\xb3\x89\xeb\xf8?\xac\xa6\xaa-e\xc4\xf1?\xd8\xdbZb\x18`\xf1?\xce\xfc\xfa\xa9\x88j\xe7?t\x04[mq\xc9\xf1?*\xab\xdfQir\xf7?\xe8\x18\xfbz\x82 \xf4?U~\xc8(\xce\xb2\xee?\x07\x14\xf8\xc7\xb8=\xe1?\n\xc8\xaa\xae\xc1\xde\xe8?\x14\xbeU\x03\xc31\xf5?\x82\xf7\xa7\xa9\xad\x91\xdc? F*\x10>z\xfb?=\x7fef\xa6\xf7\xf3?\xdeLr\xb2<\x15\xf3?3\x01\xf3\x17\xeb\x90\xf1? CEK\x98;\xf1?\x0f\xd3LXr\x04\xe3?\xe7o\x9d\x89\xaa/\xf8??+\xf9[\xd8>\xe2?S`\x9d\xb2\xf1}\xe8?\xfc\xc9g\xe9\x05\xc4\xf6?\xada*\xf6\xd5\x7f\xfe?\x00Cy\x80\x94B\xf2?\xf2\x83\x93J\xc9\x9a\xf9?C\x03\xbfe/i\xed?\x8a\xadP\xfb2\xf0\xf4?9\x1c\xa6 6\x9c\xf1?,\x03\xf6\xd1\xac\xc4\xe0?\x19\xa1\x0c\x93|7\xe6?\x88:\x1e)ns\xfa?\x03\xa8\x9a\xf2\x9b\xf5\xe2?]\xa5\x90H\xa5\xb4\xee?\xe6Z\xd0\xf1+\x90\xf2?\x85\x8f>O\xc8Y\xec?\xd2d\xe7\xe4\x05\xef\xf8?\xf7gy\xf0\xeb\xbd\xf1?P\xde|\xde\xfd\xef\xfa?\xa9\xc5\xb6\x12N\xf4\xf0?\x8e\xf4\xe1\x0b+c\xf0?{\xad\xcau\xd8\xd6\xe1?f\xb8\x8b\xbc\'\x83\xf6?u\xf9\xac\x90\xabn\xef?\xf5\xe66S\xa3\xd6\xf2?\xa6W\xa3f\x02\xf4\xf1?\xbc\xb4S\xca\xb6e\xf4?\x88\xfb\x08d\x99\xcd\xd7?\x82\xe3\x18\x81\x93h\xf5?\n\x19\xf4\x1c\xe4T\xf1?\xd0\xee\xa6%\xec\xa5\xe8?\xca\xe9\xa1\x9a\xefV\xed?`\xa7p+\xbd\x17\xf0?>?6\x9b\xc4\xb3\xdf?\xb2l\xdd\x03)\x9b\xdc?\x82\x95\x8eA\xb0\x1d\xfc?"\x1dN\x04\xa1,\xf0?\xb0\xc8.\x05\x0cI\xf0?\x11H\x187\xe4f\xfc?\xaf\n \x9fe\x86\xef?,\x932\x125\x07\xf2?\xc6\x90\xbc*\xfc\x1e\xfa?2\x1d\xab`\xa18\xdf?\x8e\xe3\xf8\x15F+\xf0?\xff\xc6L\xe4\xf5\xb9\xff? \xd3\x85\x8c"\x1e\xf7?~t\x9ba\x86 \xf7?t\x1c\xc8\xcd\x02\xaa\xd4?\x88\x8a\xea?\x9e-\xee?(04J\xbc0\xf6?\xee\xa8@\xabi\x1b\xf3?\x87\xf5s4\xbe\xa9\xf0?\x16J\x1d\xf8\xddu\xf2?\xb3\xa0!*\xb3\xb2\xfa?fv\x9c\x99\xdc\xfb\xef?BT\xc5\xddZ\xaa\xf9?\xd9\xb1\xf9\xe5Q\x01\xe6?z\x065\x9b\x81\xb5\xe2?\xb2\x97c-\x87]\xf4?\x1c\xa9?\xe9\xaa\x93\xf5?\x82\xa3Q\xffTD\xf8?\x95\x8e\xf0\x90\xa1\x82\xf8?\x1a\xb9\xff\xe7\xfb\xa1\xd6?\xe3*#\x97\xad\x86\xe4?t\x81#\xb8\x87\xad\xfb?J\xe52\x8c\x90\xf4\xf7?\xc7\xa1\xd46]\xea\xf8?\xffjIt\xfaN\xef?\xe5\xc8\x94\xeet`\xe6?\xb0\xcf\xcb\xf3\xf3X\xf0?\xe8\x19\xbbm\x07 \xe3?\x9a\x10\xae\xe6\xc3?\xed?\xf9{\xb6\xb5{\xc2\xe5?\x08\x8b$\xcd\xe2|\xf8?%Z\xdd\x89W\xa1\xe6?\x99\xde\xde\x11\xec\x11\xf6?\x86\xcd\xb5\xc0]S\xf7?\xe4)\xc8\xea!\x08\xf1?\xa4\\\x10\x90\xec0\xf3?d&h\xbd\xb79\xe9?\x98\xd1!P\xfc\xf6\xdf?\x8c\xed5\xf3\xba\x8c\xea?VAq0\xf98\xf5?{RM\xf9\xfc\xca\xf7?\xc3\x07\x8a2\x15,\xf6?\xa4\xa4\x8b\xc8\xcf\xe1\xf6?\x14F%\xab\xcb\xfd\xe4?\xc3\x99\xb2\xfd\x1eO\xe4?\xa8o\r\xc8e;\xf2?\xc8\x8e\x033\xf4\xc9\xfa?\xff\xaeht\xac\x9a\xf6?\x8e\xcd\xb1\xe1 7\xe1?\n\x19\xf4\x1c\xe4T\xf1?\xa4\xf6\xd1\xc1j\xd0\xf8?l\x01\x89v3\xda\xf3?70(\xef\x99C\xfa?\xf1\x11\x04\xb6\x08\'\xee?\x8e/\xe0x\x1f\x1f\xf5?F\x99px\xa8\x02\xf2?\x12f{\xcfW\xbd\xec?X\x07\x95_\xe1\xc8\xf0?\xe2>tC\x05\xf6\xf0?\x00\xd6\xb7\x9f\x92\x81\xe5?\xf6\x82{\x0e\x05\x96\xec?\x9a\x02\x02\xbc\x1e\xac\xf1?U\xcf\x1f\xf2\xbe\xd0\xee?2t\xf3M\xad\xdf\xf5?D\xca\xb4\x99\xe5\xb0\xf2?\xfc=\xd0\xf8\xb1=\xf6?\x06E\x19G\x9f+\xf6?\xa3,\x16\xb9\xa7P\xec?\xba Bx\x19g\xee?\xed\xca\x8b\x81\xb3G\xe1?g\xca\xabJ\x93\xc2\xf0?\xa9`\x89mOC\xfa?\x04:>\x06Z\x81\xf2?\x88\'\x97\xc4b\xa2\xd0?`\xf3\x14\xc9\xa0\x90\xf3?P\xfc-YE\x93\xf0?\xbd7\xd9\\\x0b\xfd\xec?\xce\xbb\xbb\x91\xdf\xb8\xf2?l\xd8{\x98\x96\xfa\xd9?\xe4\xca4_\x19\xf0\xf0?@\xba\xd4*\xd4\xd8\xf7?P\xe0\x01\rSJ\xea?\xadvW3D\x12\xe8?(l\xe4\xbd\xf4\xb2\xf3?|eY\x04\x17T\xd3?\xeaVq\x1c\x04\x0e\xea?5\x08\\Y\x9e\x0b\xe9?\xe6Ot\xc2\x8c\xec\xf2?\xf5?\xe5\x95\xf8\xba])\xe3?\xdeg\xcda\xd2\xa7\xf2?\xd4\xb0V\xed\x11\x1d\xdf?t\xb9\x82\xffw\xae\xc8?n\xf2\x1b.\x92\xa7\xe2?\xce\x8cg[#<\xf1?\xb5\xe5\x92\xfa\xecL\xe2?v-\x83\xf5\xd9\xad\xe9?\xeaV\x8dy\xfb\xc5\xf3?%\x8b_\xd1\xa3\x87\xf4?\x18\x06\xed\xf1e\xeb\xfc?\xae\xa2\xb4Dq\xb0\xd7?b9jn\x95\xdd\xf1?j\xb8J\xcfY\x06\xf6?#s\x86\xaa#\xdf\xe9?\xfaG\x82[\x97\xba\xe9?\xf6\xd8\xab\x0f\xf2b\xfe?\xc8\x1a\xfds\xbf\x8d\xf4?\x00\xd4g\xd7\x9cn\xf5?\xca\xe9\xa1\x9a\xefV\xed?70(\xef\x99C\xfa?\xe2\x986;\r\xdb\xed?\x8a[c\x0e\x12\x00\xf5?NY\x9c-\xc6\x04\xed?\t\x8ce\xe9\xd0N\xe9?\x96(\x92\xb6*\xfb\xec?V\xb5\x02\xfc\xf9\x12\xd8?{\xac=\x03\xcb\xff\xf3?\xac\xfd\xcaJK\x15\xd3?xP\xd7\xa6\xd2\xcd\xd2?\xee\xe7U\xe8\x11z\xf3?\xb6\x9d\xc6~Ou\xf4?\x06\xe2\xd5OA\xab\xf8?0\xf8\x13\x04Z\x1f\xe8?\x11\xa8\xee\xdc\x1b\xf4\xf9?2);(\x04@\xf9?\xf2\xb6&>\x9ar\xec?z\x01x9G\xe2\xf3?\xf9\xc9F\x14\xb7\xdc\xed?\x1c\xe1\x01r\r\x98\xd4?\xdd\xf1\xe3\x8a\xf4M\xeb?\x91\x18\x04\x12\x1e\xec\xec?\xf3O\x97N\xde\xa3\xef?\x08\x8c\xef\xc4\x04l\xdb?\x12\x1a\x18K\xd6U\xf3?m\x03\x12\xf8\xa6\xeb\xf3?p\x98\xb9\x95f=\xe8?8\xcb\xa4\xb1\x90+\xee?X\x9a\x12(\xe8\x82\xf0?*\xcf%b\xaa{\xe2?\xearF]*\n\xed?\x0es\x92/!\x1f\xdc?i\x1a\xccac\x18\xe3?\xdf\xc6\xacnR\x04\xea?\x0c\xb2\x08^\xe0\x1c\xf6?\xc6Vn\x87\xca\x84\xf3?jz-\x89\xd6\x94\xf3?\x85\xd9Q\xf5X\xd8\xe4?\x05\xb0*\x0e1\t\xf3?\x06?.\xac\xc77\xd4?\xbb\x9e\xa1\xe5\xfcK\xee?<\xa7L\x1c\xff\xa1\xf4?I\x11\xb50%G\xf5?5\xbf\x1f\x8e\x81]\xea?\x0e;#\xb4\x10\xd0\xf2?lD0\xad\xabT\xee?y\x15V\x02~\xfd\xe1?\xb0 \xc6\x9b\xb15\xf9?W\xea\x08L\xceq\xef?\xba\x7f`\xf1F*\xf2?\xc9\xf4P\xde\xac\x97\xe2?06>\xbdPU\xf3?\x05\x1b\x99\xe4\xbd\xda\xe7?\xd9\xe7{\xa7e\xe6\xe9?\xb1\xbf\x0c\x81\x1bp\xf5?\x02s/\xb5}\xa4\xe2?\x0f\x92v\xd0Z\x14\xe0?\xdb\'!\x1e\x00\x99\xfb?\xa2\xa7"\xbd\xfc\xdb\xe9?\xc5"7\xaf\xb4\x96\xf4?\xba\x9e-\x9e\x16\xae\xf1?,\x9e\xce\x92\xa4\xde\xf0?r\xcc\xc8\xc13\x83\xdb?`\xa7p+\xbd\x17\xf0?\xf1\x11\x04\xb6\x08\'\xee?T\x91\x1aut\xfa\xe1?NY\x9c-\xc6\x04\xed?\xc6\xc8g7\xaaG\xe7?\xcc\x08`\xfc\x9db\xdb?T\xde\xc2_\xf2\xa5\xee?Q\xca\x04z\xc3\xc8\xe6? \xe4r4\xce#\xf0?\xc2\xe0\xd0wnV\xf6?\x04\xcb\xe5\xf3\xf0<\xee?\xf2\x92}s\xc65\xee?\xf9\x15+\x8f\xf1,\xec?\x995;\xa0\x02`\xe3?\xb0\xae\x10\xa5\x8c\xff\xf4?\xcc\xb1i\xc0\xe6\x88\xf4?\x13c\x06\xb4$&\xe4?t`\xbc\x10\x0c6\xfc?\n\xd1\x12\x10\xdb\xd8\xf9?ch\xcf7RR\xef?~c\xec\xad\x0bw\xf3?n\xad\xbc\x19\xa0\x12\xf1?\xc0\x03\x15\xc9\xc6\xe6\xf1?\\\x88\x8a\xf2\xafS\xcd?ZO\x8d\'\xfaP\xf1?\x96\x90WH\xac\xa8\xfa?i\xf2\x02Mk\x8c\xf2?\xb4\xb95`{\xfe\xf1?+\xa5\xf4\xbc\xbf\xb3\xf3?\xf6\x85bpW\xa0\xef?X\x9d\x80\x0c\x82|\xfc?\x88\xd1\xe7\x9e\xbd\xf5\xf3?7\xcb\x1d\x07\xa2n\xed?.4/O\xf2\xd0\xf8?\xafs\xbdF\xdf\x82\xe7?}\x97\x12\x87\xabK\xe5?\xd4\xf01\xf1\xa3w\xf1?\n:L\x02\x82\xf6\xe6?\xc4+NK\x00f\xec?\xd8\xb2\xff\x81\x97\xca\xf5?>\xe2{-\xcb\xc4\xf2?\xaf\rD\xcc\x99K\xf0?\xe0c;b\x1dq\xe3?J\xe3\x1b\xe9\x8e\xdb\xec?\x1arM\xd5\xc9\x9a\xf8?|9S\x03\n\xaa\xf3?n\r\x04\xb7/\xb7\xe8?\xca\xa1\xdb\\\x9f\xa4\xf1?%\x15\x8b\xdf-}\xf2?\xf9\xca\x7fa\xd2\xce\xe2?\xf9\xe0\xfd\xc0e\xc9\xe8?\x8fZ"\x91C\xc8\xec?\x87\xdb\xcf\x13Q\xb8\xed?\xcer\xbc/kN\xe3?\xd5\xb4\xc1\xacU\x01\xef?\x14\x999L\xaf|\xc5?\xda\xc5,\x8b\xe68\xf3?\xec\x13\x15\x8a\xf0t\xf9?>\xb0\x91\xee\xf2=\xf3?V\xc5\xf1;\xdcR\xf0?\xc2f\x0f\x1a\x9b$\xd1?Q\x0b\xf6\x072K\xf1?H\xbcL\xe6\xca\xa8\xef?\xbc\xe1\xa4\xee\x9e\x8b\xea?>?6\x9b\xc4\xb3\xdf?\x8e/\xe0x\x1f\x1f\xf5?\xc8Cf\x93\xf1\x18\xf3?\t\x8ce\xe9\xd0N\xe9?\xcc\x08`\xfc\x9db\xdb?\x14\xa6a\xac\xad\x0e\xd9?~\xb7\x82w\xa7\xed\xef?\xb8\xefj\xf8&5\xe0?,\x92\xf3\xb5\xce\x8e\xf5?"\xa2^H/z\xf7?m\xe2\x98Q0\x04\xf5?\x1a\xe9.\x96\x11\xf8\xeb?\xcfV\xc6\xda\x1c?\xe4?\xad:\xcct\'\xb7\xea?\x07\rZ\xe3\x8cz\xe4?\x1a=\xed[*B\xf6?`\x83\x92\xc76\x0f\xf0?\xaf\xe9*I\xc5\xf4\xee?\xd0DR\x90\x85Q\xf3?\xfa\xbd\x16\xd860\xe0?\xfe\xe6\xed\xa8E\x95\xf2?T\x90+\'\x06l\xe7?\x10\x14\x0exg\xf9\xf8?\x82}\xf1< 9\xe9?\xf4H\xea\xda\xe0R\xdc?\xd4^\xb0`}\xc2\xda?2\x8e,\x1f\x02\xd7\xf4?\xdcP\xa3\xcb\xa5\xa7\xce?\xbca\x03eo\xc5\xef?6\'\xcbd >\xfa?\xe0\x12\x99\x06\xd8A\xf5?\xca8=(\xa4O\xe9?\xc0\x97\xc7\xb1H\xe4\xe6?AI7Ux\xa1\xe7?$ \x9c\xcc\x88\xe2\xf8?\xd5\x8fI \xcf\x8c\xeb?\r\x16\xcaS\xa5(\xe9?\x8a\x80\x0f\x04\xd1\xa2\xf0?\xfa\rtZ\x02=\xfe?h\x1d`\xe2.m\xde?\x84\xcc \xdf%\xf3\xee?P\x19\x92\xf2W\xc3\xf2?\xac\xbdn\xe6\xb9\x86\xf8?>`\x90\xdf\xd5G\xf1?d\n\xe7\x07W\x97\xdd?\x17\x06\x954\x8b\xb5\xf4?\\iv\xa1\xbai\xf3?\xe8\xa4\x83\xf7\xd5\xc7\xd5?$yV\x9cZ\xbd\xc8?G\xe5\xee\x97ht\xf0?\x9a\xbdQC=F\xf7?\xae\x83\x8a\xc4\xee}\xf2?5\x8c\xaf\x87,\x11\xe9?\xba\xf3\xf2\xd3l+\xe7?\xa45\xed\x9b\x1b\xe9\xdf?\x00\x8d\xf0\x06\xdc\x12\xf3?\xa85\x19\xc8V \xf9?6-\xec~a\xfe\xf1?\x0eT\x07\x0e@\x8f\xde?\x86ja\x9c0\x17\xf2?.\x8c)\xae\xaa\xe3\xe8?Is\xacxI7\xf5?\xce\x0e\xcd\xd99\x83\xfc?_\x11\x97Zt\xf5\xed?\xb2l\xdd\x03)\x9b\xdc?F\x99px\xa8\x02\xf2?0\x84d{\x869\xf8?\x96(\x92\xb6*\xfb\xec?T\xde\xc2_\xf2\xa5\xee?~\xb7\x82w\xa7\xed\xef?L\xf7G\xfe}\x81\xf4?\xb7\x05\xfb\x88}\x81\xf0?R!6\x1ci\xa5\xf8?\xb2\x8aT\x8b\x15\x87\xfc?\xb8\xb3\xb4\xef6\x92\xd0?v\xc8Ux\xf2~\xf9?o\xa1w\x0fU\xcd\xf4?\xd6\xab\x93\x9e1X\xf1?\xd6\x9e\x9a\x91nU\xdf?x\x11e\xcf\x18@\xf5?p\x16T\xf3(\x9d\xe6?\xf4\xa5\xb6R\x14\xe1\xda?\x8cI*\x0e\xb6c\xf7?dF\x82\xad\x1a\x86\xdf?\xfe\'f{Lm\xf9?\xb8v{\xf8\x1b\xfe\xce?\x04\xea\xa9CB\x18\xf1?\xd4~>%\x860\xe8?0{\xd0{\x19\xf9\xf5?\xf8\xa5\xee\x8cw\x0b\xf5?\xe0\xaa\xd2\xc4\x12D\xc8?\xa6\xf6g0\x1f\xd0\xf5?\x86\xe7\x82\x90\x9e\xd2\xf3?\x80d8U\x18\x8c\xee?\xa2\xa0\xdd\xa9QT\xeb?\x1d\xfas\xe4A\xff\xe5?\x845\x83\xe2\x11.\xf9?\xf4,+\xa0>\x1c\xe7?|G\xe3\xf0UF\xe5?\xcf\xd22x\xbe\xfb\xf1?l\xacC\xe1\x17\xc9\xd5?\x02\xa4\x9b3\x1e\xf2\xf6?\xf7I\xfc5\x95U\xed?\xa6\xb5i\x05\x9b\x18\xe0?0\x8b:\xec+\xe5\xf2?*`\xb6\xd5\xa7\xac\xf8?\xcd(\xa5}\xe8\xf3\xec?u/7\x80\xae\xff\xea?\xa2#\x110\x9cC\xfd?I\xfd\xba\xed\x84\xd2\xe4?$\x14@I&\xc2\xf5?\xe3}\x97P\xef\x90\xfb?\xdc\x106SD*\xef?$Uo\xe0\xcc\xbc\xf7?;\x0f(\xaf\xb3\xb9\xf6?\xebx\xfdA\xb8\xbe\xf7?\x06\xee"\xb9\\\xd7\xd8?\xd2\x86\x88\xb2\x81\x1c\xef?\xcc\x13T\x00/E\xf0?\xab\x9c#\xe1r\xd7\xe7?*\xab7\xb6Q\x03\xf2?8amCUj\xf2?\x9e\xaa\xa3I\x1b\x01\xf3?\xb9\xbek\xe8wC\xf3?\xfb\xcd\xc2\xe7\xc5=\xef?\x88\xf9\xd9\x14\xcdd\xf2?\xcb9\xd8\xc9k\xf6\xe2?\x92[[\x92(\'\xe0?\x82\x95\x8eA\xb0\x1d\xfc?\x12f{\xcfW\xbd\xec?\xc0\x8a&\x83-,\xf4?V\xb5\x02\xfc\xf9\x12\xd8?Q\xca\x04z\xc3\xc8\xe6?\xb8\xefj\xf8&5\xe0?\xb7\x05\xfb\x88}\x81\xf0?0G\x005\xf56\xc8?g\xd8\x8c\xf0X\xc0\xe2?\xf0\xc4\x82\xca\xbe\xfc\xf5?\x0b\x970\x97\xabu\xe5?\x0b\x84\x1f`?\xc2\xf8?\x8c3\x0c\xef\x06G\xfc?W6\x92$e\xbb\xf5?\x18\xd9\x0e\xd5\xa7\xa4\xd7?\x8eQ3D\x1b\xd8\xf1?\x131\xf6\x0e\x87\x99\xed?\x7fj"\xa1`\xe1\xe1?(v\xee\x81CC\xcc?\xcf3h1\xbd\xd1\xed?[\x83[\xee\x8d\xde\xe2?\x9c%\x87\xce\x84\x01\xf0?BZ\xf9j\xc4\xf8\xec?\xc4e5\xc6\xd2\xdd\xf3?l\x9aW]\x846\xf2?\xed\x08\xfbK\xf8\x9e\xf9?\xf0}\x82U\xc58\xee?\xbami{\xc5\xdd\xf2?\x0e\xd2\xa4(\x8a\xcb\xf8?\xc8h\xf1\x031G\xd8?J\x03B\xda\xd9\x0e\xf5?\x91\x96\xfaz\x9b\x81\xf8?\xb6a\x1f\x1d~d\xe4?\xc3*\xbc\x1bk|\xef?\x7f\nc\x19\xa4\xcd\xf0?\x89\xaf\xfc\xdf\xd2J\xef?\x84\xd5\xc2$1\x86\xfa?\x16/\x87\x91y\xcf\xf2?\x1ch\x11p\xa3\t\xf3?=n\xfb\xa1\xe8n\xf1?\xfa\x15\x9a3\xc0\xb2\xf6?\xc4\xe6nq(K\xea?VJ\x9a\xc3\xb2!\xfe?~[\xa1\x0f\x9d\x99\xf4?_\xe6\xbb\xe2S\xc2\xe1?JX\xb9\x92dN\xf0?\xd3\xfdP]N\xeb\xe3?v\x86L\xcb2l\xf0?\xc8\x08\x85\x83\xea\xfd\xfe?[\xa0G\xe0E\xe2\xf0?4\xb7\xa2\xc72C\xf9?\x9a\x00.av-\xfb?\x10JD\xc3}\xe1\xf5?t\x12\xf3\xdc\x9b\xac\xf5?>d\xa0x\x15\x8f\xdb?\x19\xaa\t\x83\\\x10\xf3?\x06T!\xb8!\xb6\xf1?\x1a\x7f\x8c\xc1T\x81\xf2?+\x9e(\xd6\x9f:\xf4?\xecT\xcf\xee\xb2\xb2\xfa?\xb9\xd5\xe5\xdc_r\xe6?\xe4\x1f\x9d\xd5\xa3\x02\xf7?\xbe\xedf@\xe3\n\xf6?\x1b\xe2\xf9=\xca&\xe8?"\x1dN\x04\xa1,\xf0?X\x07\x95_\xe1\xc8\xf0?\x9e\xe1q}\xe9\xff\xf3?{\xac=\x03\xcb\xff\xf3? \xe4r4\xce#\xf0?,\x92\xf3\xb5\xce\x8e\xf5?R!6\x1ci\xa5\xf8?g\xd8\x8c\xf0X\xc0\xe2?L\xe5\xf2\x86\xf0y\xd0?\xdbr\xbcF\xc9(\xf1?\xfc\x95-\x99\x04]\xf3?H\xfc\xce\xeb\x1b\xce\xef?\xaa\xb0\xfcJ\x91\x90\xf8?\x8f\x94\x17\x1a\x13\xac\xea?Y\xec\xff\xe2\x89\xd6\xe5?\xd0G\xcen\r+\xf1?~2s\xbd\x9c\xf8\xe9?\xc5\x97\xff:^p\xe0?\xf0\xe9&\xe18K\xfa?\n\xfc\xe9K\xd0\x93\xf1?\xa6\xdb\x88=\xdb\xed\xf2?\x96\xce\x05\xde\x92R\xf8?zX\xc8sk\xb9\xe3?\xba)\xec08\xa4\xf7?\xb8Dv1\x17\xbb\xf7?\xd8\xbc\x0c\xb87e\xeb?\xf2taj\x8c\xef\xfa?,\xccb\x02\xe1\xb9\xdb?m\xa8\x92Z \x1f\xed?\xbe\xfd\xc0\x1d\xb0\xdc\xe1?\x12\x99\xd9\x0f\x93\'\xf0?\xe5\xa0Z\x94\xd8\xc6\xeb?\xd2m\xa4\x1d\x81\xb8\xf6?\x92\x99\x8e7\xa2\xf2\xe8?O\x99\xd9/L\\\xf7?\x93\xe5E\x8a \x1b\xe6?\xcc\xbf/\x94\xbd\x8b\xce?\x9e\xd9\xd7R\xad\x90\xe1?]+\xdc\xe3\x02H\xf2?\xad\xb3"3\xc8|\xe5?@l\xf6\xbb\xbcV\xf6?\xc9S\xf8\xbavI\xfd?\x10\xf6\xecPqi\xf3?\xc85\xb25j\xe7\xfc?\xc4O\x8ek+\xb4\xcf?\xb8\x90\xecD\xaa7\xf0?\xc85\xae>\'\x00\xbf?5\xf6_\x0eR;\xf2?\xd2\xdaS\x94+\xbb\xf4?+\xb1O\xb7\xb4\xfa\xea?z\xa5}\x80\xc1\x0c\xdf?\xa2P^X\x96\x8b\xf2?M%\xd3\xed\x1cC\xe2?\x86r\xaf\x02\x13~\xdb?/\x9c\xe2\x98(F\xed?\x90\x83\x1a\xb93p\xf0?B\xa3\xdcZ\nw\xf4?\xb6\xa4\xde\xc8sY\xeb?\x1e%\xf0\xbe\x0c\xd0\xf4?<\xbfbI\x12\x17\xea?\xa5`\x8d\x11q\xab\xe2?\xcb\x92%\x1f\xca\xba\xef?\xddr\xd8-[\x9a\xf7?\n\xdc!T\xed\x04\xf0?\xb0\xc8.\x05\x0cI\xf0?\xe2>tC\x05\xf6\xf0?\xed"\xee\xbd\xa1\xc9\xe6?\xac\xfd\xcaJK\x15\xd3?\xc2\xe0\xd0wnV\xf6?"\xa2^H/z\xf7?\xb2\x8aT\x8b\x15\x87\xfc?\xf0\xc4\x82\xca\xbe\xfc\xf5?\xdbr\xbcF\xc9(\xf1?]\xe5\x94\x92\xa3)\xf3?\xcc\x0b\xffWO\xd4\xf6?;\xd3\x92i\x08\x8f\xe9?\x0e\xe3\xca\xa0\xcd\x1c\xe8?\\\xa8\xfcA\\\x81\xf6?\x9c\x1e\x1f\x93\xec\xa6\xcb?[\xf9\xf1\xceL\xa4\xf3?,\xaf\x19ac\xfb\xf9?\xf2zk\x83\x1a\xa4\xfb?\xcf\xe7\x91\x90\xe7\x15\xf7?\xd9\xa5\xf3GG8\xf3?\x06\xe4\xae\x1d\x92v\xfd?\x13\xf0\xbej\xbd\xbc\xf6?n\xaa\x08\x9eF\x88\xeb?\x18i\x94T\x05\xdb\xe6?\xebH\x98\x87\xe3\xb4\xea?`98w0[\xdf?8\x9d`\xfc[1\xf4?\xbcXG\xc7\x01\x15\xf8?\xcej\x7f\x88\xb1\xd4\xf3?\x90\xbd"\xeeow\xc1?/\x0c\x92\x18\xf0U\xf4?}\xd3\x90\x8eZ\xc9\xf3?\xdc\x965L\x86\x88\xf1?/\xb0\xdfT\x86\x08\xe5?\xdc\xf3}\xa0\xe5\xe0\xf4?\xbc\xbc\xb0\xaa\xe5\xc2\xfa?5\x89\xb0\x94\x00<\xe1?\xc0\xe1\xa2\x17\xb6\xbf\xe7?\x84\xb3\xe7(\xc2\xde\xe9?\x80^~\x0b\xa4\x89\xd7?\x8a\x81J\xd9P\x8e\xf0?P\x9a\xe3\xcfC\x8f\xe3?dO\xea\xdd\xc76\xe7?\xdc\xabX@#J\xea?\xcc\xaa\xf0\x8d\x83=\xd5?\xbas\xd8\xfe\x9d\xb9\xf3?\xb0\x04\x8d&\xff\xfc\xf1?\x03\xf5"\xccNE\xec?\xe4\xd3iU\xea\x8c\xef?\x10\xc8\xf4\x82\xb30\xed?\xb8Y\x88/g@\xf0?\x08DEcB\xa6\xd5?n7"\x1c\xd0\xee\xfa?\xe2\xe64\xd4*#\xec?\x80\xd5\xee\xc4\x19\xa5\xea?WM\xc0\xc6e\xef\xf8?\x82\x0f\xa5h\x1d\xe4\xf7?\xac\x88\x12\xb7C\xe2\xea?x\xfaH\xb100\xfb? 3s\x13t\xb2\xf1?r\x168\x8a\xe9?\xea?\xcajR"\x1a\x03\xf5?\xef\x0e\xf2\x93M\xf5\xf4?\xde\xeecS\xb3\xbe\xee?\x11H\x187\xe4f\xfc?\x00\xd6\xb7\x9f\x92\x81\xe5?|y:Z\xc0E\xcd?xP\xd7\xa6\xd2\xcd\xd2?\x04\xcb\xe5\xf3\xf0<\xee?m\xe2\x98Q0\x04\xf5?\xb8\xb3\xb4\xef6\x92\xd0?\x0b\x970\x97\xabu\xe5?\xfc\x95-\x99\x04]\xf3?\xcc\x0b\xffWO\xd4\xf6?6\xbd\x8ai\xfb\x9b\xe4?\x8e\xb0\x83\x8b\xd1X\xf1?X\xdb\xe6\xc1Pf\xd1?\xa4\xf8\xc8W$a\xed?\xa2\xf3\xa2\xf2\xcd\xd8\xf1?$\xb3\xa9\xf8\xe1\x06\xe0?\x18R\xc2\xff\xe8\xa2\xe5?!\xb0;\xacp#\xf2?\xfc\x8b\xa5T&H\xf0?E`\xacg&\x9f\xef?\xf2\xb6\xfe5^>\xe6?\x9c\xee\x17\xda\x9a\xa4\xf6?\x96\xc9\xf7\'7\xb4\xef?\x9d:~b\xb6\xab\xe2?\x9c\x1b;\xd3\x92|\xd3?\x1a\xce\xb3\xba|\x02\xfc?\xf6\xd1\xd1F\x08\x8b\xed?b\x91\xcf8g;\xfb?RZ\xbb\xf3\xec\xe3\xf1?\x7fLv)\x00\xbf\xf3?x\x9b\xb6\xb0y\x14\xe9?\x8a\xf3QP[\xe4\xf2?2{\\]TV\xf8?fL\xf0\r\x1b)\xf1?\x1b\x04_G\x13\x1d\xe8?\xe0[\xd2\xd8\xd0\xfa\xf3?\xea\x93\x8d\xb1^\xca\xfa?\xfe\xf0\xab\xbd\x14+\xe4?i W\xfa?\x96\xe3?\xa83\x86V\x9c\x88\xe7?T\x87\xe8\x0c\t\xf7\xea?\xfb\xef\x8c\x04\xa6\x88\xe9?\x9ct\xf7\x9f\x9a\x11\xd1?\x966\x88\x0f=\xf3\xf8?\xdc\x81\xa5\xfc_\xcd\xf9?\x9b\xd8@\x06\x1bi\xf3?\x84L\xe6O\xf6\xde\xd8?u\xcd\x82<\xe7\xbb\xee?\xafOM\t[\xaf\xf1?\x01\xdcCa\xfa,\xed?\x1b\xa5a\xcc\xe1\xc3\xec?\xb8b\x1c\x13$\xb3\xf7?V\xa5<\x97\xf5\xac\xe2?\x8a*\xdc\xe6\xb0\xab\xf3?\x01\x8c\xdd\x9e\xb1\x99\xfb?\x86\xe69\x87\xc5\x14\xef?\xb8\x16\xc3\x0f\x9fn\xcb?\x82J\xaf\x91w\xee\xf3?(\x1e[2{\xa6\xf4?j*n\xc8"2\xd2?X\x10pQ3\xd7\xe8?T\x90?O\'\x95\xf3?\xa0\x9c^n\x19\xe0\xce?\xf5\xeb9)bm\xf7?\xaf\n \x9fe\x86\xef?\xf6\x82{\x0e\x05\x96\xec?:\x1e"\xc4\x9e\xeb\xf0?\xee\xe7U\xe8\x11z\xf3?\xf2\x92}s\xc65\xee?\x1a\xe9.\x96\x11\xf8\xeb?v\xc8Ux\xf2~\xf9?\x0b\x84\x1f`?\xc2\xf8?H\xfc\xce\xeb\x1b\xce\xef?;\xd3\x92i\x08\x8f\xe9?\x8e\xb0\x83\x8b\xd1X\xf1?(\x8a\xc6^\xfc\x01\xe5?\x93:}yn\xb8\xf1?z\xae\x04\xf1\xf4d\xf7?\xa6\xcfaX\xab\x03\xf4?\x96\x94\x84\x86<\x9e\xf2?\xa6>\xfc;\x17\xe9\xe8?|\x14\xac\x07\x9bk\xf9?\xab\xef.V* \xf0?\xcd\x9d\xbb6"\xe1\xea?"\x1cE\xad\xd3?/\xce|gv\xe0\xf7?\xd4~\x86]\xea\x1f\xea?^\xeb\x88\xb6\xec\xbd\xea?r\x1f\xbc\xceC\x0b\xdd?\xb9k$\x0c\xf4\xb5\xe3?\x1e"\x8d\xfd\x16\x10\xf0?\xb2\xdb\x9e\x19\x1c\xf0\xee?\xb41>\xb9E&\xf2?\xc4>\x90\xcd\xe1i\xe8?&\xa2\x00\xd0v\x83\xed?\xf8#\x84)\x8c\xe1\xd1?\x15!\xa3Tk\x1f\xe2?\x04-G\xc1\x85u\xe8?G\xf4\x8b>\x84\xc7\xef?\xac*v\x98\xf0\xf1\xc0?%u\xf9r\x99\x10\xec?\x9e\x94\x89\x8c&\x08\xf7?\xab\xa8\xc7\xca:\x10\xec?4\xaf\xfd\xc2\x01\xeb\xfa?\xb5ynT\x90G\xf3?r3\x0c\x18\xb5|\xf7?\xa3\xa5\xe9\xb8\xdb\xe0\xf9?\xd0\x14e\xa7d\xcd\xf3?\xea\x00\x8f\xa5\xa6\n\xdf?\x9f?\x13\x07\x07\xab\xe7?\xd2et\xd7\x0eF\xf9?,\x932\x125\x07\xf2?\x9a\x02\x02\xbc\x1e\xac\xf1?\xde\x07\xc6n\xed\x7f\xed?\xb6\x9d\xc6~Ou\xf4?\xf9\x15+\x8f\xf1,\xec?\xcfV\xc6\xda\x1c?\xe4?o\xa1w\x0fU\xcd\xf4?\x8c3\x0c\xef\x06G\xfc?\xaa\xb0\xfcJ\x91\x90\xf8?\x0e\xe3\xca\xa0\xcd\x1c\xe8?X\xdb\xe6\xc1Pf\xd1?\x93:}yn\xb8\xf1?`\xb9-\xab<\x06\xa3?' +p13 +tp14 +b. \ No newline at end of file diff --git a/python/test/testdata/test_searcher_data.pkl b/python/test/testdata/test_searcher_data.pkl new file mode 100644 index 0000000..55c58ce --- /dev/null +++ b/python/test/testdata/test_searcher_data.pkl @@ -0,0 +1,27 @@ +cnumpy.core.multiarray +_reconstruct +p1 +(cnumpy +ndarray +p2 +(I0 +tS'b' +tRp3 +(I1 +(I100 +I8 +tcnumpy +dtype +p4 +(S'f8' +I0 +I1 +tRp5 +(I3 +S'<' +NNNI-1 +I-1 +I0 +tbI00 +S'r y\xba\xb3\xb6\x02@\xaf\xa5Lb\t\xf9\x11@\x9d\xf7]\x17a\x13\x08\xc0+\xfd\xdb\x89\xacU\x1e@\x8a?I\xa3\xba\xc7\x18\xc0\xc4\xdb\xa0u\xa0\n\xeb?9\xe5\xf6\xf3W&\x11@\xb7\xf73Y\x00{ \xc0\xfeXK\x08R\xb5\x14\xc0\xbe\x0b2\x08\xb7\xd5\x1f\xc0\x99 r\x02\x83S!@\xedQ\x00\xca\xc1\x81\n\xc0\xf4D\x7fb\x94\x04\xf7?\x029\xff\x0b85\xf4\xbf\xd4\x0b\xe1\x07\xd7S#\xc0\xdc\xb2Y\xbc\xa27\xcb?\x0e\x16H\x18a\xc2\x06@\x1b\xc7\x91,i\x11\x10\xc0yw@\xe0\x0e\x04\x07@\xb7\xae\xcd>\x9c\xf7\x18\xc0j4\xe4\xf8AM\x11\xc0\xd4\xde\xd1\xdftB\x00\xc0\xe5\xc4B\x80\xd1@\x00@\xb0\xf8\x18\x90|L\n\xc0*\x14\xb7\xea\x01\x02\x1f\xc0U\xf4\x13\x97\x9aS\x1b\xc0\x16\x8e!\x11\x98\x81\r\xc0}\x97\x17\x1c\xa1\x82$\xc0U\x9a\xf6{\xda\xaf\x13@s7\xa4\xd9z\xbc\x17\xc0\x039\xacy\xf2\xac\x05\xc0\x07\x8f\x19\xd9\xeb\r\xf9\xbf<\x90Z\xab\x97\xac\x17@\xc6\nh\xe1\x13n\x02\xc0\xb2!\xcd\xf51\xe0\x18@\x06\x03\xfa\xb3\x86:\x17@P\x0b"\x11\x98\xbe\xe1\xbf\xb8\xa8\xbd7\x88\xa2\x14\xc0\xb3\xea#*\x14\x8b\t@=g\xca\xa2\xd8\x95\x12\xc0\x98{\xf6Fp\x00\x18\xc0F8my\x0e\xae%\xc0\xf2@\x8c\x07\xb8\x9e\xf7?\xce\x82`B\xfe\x8d\x1d\xc0NF\xf1\xcb\xc1\xf5\t\xc08N%"M\x84\x02\xc01\xd1`\x8e\xb9\x1a\xc0w\x9cn\x19\x90\xea\x1f\xc0\xf8^\xe8:&\xa8 \xc0\xc9\xf6\xb8c\xe4\xc4\x12@\x0f\xe1\xe8\xe0T\x0b$\xc0\x8d\xd3\xf2\xf1\xe53\x1d@\xde\x031\xbf\xc7Y\x0e@\xc3\x03\xb3\xf2;F\x1e\xc0\x08\xee\xae\xa4{\xcc\xf3\xbf\x88"\x86&`\x07\x15\xc0\xa4c\xdf\xbe+\xdf\xc5?Sl^\xea\x81\x17*@\xe7\xbfr\x12\xfd\x89\x1d\xc0+A\xe07\x15\x07\x11\xc0F\x82\x95\xcfI\xac\x1a@\xe3\x86\xc6\x01\xd0p\x17@\x15\xc4\xc9}\xda+\x1b@"U\xa0Da\x88\x15@\xce\x1e!\x9b\xaf)\xfa\xbf\xd0h$\xcc\xba\x14\x10\xc0(\x12j50\xc2\x13@\xb7\xee\xbb\xf19\xbc\x15\xc0D\x1e!\xebC\xc9\'\xc0xG 7A\xe2\x16@\xd4\xbd>C\x05$\x18\xc08\xc0u\x9f:\xb3\xe2\xbf~\xa53dB\xa9 @\x00@\x8e\x90\xb7+\xa3\xbf\x86\xb1\xc1\x1f\xac\x92\x01\xc0\xce\xd3O6\xba\xf0\xf2?\xda\x1e&?=;\x14\xc0v\xb9\xec\xdeg\x02#@hD\x9e\xe51\xe7\xeb\xbf0\xec\xf4\t\x19=\x1a@\xdcXOx"Y\xe2\xbf@\xde\xb8O\x97\x18\xe2\xbfG\xb1\x81\x12\x15\xcb\x12\xc0\xec\xac\x0f\x1e\xc1\xd2\x0b\xc0\xaew\xf8\xfe,\x1d\x10@\xe4A5\x90h$\xe4\xbf\n\xa3:\xfa\x85\xdc\x18\xc0=vC\xf6\xe3a!@~\x9bTQ\xf5\x0b\x0b@@H\x92\xbe\xab\x90!@_\x0f]\x88\xf0\xc6\x12\xc0\x8c-B\xf5\xfc\xba\xff\xbfD\xd5\x02y"\x02\r\xc0\xf4YD\xbd\x89\xba\x16@\xe8EF\xcb\x1d\xe4\xe5\xbf\xd5\xae\xb6\xd5\xc1Z\x1e@\xc4\xcf\xe7ad\x96 \xc04\xa9\x8c\xef\xd8M\xf6\xbf\xe4\xc9\xddr|\xb1\x11\xc0o\x06Q\xce\xf3x\x1b@\x15\x84]\xfdL\x1a\x16\xc0\xf5r\xe9\xef\xbd&\x11@\xf92\x07 \x92:\x12\xc0~\x0e(\xc1\xe9\xbd\xfb\xbf\x10Re&"h\xd3?\xa1\xf7\'\xe0\xab|\xf4\xbf\tq&|)\x05\x19@\xccL"\x87F\xe6\x18@\xb6H\xf8\xcc\x15\xb6\x1b\xc0p\xfc}\x80\xdb\x19\x14@\x12\xab\xe0`\x19/\xf8?z8\xd9H\xfe\xae"@\xdd\xda\xf1\xb1\xfb\xa9\x04@\x106\xecq\xbbb\x01\xc0.\xdfi\xf6\x06\x9a\x00\xc0\x80\xe6\xe8tu\xba\x16@\xfb\x7f\xab\xcbb\x1a\x1f@\xde&\xb6\xfa\x90\xe1\x19@\xcct/Ko\x1d\xfd?\xe7\x9aFd\xcbS\t@l#\x89T\x0f}%\xc0\x93Z\xe8\x92\xe3\\\x12\xc0\\\xa1\xc0\xb6\x9bU\xef\xbf\xe8\xfd\x855\xd6p\x0e@ \xac\xddgw\xdd\xf4?p\xfd\xe0\xd8\xaeE*@\xb4\x03\xde\xff^?\x12\xc0\x92D\x96\x1c\xfa<\x0f@!D(\x16\xd0\x1d$@\xeaM\xf3\xc6+S\xfe?Lp\x0b\xe3V% \xc0F\x8f_)\xf6\xec\x00\xc0\x18\x83t\xef\xa1\xf3\xff?\xf0\xf4M\xce\xb4H\xfb\xbf\xec\x12w\xdb!\xcc"@h\xa3\x9a{w\xd7\x1f@\x12\x94\xf4a\xf8\x94\xfd\xbf\xe85\xd0\x10\x88\xbc\x10\xc00\xfd\xbc\xdf\xb3\x8c$\xc0\xbeTF\xe9\xcb\x9e\x0e\xc00\x8c44Q\xf2\xdc\xbf`\x03\x98\xd5\xcb\xc4\xfc\xbf\xd9\x0c\x8b|&\xf7\x1b@T\xb6\xa9\x7fKT\x0f\xc0|D\xf0\x12\x9e\xfb\xf2?K\x1aD\x83\xdd\xf3\x19@k\xa8\x8f!*!%\xc0\x8e+\xf0[V\x85"@\xc1\xc1\xe5\x10\x85\xbd\x16@0\x0c\x15b:/\xe1?\xce"Zis\x95\x05@\xa0\xa0\xd4\xf3\x1e\x13\xde\xbfV\x0f_\xea\xfe\x07\xe8\xbf \xa6\xe1\x1aX\x96\xbb?\x07\n\xc4\xec\x84E\x1a\xc0`IG1\xb4\xaa\xcf?\x0f\xb9\xa2\x85\x9c\x9f\x1e\xc0\x10#V\x04>\x04\n\xc0\x82*p\xba\x07\xe4\x0f\xc0\xf8!\tK\xff\xf8%@\xb2\xbd\x8e\xff\x02c\x0b@\x90@\\l\xcf]\x08\xc0\xd3\xb6\xbd\x10\xe0\xd8\'@\x84\x9c\xf67jN\x16@\xcc<(\xdf\r9&@>*I\xaa"\x06\x01@\x8a\xf0%\x84\xb5F\'\xc0FD\xedjHO\x06@\x89)\x8a\xd8 \x17 \xc0D\x12\xa0m\x16\x97\x18\xc0\xb8\xd1\x82\x82s\\\xd1?\x08\xc4\x86`\xe1\xe9 @0e!\x19\xa85 \xc0\x00\x15\x9b\'\xb9\xc5\xf5\xbf\x00\xfd\xec\x8eL\xcb\x01@\xa0zPT`V\xce\xbf\xa2\xcf9\x9b\xc6\xb2\xf4?\x10\xc24\x8eZ5\xfc\xbf4|U\x9e\x13\xe2!\xc0\xb4\xb8O]75\x15\xc07\x03K\xab&y%@\xe0\xb1\x02\xa0\n\x87\xf5?{\xe1(\xdf"P\x01@i\xe63m\x9b\xaf\x06@s \xe0\x93\xbc\x94%\xc0\x8dgM\x826\xd8\x16\xc0\x88\'Ri\xe4A#\xc0l\xe2\xde\xf4J_\x00\xc0\xaa\t\x13p\xdaW\x1f@\xb0-+@ \xc2\x03\xc0k\x86`\x06I\xd1!@\x9d?\x05\'\r\xa1\xf6\xbf\x84\xc8\xf7\xc1\x8c=&@j\x85%x\xdf\xf9\xfc?wt1\x00w~\x16\xc0\xf3\x83[\xfcv\xaf\x1a\xc0\xc25U[i\xf7\x07@pC\xe8\x9a\x94\x85\x11\xc0oJ\x90\xcd\x13)\xe8\xbf\xa8\x9e[A#\x08"@\x1e\x15\x94\xf4\x16\x18\x10@\x9d(\x8dK6\xc5\x02\xc0f\x82S`\xc1k&@K\x14\xae\x90\xe2\xed\x12\xc0\xcc\x04\xa4\xa1-\xca"@D\x91Y\x90\xf7\xe1\xe0\xbf\xaa\xc2\x19$\x84R$\xc0\xb2jh\xbe\x82\xf0\x05\xc0\xde\xb6\r=\'\xa9\xd4\xbfs\x94\xd6\xaa\x99\x11\xea?\xc38hP\xbd\xc1\x1a\xc0\xbe\x1a\x7f_:v!\xc0`\xef\x18:,*&\xc0\x00 \xc8\xfd~\xb0\x1b\xc0\xd6\x7f\xa5\x18)\xe5 \xc0T1qj\xec.\xf4\xbfb\x1b\x12\x16\xfb\' \xc0\x19(\xeaf\xa6\x8f\x1b@ 5,\x1ds\xfe\x10\xc0\x05\x0b6\xe7\xd9\xad\x16@\xca\xb5{T"\'\x13@\x98\xb4h\x99\n\x9c$\xc0UG][f\xb6\x19@\xad7g\xb82_\xf7?\xda\r\xdb\x91{\xf6\xfd\xbf\xb0\xe4\x88U9\x8b\x1b@~\x0f\'sP\xcd\x13@\xaf\x97\x1daM\xf0\x1a\xc0\xae+\x01I)1!@\x80\xeb&\x80\x1e?\xea\xbf+(,\xa82f\t@\xe6\xd0\x0f\x1a9a\x1f@\xd8\xb2\x0b\xd7\xecx\x1f@L\x0c\xfe\xf2+Z\x1e\xc0\x86\xeb\\\xc1\xd6\xf5\x1b@M\xeciM\xf6\xcf\x04\xc0\x00B\x86\xf3\x0co\xee?h{~\xa2\x1e\x1b%@@oVF*E\xc4\xbf\xdfS\x02\xefV\r\x12\xc0r\tm\xc3\x0e\xfc\x1d@\x94\xc9\xa0\xfe\xdd\x9e\x1d@\x1a6\xc3\xce\x97"\x1b@F\xaa\xa2W\xedN\x10\xc0\xff\x9cssj\x1a\x1c\xc0@\xaf\xe9\x02\xf2\xa2\x16\xc0T\x06\x11\xaa7W\x17@\xe0%>\x9f\xf4<\r\xc0\x9e\xbe\x08\x1dJA\x18\xc0,\xe9\xa5\x00\xb5n\xed?\x00=\x11%~.\x1a@b\xc8\x0b\xec\xc6\xa8$\xc0>\x0e\xc0y|\xd2\x1e@\x90\xad\x1aH\x1d\xc1\xdd?\xb4\x97X\xc1O\x91\x11\xc0\xe4\x13^\x94\xf6\x9b\x0c@\xf0\x05W\x870p"\xc0$\';\x84\xf9i\xfd?\xa4V\xd3x\xaa\x0f @\xa8\x9c\xda\xb1\x99\'\xe8\xbf\xe0\xac|\xac\xdb8\xfd?\x00e\x861\x15\x96\xf4?\x12 \x04\x05\xdf@\xfd\xbfK\xa3c\x1b\xa33\x13\xc0\xef\xff\xe3\xc3)\x10\x1f@\xee\x0b\xfe\xe0\xef<\x1a@\xbe\xfd\xd6\x11l\x18\x14@\xd3\xa8r0\xa5\xc8#\xc0\x9a\xc9\xb8\x9fR\n\x01@b&>S\r\xff\x19\xc0\xba\xc0t\xad\x7f\x05 @\xd2\xfd\xd4\xc8\x96i\x18\xc014uT\xa2?\x07@xJ\x05R\xeff\x16@\x04Tv\xce\xbd\xad\xf6?\xce\x9d9\xf8\xfdq!@\x90\xb8\'\x820\x9c\xf1?\xc8\x80H\xa8\xdc\xb2\x04@\xdcb\x99\xafYO!\xc0\x8c}S\xb6\xb8\x85\xf3\xbf\x80\xb5\xe0\x88\n1\x04\xc0\x9daE\x96R&\x1a\xc0\x80J\xcd\xe6V\xa5\xf4\xbf\xf7\xb3] \x87\xa7\x17@\xc1\xc1\xe5\x10\x85\xbd\x16@0\x0c\x15b:/\xe1?\xce"Zis\x95\x05@\xa0\xa0\xd4\xf3\x1e\x13\xde\xbf\xc7\x7f\x99\xa4\xb6\x98\x1d\xc0\x84\x92f\xc7\x0b\xde\xfe?\xf6\x11"g\x1cr\x11\xc0*bC\xd2\xf58\x01\xc0\xdf\xb3ZW.\x13\x17@B;{R\xd7T\x12@\xb9\x95\x8aFc\xb7\x18\xc0N\xd1\xa4\xe2&\x8a\x07\xc0\xf6\x19\xa5\r9t\xf1\xbf\xef\x1600\xbd\xd4\xea?H0\xcc\x80\xbd\xc1 \xc0\xd6\x9e\x03\xd9\xa8i\x16\xc0 k\xa8 -4\xcc?\x84E(U\xa4\xe0\x1b\xc0\xa2\xe4\xa9\x8a\xfd\xbb#@s\xd7\xcdx\xb7R\x13@8"n\xf1F\xa7\x0f@\xe3\xe4bH[)\x12\xc0\xc0@\x8e\x01Y\x8b\xcc?O2\xc6A\xdf3\x03@X\xef\xf0\x0fv\x0f\x1b\xc0\x001\x00dg4\xc0?\xd4\xf8\x8c\x97dH\x04\xc0p\xeb\x1385H\xee\xbf\x03o1\x1aY\xc5\xfc?\r$^Qb\x1c\x0f@0j@\xc4=\xf5!\xc0\xb8\x00?\xb0=\x03\n\xc0t6\x04\xd6\xc0/)\xc0(\x08R\x8en^\x14@B\xb7\xf7K\xde\xa5#\xc0\x0e\x81\x9d\xb4\xca\x96\x02\xc0\xf3\x1dU^\xfb\xe5\x1d\xc0\xa1\x80\'\xbc`\x82\x13\xc0\x90@\x1c^,\x0c\x05@\x04>\xab\xdf!F"\xc0\x9f\xc0\xe8\xb2\xcfV\x12\xc0@\xc8\xae\x9fa(\x1e\x8c\xcb?R\xee\xc3\t\xdb\x0e\x16@\xbb\xcct\x06eC\x11\xc0U\x9a\xf6{\xda\xaf\x13@s7\xa4\xd9z\xbc\x17\xc0\x039\xacy\xf2\xac\x05\xc0\x07\x8f\x19\xd9\xeb\r\xf9\xbf\x18\x93\xb8w7\x1a\x0b@yrU\x01u\xc5\x1d@\x7f\xfd\xe0\xe3uS"@\x8d\xa2\x12\xd2\xf3\xed\x07\xc0?*\xaa\x03\xba\xe3#@!\x82k\x89$\x81\x16\xc0\x8c\x02\x83\x8b\x92y!\xc0L\xef\xfc_5\xc6\xb5\xbf\xa8\xf7\x8b)\xd6\xa8\x03\xc0\xe1\xe9e\xba~\xd2\x03@\xd6w\xc8\x13\x14[\t\xc0}\xfa{y\xa8w\x1d\xc0\xe0\xac|\xac\xdb8\xfd?\x00e\x861\x15\x96\xf4?\x12 \x04\x05\xdf@\xfd\xbfK\xa3c\x1b\xa33\x13\xc0 ^\x8e\x98B\x98\xc6\xbf\x90\xb1\x9e\xf3Ku\x0b@\x02,@\xb6|\xa2\x13@\xfa\x04&$\xf8\x80"@G\xf3\xd2\xd9\x8e\xd0!\xc0\x00\xea^H\x8d\xf8\x0c@A\xf2\xeei\x89\xca\x05@5\x07\xc7\xf0h\xa3\x1a@\xe3\xe5\xf5\x9e\xf3\x13\x0e@fcI\xf0\x943\x1e\xc05\xd8-]\x15\xbd"@\x9ctn\xe0\x1e@\xf8\xbfci\xec\x08)j\x18\xc0\x8b\x1f\xfe=\xd3\xa5 \xc0\xc2\xfe\xc0k\x11\xc7\x15@7\xfa\xb1\xe4\x18o\x11@O\xe8\xa1\xca\xdc\x1e#\xc0\x04\x17\x96x\x97\x83\xf0\xbf\xfc\xe2I:\xd1\xf1\x14@\xe9\xb1\x9b\xe4\x0b\xce\x18@&\x7fSk\x7f\xec\x12\xc0\x90\xce\x1e\x0f\x1ew\x06@\xd0%\xb5\x16\xf9o!@\xe0w\x04;\xcd\xd9\x1a@\x9e\x11ZD\x1b\xb9\x1e@\x00\x86\xd6\xd1%\xb1\x1f\xc0(\x1d\xebh\x0e{\x08@\x07j\x9e8\n\xa2\x15\xc0F\x82\x95\xcfI\xac\x1a@\xe3\x86\xc6\x01\xd0p\x17@\x15\xc4\xc9}\xda+\x1b@"U\xa0Da\x88\x15@\x08\xc76/S\xea\xff\xbf\r\x96\xb2\xd3\xd7\x94\x1b\xc0h\xef\x02\x1e-|\x13\xc0\xcew\xe3h\xb1\xc8\xe6\xbf\xdf\x858cw\x1f\xfe\xbf`\xfc\xef$^\xe1&@\xc9\x02\xd0H\x99g\x0b\xc0RG \x80\x16e\x0b@d\x02\xea\x1b02\x16\xc0!\xcal\xe9R#\x1c@\xe3E%\x89\xe3\xe9 @\x17\xdd\xed\x15\xd3\'\x13\xc0\x13\xfb\x8a\xac\xd6\x95!\xc0\xdc\xd7\\\xfc\xbb\xe2"@\xb1\xc0\x08Z\x19\xdf\x17@\x14\xf8\xe6\x9e\x1d\xfa\x13@Bh`C\x03\x03\xf8\xbf\x98\xf8\nN\xe86 \xc0\xea\xc4\xc0e\x8f\xa0\x14@\x8f\xe3/0[\t\x15\xc0jz\xf8L\x8f\xb1#@p[9},b\x10@\xa8\x91r\xd6h\xf2\xe2?\xf4.c\x96\xb1T\xf8\xbf\x11\x08\xb50+\xd4\x10\xc0\x00\xac\xd6\xad\x9b\xcf\xfe?\x84\xccW\xe8tn\t@P\x05\x05\x87S\xab\xde?\xe9/\x81\x98\x864\x1c\xc0\x9f\xba\xf1\x0fU\xd7\x11@h\xe3\xaa[\xb6\x8c\x1f@+\xec\x18\x118\xe2\x00\xc0\xfc\xf5\x0cm\xcc\xc0\x18\xc0H%A\xfb\xf9\x8b\x18@\xa2SI\xa1g\xfd\x19@l\r\x9aC\xe8\xc0%\xc0_!\x80dA\xd8\xf9\xbf\x80k}\xe8\x04B\x17@\xaa|\xa0\xdf\xf6A$\xc0\xd4V\x96\x8d\x15\xad\xfe?1\xc7\x84uA1\x01@\x82X\x89\xbeO\xbe\xe2\xbf6\x80\x9aG\xf1\'#@\xda\n\xd8\x18\x9e\xa5!@\xecl_V \xd6!\xc0\x88l\x89Py7\x0f\xc0\xba5\x9fE\xf0s\x0f\xc0\x18L\xdb,a\x10!@\xd7>P\x1d\xda\xb3\x1c\xc0Z\x04\x83\x85 \x94\x05@-\xda\xc2\x14\x0c\xbc\x03\xc0\x9by\xee\xc3\xe5~\x16@\xdf\xe0:=\x02X\x11\xc0\xc0\x0f\xc1\n\xfd\x80\x08\xc0HU\xb4E\x19v\xfa?&K\xa3\xb6\xd2\x9d,@\xb6\xec\xdd\xa4gF"\xc0\x90F\xeb\xe4,W\xf9\xbf\xf6k\x9b\xe1\x8b]\xf6?\x16\x9aM\xdct\xce\xeb\xbfLF\xb6\xb3V)\x16@\x86\r\xa6\xd6\x94Q\x10@ygjo\xa8\x90\x07@9\x99\xce]\xc0?!@\xdd\xda\xf1\xb1\xfb\xa9\x04@\x106\xecq\xbbb\x01\xc0.\xdfi\xf6\x06\x9a\x00\xc0\x80\xe6\xe8tu\xba\x16@?b.A\x9aR\'@\xb1uw\x84\xa8\xc3\x07\xc0l0\x8f\xeb\xc3m$\xc0\xbf\xe3*\xbf\xcc\xc8\x15@\xc7\x7f\x99\xa4\xb6\x98\x1d\xc0\x84\x92f\xc7\x0b\xde\xfe?\xf6\x11"g\x1cr\x11\xc0*bC\xd2\xf58\x01\xc08gK\xeb\x95\x03\xe6?\xa2\xf1\x1c\xa2?\xaa\xf4\xbf\xd0\xae\xdb9\x0c\xf6\x0e\xc0\xa6\xe1\x0c\xd8\x9cS\x06\xc0\xd8\x92p\x90\xbdZ\x0e@\xc6\xc109\xe7\xfb\x04\xc0\xb8\x8c\xac\xbd\xf5y\t@=\xea\x9f\xda\xb3/\x08@\xaew\xf8\xfe,\x1d\x10@\xe4A5\x90h$\xe4\xbf\n\xa3:\xfa\x85\xdc\x18\xc0=vC\xf6\xe3a!@LBQ\x8a\xd1\xb5\x12\xc0\x88/e\x7f/\x8b"\xc0\xa2c(\x11\xf7\xf9\x13\xc0o\xb1C\xd1\rX"\xc0\x90\xa2Porn\x12@D\x05\xfa\xb47\x80(@\x82\x9b[m\xc4\x87\x15\xc0\xbf\xa3L\xd0\xcd0\x16\xc0.4\xfe\x9cS\x9b\x04@\xaf\xd9V\xcb]$\x0c@\xc04\x9f\x88\x9f\xfc\x02@o\xeb\xca^\xd8\xab\x19\xc0l\xea\xf2\xf3ov\xf4\xbf(\x8ay\x9dV\xac\xf7\xbf(36\x1cb\xef\xfb\xbfW\x01\xe9\n*\xd9$@\xe0\x9f~?g\xdc\xc2?<\xc0/Y\x08\x08\x11@\x06\x15pO\xbej"@\xb6\xc4\xbc_\x1c: @\xe4\x0e6\xd8b?\x12@\xb8\x1ce\x1c\xb1\xd3\x16@4\x0b5\xc8\x05\x9e\xf6?\xeb\xea\x1azg\x8e\x1e\xc0\x0fL\x1e\xeb&\n\x18@\xe8T/Xh\xeb\x15\xc0>3\xb4(\xc9\xb8\x0b\xc0\xa8:~\x05w\x0e!@\x01\x9bs\xd29\xef\x17@\xaeb\xf8\xa6\x8b^\'\xc0\xa4\xffC#>\xb9\x14\xc08h\xfd\xca\xd0\xae\x03\xc0\xe4g\x90\xcd3X\xfe\xbf\x1b\xa1;\x07\xe2\xa9\x17@h\xafLgM\xb0\x06@<\x8cF#\x87D\xfd\xbfA\x03\xf6\xc4\x1fh"\xc0\xd4\x00\x95\xe8\x90\xc6\x1c@g\xdc\xfa h\x12\x18@\x18\xbc\xc4\xc6Nb\x06\xc0]\xa6\x18c\xfe\xfc\x12@Z\xa2\xe9\x85\xbf@*\xc0\x08\xb7\xc9KU\xbb\x08\xc0\x07\x8b\x13j{\x18\x07@\x12]\xcbP\x88\xa0\x15\xc0\x8c\x0b\xc7\xea\t.\x17@\xfd\x07\x9a\'\xa1\x06\x11@I%\xe7n>\x1d"@\x0ee\x85}\xbc\x95$@\x00\x86\x1b\xd1\xf3\x97\x01@\xd2\xad\xbf;\x02e\x0e@\xd2\xde\xaa\xa0\xc5\\\x12@@\xc7\xf6\xb1C\xa3\xcd\xbf \xfcsI\xf9c\xfe?\xe1\x04P\xbd\x95\x03\x1d@y\xf9\xabf\xef\xba\x06@\xba}\xc4\x7f\x1b\x02\t\xc0\xe2z\t+\xe7;\x12@&<\xf0\xa4\xd7\x95\x19\xc0]\xd7I\xa7*\x8b\x1e@\xdb\x12R\xe6\xb9\xb4\x1c\xc0\x8a\x8f7O\xd6\x91\x1c\xc06\xe2=eN(\x14\xc05\x14\x1f\x12Pt\x11@8"n\xf1F\xa7\x0f@\xe3\xe4bH[)\x12\xc0\xc0@\x8e\x01Y\x8b\xcc?O2\xc6A\xdf3\x03@.+v\x1fH\xdd\x1e\xc0\x00\x17\xc3\x1aiI\xb3?\x02 T\x92\xf4\xad\xf8?\x0c\xc9\x83\xfe\x80\xd2\x1f\xc0\x85D/\x92\xc9\x92\xe5?\xae\xcex\xd5z\xbf\x1d\xc0\x8f}N\x14\xfb\x96\x1d\xc0\xf7\ru=U\xa9\x1c\xc0\xd7\xec\xb5\xc9\xd7\x19!\xc0\xd0\r\xa9\xe1t\xea @\x02\x8e\x14\x1fU\xcd\x16@p=\x8a\x908\xc1\xf0?$Kn\xfc\xda\xa0\x07\xc0\xd5\xe5\x17\xfa\x92\xe0\r@\xe3\x97Gk\x0c-\x19\xc0\n^V2\x16_"\xc0\xc6\x07;\xd2\x81p\x0e\xc0\x16\xe6 \x96F9\x10\xc0\xbeB\rd\xe3\xc1\x16@\xee_\x87\x8e}\xbc\x05@\x9e\x8a\x15s\x9d\x00\r@\x0e\xb6\xaf@\xfe\xb4\x0b\xc0\xd4~\x02\xc5\xe5M"@<\xa1\x9e\xe6\x99\x0b\xfb\xbf\xf0\xf2:\x06\x85\xdc\xbd\xbf\xb0+\x17R\xf2\xac\x05@\xd7g\xf1\x85\xc8\xad\x13\xc0\xba\xe1/J\x9d)\'\xc0\xc9\x05\xf6\x84\xc3\x89\x1a\xc0\x04~\x0cZ\xc3\x9f\xf9?\x9c\xef\xd3Ln\x88\xea?\x91\xeaV\xac\x8c\xa7\x13@\x97F\x89\x8a\xcc\x9f\x1e\xc0\x14\xa0\xb5\xabN\xfe\x13\xc0L\n\x0c\xf4h\\\xfc?nD\xe8\n\xc9\x99 @b\x1b\x12\x16\xfb\' \xc0\x19(\xeaf\xa6\x8f\x1b@ 5,\x1ds\xfe\x10\xc0\x05\x0b6\xe7\xd9\xad\x16@\x9a\xc9\xb8\x9fR\n\x01@b&>S\r\xff\x19\xc0\xba\xc0t\xad\x7f\x05 @\xd2\xfd\xd4\xc8\x96i\x18\xc0\x12\xd1=1$\xfc\x05\xc0\xa3(i\xbf4\x03\x10@=\xd3h\xfb\x06! \xc04v\xc3\xf2.\xa3\xf0?\x10\xe7\xba\xc9\x01t\xee?\xfd\xe0\xedd\xd5R!\xc0\x9cL\x89\xcfU\xe6\x14@\x03\xbfN\x04\xc8=\xe6?*\x14\xb7\xea\x01\x02\x1f\xc0U\xf4\x13\x97\x9aS\x1b\xc0\x16\x8e!\x11\x98\x81\r\xc0}\x97\x17\x1c\xa1\x82$\xc0\x17\xfa|\x01#\xcb\x17\xc0H/*.O\xfb\x03\xc0\xa8aN9\xc5\x93\x12\xc0\xcc\x8cfz\xca+\xf3\xbf\xce\x07\x8bnW\x00\x05@\xa3\x9b\x91\xed\x17\x7f\x1d@\xb4\x9a\x14F:\xab\x10@Q0/;}+\x16@\xda\xdb\xac>\xd3: \xc0.A\xaa\xd9\xe4\x10\x08@\x83\xc6\xea\xa8\x1f\xef @\xf5j\xfck\xa3\x85\x18\xc0\xe69\x90\xd4\xf1k\x17@\x1cY\x8b\xfb\x00\xc8\x1b\xc0\xf2\r\xe2\xbd\xcd\xa7!\xc0\xd8]\xeb\xaf3~\xe6?F\x82\x95\xcfI\xac\x1a@\xe3\x86\xc6\x01\xd0p\x17@\x15\xc4\xc9}\xda+\x1b@"U\xa0Da\x88\x15@s \xe0\x93\xbc\x94%\xc0\x8dgM\x826\xd8\x16\xc0\x88\'Ri\xe4A#\xc0l\xe2\xde\xf4J_\x00\xc0\x8f \x82]F|\x19\xc0\x1cK\xceJ\x8cw$\xc0|\x00N\xd9\xa1\x1c\x13\xc0B)\x91\x9bD\xce\x05\xc0-Np x\xf2\x0f@DQ\xb9\xc9i\xdd\xf6?\x9b\x86\x07\x91\xa4\xbf\x1f@\x17\x807\xe1\x8d\x89\x1f\xc0\xa3\xfdH\xf7\xfe"\x1c\xc0\x98\x90\xb7p\xf4\xcc\xfb\xbf\xdd\x83\xa6\'\xde\x97\x00@\xa6\xd2O!kw\r@1\xc7\x84uA1\x01@\x82X\x89\xbeO\xbe\xe2\xbf6\x80\x9aG\xf1\'#@\xda\n\xd8\x18\x9e\xa5!@\x91U\x90\xf3\xed\x7f%\xc0\x14\xdf\x9f\x94\xa5\xc7\x14@\r*=\xf1a\xb0\x04@ #\x88\xba\tK\x01\xc0\xd6a`\xf4KF\x17\xc0\xd6*\x16\xb0\xc5\xd2"\xc0L\xce+\xb0&\xd1\x17@"<\xed\'?\xc6\xe6?\xb8#\x9b&2\xf0\xe3\xbf\xbaF[\xec\xc5\x94#\xc0\x86\xed^\xcd|\xc1\x0e@\'\x13\xd8el\xfe\x17@\xa2\xcf9\x9b\xc6\xb2\xf4?\x10\xc24\x8eZ5\xfc\xbf4|U\x9e\x13\xe2!\xc0\xb4\xb8O]75\x15\xc0@\xbaA\xf0\xb2\xcc\x1f@\xc0\xf8\xf1\x96X\xdd\x12@X\xa0\xb8g\xbc\x1f\xf5\xbf^?\x85O\xc0N\x06\xc0:V\'<;\xc8\x0b@\xbe\'\xbb\xa60U%\xc0\xbdH+8\x88\xbf\x15\xc0\xb4>\xc3\xae\x0f\x89\x18@\x10\xca\xe5\xba\xcav\x0b\xc0\xd4I\x83\xef\x83\xeb"@\x95&T/\xbc\xb9\n\xc0\x06\xba\xdc-\x08\xbf\x02\xc0,a\xa29\xf7\xfc\xf6?\xce\xa5:\xc3O_!@\xd8\xc3y\x9e\x1d*\x13\xc0\x95\x8e#\xad\xa0\x84\x1e@\xb8\xcc\xd1\r\xd8\xc8\xf9\xbfx&0\xe5\xe6\x8b\x15\xc0\xb9\x80[\x05,k\x1c@\xf1W\xafb\x8d\xf1\x17\xc0|\xb5\x83\xf8M\x1c\xe3\xbf\xa4j\x898F\x1c\x1e\xc0p\x8fl\xf3\xe9y\x10@\xffc\x1dJ@\xd6#\xc0\x06]\xe5\x04\x04*\x07\xc0\xd3\xf7a\xf5\xc3a$\xc0\xeb\xbb\xae\xf4E\xea\xe9\xbf.k*\x03\xee\x81\x08@d\xf41bX\xdb\xf1?\x8e8\xaad\xa9o\xf1\xbf\x94\xa8v\x8bK\x9f%\xc0\xcc\x9f\xf6\xb2\x04:\xf9\xbf\x88\xd8UoGh\x0f@ \r\x9b\xa5A\x0e\xeb\xbf\x07\xfd&M#\x86\x06@-\'\xe7\x808\xf8$@\xb60\xd2b\xcd!\x18@|\xfb\xedEmy\x1e\xc0#\xc3\x81\xa1+\xd5\x1b\xc0\x0cJ)u\xdf-\x08\xc0P\x0b"\x11\x98\xbe\xe1\xbf\xb8\xa8\xbd7\x88\xa2\x14\xc0\xb3\xea#*\x14\x8b\t@=g\xca\xa2\xd8\x95\x12\xc0' +tb. \ No newline at end of file diff --git a/python/test/tests.py b/python/test/tests.py new file mode 100644 index 0000000..2bb2a2a --- /dev/null +++ b/python/test/tests.py @@ -0,0 +1,221 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +from nose.tools import assert_true, assert_equal + +import pickle as pkl +import sys +import os +import numpy as np +from sklearn.cross_validation import train_test_split + +sys.path.insert(1, os.path.abspath('..')) +from lopq.model import LOPQModel, eigenvalue_allocation, accumulate_covariance_estimators, compute_rotations_from_accumulators +from lopq.search import LOPQSearcher +from lopq.eval import compute_all_neighbors, get_cell_histogram, get_recall + +######################################## +# Helpers +######################################## + + +relpath = lambda x: os.path.abspath(os.path.join(os.path.dirname(__file__), x)) + + +def load_oxford_data(): + from lopq.utils import load_xvecs + + data = load_xvecs(relpath('../../data/oxford/oxford_features.fvecs')) + return data + + +def make_random_model(): + m = LOPQModel(V=5, M=4, subquantizer_clusters=10) + m.fit(np.random.RandomState(42).rand(200, 8), n_init=1) + return m + +######################################## +# Tests +######################################## + + +def test_eigenvalue_allocation(): + a = pkl.load(open(relpath('./testdata/test_eigenvalue_allocation_input.pkl'))) + + vals, vecs = np.linalg.eigh(a) + res = eigenvalue_allocation(4, vals) + + expected = np.array([ + 63, 56, 52, 48, 44, 40, 36, 30, 26, 22, 18, 14, 10, 6, 3, 0, + 62, 57, 53, 51, 45, 41, 39, 33, 32, 31, 29, 25, 21, 17, 13, 9, + 61, 58, 54, 49, 47, 42, 38, 34, 28, 24, 20, 16, 12, 8, 5, 2, + 60, 59, 55, 50, 46, 43, 37, 35, 27, 23, 19, 15, 11, 7, 4, 1 + ]) + + assert_true(np.equal(res, expected).all()) + + +def test_eigenvalue_allocation_normalized_features(): + eigenvalues = np.array([ + 2.02255824, 1.01940991, 0.01569471, 0.01355569, 0.01264379, + 0.01137654, 0.01108961, 0.01054673, 0.01023358, 0.00989679, + 0.00939045, 0.00900322, 0.00878857, 0.00870027, 0.00850136, + 0.00825236, 0.00813437, 0.00800231, 0.00790201, 0.00782219, + 0.00763405, 0.00752334, 0.00739174, 0.00728246, 0.00701366, + 0.00697365, 0.00677283, 0.00669658, 0.00654397, 0.00647679, + 0.00630645, 0.00621057 + ]) + indices = eigenvalue_allocation(2, eigenvalues) + + first_half = eigenvalues[indices[:16]] + second_half = eigenvalues[indices[16:]] + diff = np.abs(np.sum(np.log(first_half)) - np.sum(np.log(second_half))) + assert_true(diff < .1, "eigenvalue_allocation is not working correctly") + + +def test_accumulate_covariance_estimators(): + data, centroids = pkl.load(open(relpath('./testdata/test_accumulate_covariance_estimators_input.pkl'))) + expected = pkl.load(open(relpath('./testdata/test_accumulate_covariance_estimators_output.pkl'))) + + actual = accumulate_covariance_estimators(data, centroids) + + # Summed residual outer products + assert_true(np.allclose(expected[0], actual[0])) + + # Summed residuals + assert_true(np.allclose(expected[1], actual[1])) + + # Assignment count per cluster + assert_true(np.array_equal(expected[2], actual[2])) + + # Assignments over data + assert_true(np.array_equal(expected[3], actual[3])) + + # Residual data + assert_true(np.allclose(expected[4], actual[4])) + + +def test_compute_rotations_from_accumulators(): + + A, mu, count, num_buckets = pkl.load(open(relpath('./testdata/test_compute_rotations_from_accumulators_input.pkl'))) + expected = pkl.load(open(relpath('./testdata/test_compute_rotations_from_accumulators_output.pkl'))) + + actual = compute_rotations_from_accumulators(A, mu, count, num_buckets) + + # Rotations + assert_true(np.allclose(expected[0], actual[0])) + + # Mean residuals + assert_true(np.allclose(expected[1], actual[1])) + + +def test_reconstruction(): + m = LOPQModel.load_proto(relpath('./testdata/random_test_model.lopq')) + + code = ((0, 1), (0, 1, 2, 3)) + r = m.reconstruct(code) + expected = [-2.27444688, 6.47126941, 4.5042611, 4.76683476, 0.83671082, 9.36027283, 8.11780532, 6.34846377] + + assert_true(np.allclose(expected, r)) + + +def test_oxford5k(): + + random_state = 40 + data = load_oxford_data() + train, test = train_test_split(data, test_size=0.2, random_state=random_state) + + # Compute distance-sorted neighbors in training set for each point in test set + nns = compute_all_neighbors(test, train) + + # Fit model + m = LOPQModel(V=16, M=8) + m.fit(train, n_init=1, random_state=random_state) + + # Assert correct code computation + assert_equal(m.predict(test[0]), ((3, 2), (14, 164, 83, 49, 185, 29, 196, 250))) + + # Assert low number of empty cells + h = get_cell_histogram(train, m) + assert_equal(np.count_nonzero(h == 0), 6) + + # Assert true NN recall on test set + searcher = LOPQSearcher(m) + searcher.add_data(train) + recall, _ = get_recall(searcher, test, nns) + assert_true(np.all(recall > [0.51, 0.92, 0.97, 0.97])) + + # Test partial fitting with just coarse quantizers + m2 = LOPQModel(V=16, M=8, parameters=(m.Cs, None, None, None)) + m2.fit(train, n_init=1, random_state=random_state) + + searcher = LOPQSearcher(m2) + searcher.add_data(train) + recall, _ = get_recall(searcher, test, nns) + assert_true(np.all(recall > [0.51, 0.92, 0.97, 0.97])) + + # Test partial fitting with coarse quantizers and rotations + m3 = LOPQModel(V=16, M=8, parameters=(m.Cs, m.Rs, m.mus, None)) + m3.fit(train, n_init=1, random_state=random_state) + + searcher = LOPQSearcher(m3) + searcher.add_data(train) + recall, _ = get_recall(searcher, test, nns) + assert_true(np.all(recall > [0.51, 0.92, 0.97, 0.97])) + + +def test_proto(): + import os + + filename = './temp_proto.lopq' + m = make_random_model() + m.export_proto(filename) + m2 = LOPQModel.load_proto(filename) + + assert_equal(m.V, m2.V) + assert_equal(m.M, m2.M) + assert_equal(m.subquantizer_clusters, m2.subquantizer_clusters) + + assert_true(np.allclose(m.Cs[0], m2.Cs[0])) + assert_true(np.allclose(m.Rs[0], m2.Rs[0])) + assert_true(np.allclose(m.mus[0], m2.mus[0])) + assert_true(np.allclose(m.subquantizers[0][0], m.subquantizers[0][0])) + + os.remove(filename) + + +def test_mat(): + import os + + filename = './temp_mat.mat' + m = make_random_model() + m.export_mat(filename) + m2 = LOPQModel.load_mat(filename) + + assert_equal(m.V, m2.V) + assert_equal(m.M, m2.M) + assert_equal(m.subquantizer_clusters, m2.subquantizer_clusters) + + assert_true(np.allclose(m.Cs[0], m2.Cs[0])) + assert_true(np.allclose(m.Rs[0], m2.Rs[0])) + assert_true(np.allclose(m.mus[0], m2.mus[0])) + assert_true(np.allclose(m.subquantizers[0][0], m.subquantizers[0][0])) + + os.remove(filename) + + +def test_searcher(): + data = pkl.load(open(relpath('./testdata/test_searcher_data.pkl'))) + m = LOPQModel.load_proto(relpath('./testdata/random_test_model.lopq')) + + searcher = LOPQSearcher(m) + searcher.add_data(data) + + q = np.ones(8) + + retrieved, visited = searcher.get_result_quota(q) + assert_equal(len(retrieved), 12) + assert_equal(visited, 3) + + retrieved, visited = searcher.get_result_quota(q, quota=20) + assert_equal(len(retrieved), 28) + assert_equal(visited, 5) diff --git a/python/tox.ini b/python/tox.ini new file mode 100644 index 0000000..045ae34 --- /dev/null +++ b/python/tox.ini @@ -0,0 +1,37 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py26, py27 + +[testenv] +deps= + nose + nose-cov + coveralls + +commands= + nosetests --exe --with-xunit --xunit-file=nosetests.xml --with-coverage --cover-xml --cover-erase --cover-package=lopq --cover-xml-file=cobertura.xml test/tests.py + +[flake8] +filename= *.py +show-source = False + +# H104 File contains nothing but comments +# H405 multi line docstring summary not separated with an empty line +# H803 Commit message should not end with a period (do not remove per list discussion) +# H904 Wrap long lines in parentheses instead of a backslash +ignore = H104,H405,H803,H904 + +builtins = _ +exclude=.venv,.git,.tox,build,dist,docs,*lib/python*,*egg,tools,vendor,.update-venv,*.ini,*.po,*.pot,lopq_model_pb2.py +max-line-length = 160 + +[testenv:pep8] +deps= + flake8 +commands = + flake8 {posargs} + diff --git a/scripts/example.py b/scripts/example.py new file mode 100644 index 0000000..02b21e8 --- /dev/null +++ b/scripts/example.py @@ -0,0 +1,134 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +import sys +import os + +# Add the lopq module - not needed if they are available in the python environment +sys.path.append(os.path.abspath('../python')) + +import numpy as np +from sklearn.cross_validation import train_test_split + +from lopq import LOPQModel, LOPQSearcher +from lopq.eval import compute_all_neighbors, get_recall +from lopq.model import eigenvalue_allocation + + +def load_oxford_data(): + from lopq.utils import load_xvecs + + data = load_xvecs('../data/oxford/oxford_features.fvecs') + return data + + +def pca(data): + """ + A simple PCA implementation that demonstrates how eigenvalue allocation + is used to permute dimensions in order to balance the variance across + subvectors. There are plenty of PCA implementations elsewhere. What is + important is that the eigenvalues can be used to compute a variance-balancing + dimension permutation. + """ + + # Compute mean + count, D = data.shape + mu = data.sum(axis=0) / float(count) + + # Compute covariance + summed_covar = reduce(lambda acc, x: acc + np.outer(x, x), data, np.zeros((D, D))) + A = summed_covar / (count - 1) - np.outer(mu, mu) + + # Compute eigen decomposition + eigenvalues, P = np.linalg.eigh(A) + + # Compute a permutation of dimensions to balance variance among 2 subvectors + permuted_inds = eigenvalue_allocation(2, eigenvalues) + + # Build the permutation into the rotation matrix. One can alternately keep + # these steps separate, rotating and then permuting, if desired. + P = P[:, permuted_inds] + + return P, mu + + +def main(): + """ + A brief demo script showing how to train various LOPQ models with brief + discussion of trade offs. + """ + + # Get the oxford dataset + data = load_oxford_data() + + # Compute PCA of oxford dataset. See README in data/oxford for details + # about this dataset. + P, mu = pca(data) + + # Mean center and rotate the data; includes dimension permutation. + # It is worthwhile see how this affects recall performance. On this + # dataset, which is already PCA'd from higher dimensional features, + # this additional step to variance balance the dimensions typically + # improves recall@1 by 3-5%. The benefit can be much greater depending + # on the dataset. + data = data - mu + data = np.dot(data, P) + + # Create a train and test split. The test split will become + # a set of queries for which we will compute the true nearest neighbors. + train, test = train_test_split(data, test_size=0.2) + + # Compute distance-sorted neighbors in training set for each point in test set. + # These will be our groundtruth for recall evaluation. + nns = compute_all_neighbors(test, train) + + # Fit model + m = LOPQModel(V=16, M=8) + m.fit(train, n_init=1) + + # Note that we didn't specify a random seed for fitting the model, so different + # runs will be different. You may also see a warning that some local projections + # can't be estimated because too few points fall in a cluster. This is ok for the + # purposes of this demo, but you might want to avoid this by increasing the amount + # of training data or decreasing the number of clusters (the V hyperparameter). + + # With a model in hand, we can test it's recall. We populate a LOPQSearcher + # instance with data and get recall stats. By default, we will retrieve 1000 + # ranked results for each query vector for recall evaluation. + searcher = LOPQSearcher(m) + searcher.add_data(train) + recall, _ = get_recall(searcher, test, nns) + print 'Recall (V=%d, M=%d, subquants=%d): %s' % (m.V, m.M, m.subquantizer_clusters, str(recall)) + + # We can experiment with other hyperparameters without discarding all + # parameters everytime. Here we train a new model that uses the same coarse + # quantizers but a higher number of subquantizers, i.e. we increase M. + m2 = LOPQModel(V=16, M=16, parameters=(m.Cs, None, None, None)) + m2.fit(train, n_init=1) + + # Let's evaluate again. + searcher = LOPQSearcher(m2) + searcher.add_data(train) + recall, _ = get_recall(searcher, test, nns) + print 'Recall (V=%d, M=%d, subquants=%d): %s' % (m2.V, m2.M, m2.subquantizer_clusters, str(recall)) + + # The recall is probably higher. We got better recall with a finer quantization + # at the expense of more data required for index items. + + # We can also hold both coarse quantizers and rotations fixed and see what + # increasing the number of subquantizer clusters does to performance. + m3 = LOPQModel(V=16, M=8, subquantizer_clusters=512, parameters=(m.Cs, m.Rs, m.mus, None)) + m3.fit(train, n_init=1) + + searcher = LOPQSearcher(m3) + searcher.add_data(train) + recall, _ = get_recall(searcher, test, nns) + print 'Recall (V=%d, M=%d, subquants=%d): %s' % (m3.V, m3.M, m3.subquantizer_clusters, str(recall)) + + # The recall is probably better than the first but worse than the second. We increased recall + # only a little by increasing the number of model parameters (double the subquatizer centroids), + # the index storage requirement (another bit for each fine code), and distance computation time + # (double the subquantizer centroids). + + +if __name__ == '__main__': + main() diff --git a/scripts/query_runtime.py b/scripts/query_runtime.py new file mode 100644 index 0000000..795fd6a --- /dev/null +++ b/scripts/query_runtime.py @@ -0,0 +1,67 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +from math import sqrt, ceil + + +def multiseq_flops(V, D): + """ + Given the number of coarse clusters and the dimension of the data, + compute the number of flops to required to rank each coarse vocabulary + to the query. + """ + # (total coarse vocabulary) * (dims per coarse split) * (flops per squared distance) + return (2 * V) * (D / 2) * 2 + + +def cluster_rotation_flops(D): + """ + Given the dimension of the data, compute the number of flops for a + single local projection. + """ + D2 = D / 2 + return D2 ** 2 + D2 + + +def subquantizer_flops(D, M, clusters=256): + """ + Given the dimension of the data, the number of subquantizers and the + subquantizer vocabulary size, compute the number of flops to compute + a projected query's LOPQ distance for a single half of the query. + """ + # (subquants per half) * (dims per subquant) * (cluster per subquant) * (flops per squared distance) + return (M / 2) * (D / M) * clusters * 2 + + +def total_rank_flops(D, M, N, cells, badness=0.5): + """ + Given the dimension of the data, the number of subquantizers, the number + of results to rank, the number of multi-index cells retrieved by the query, + and a badness measure that interpolates between best case and worst case + in terms of reusable (cacheable) subquantizer distance computations, compute + the number of flops to rank the N results. + + The 'badness' will vary query to query and is determined by the data distribution. + """ + # Corresponds to traversing a row or column of the multi-index grid + worst_case = cells + 1 + + # Corresponds to traversing a square in the multi-index grid + best_case = 2 * sqrt(cells) + + # Interpolated number of clusters + num_clusters = ceil(worst_case * badness + best_case * (1 - badness)) + + # (total local projections required) + (total number of sums to compute distance) + return num_clusters * (cluster_rotation_flops(D) + subquantizer_flops(D, M)) + N * (M - 1) + + +def brute_force_flops(D, N): + """ + Given the data dimension and the number of results to rank, compute + the number of flops for brute force exact distance computation. + """ + return N * (3 * D) + + +def ratio(D, M, N, cells, badness=0.5): + return total_rank_flops(D, M, N, cells, badness) / brute_force_flops(D, N) diff --git a/spark/README.md b/spark/README.md new file mode 100644 index 0000000..013d71d --- /dev/null +++ b/spark/README.md @@ -0,0 +1,134 @@ +# Spark + +This is an implementation of LOPQ training for [Apache Spark](https://spark.apache.org/). Spark's in-memory execution model is well-suited to LOPQ training since there are multiple steps of clustering that involve repeated access to the same data. The scripts provided here run with pyspark and use core functionality implemented in the `lopq` python module. + +#### A note about Spark environments + +The following usage examples assume that you have a well configured Spark environment suited to the available hardware. Additionally, we assume that the python environment available on both the Spark driver and executors contains all the necessary dependencies, namely the modules listed in `python/requirements.txt` as well as the `lopq` module itself. The [Anaconda](https://www.continuum.io/why-anaconda) environment is a good starting point. At the time of writing, it contains all required dependencies by default except the `protobuf` module, which can be easily installed. To distribute the `lopq` module itself, you could either install it into the environment running on your Spark cluster, or submit it with the Spark job. For example, you can zip the module from the repository root (`zip -r lopq.zip python/lopq`) and then submit this zip file with the `--py-files` argument. More information about submitting jobs to Spark is available [here](https://spark.apache.org/docs/latest/submitting-applications.html). + +## PCA Training + +A necessary preprocessing step for training is to PCA and variance balance the raw data vectors to produce the LOPQ data vectors, i.e. the vectors that LOPQ will quantize. The PCA step is important because it axis-aligns the data and optionally reduces the dimensionality, resulting in better quantization. The variance balancing step permutes the dimensions of the PCA'd vectors so that the first half and second half of the data vectors have roughly the same total variance, which makes the LOPQ coarse codes much better at quantizing the data since each half will be equally "important". + +The `train_pca.py` script is provided to compute PCA parameters on Spark. It will output a pickled dict of PCA parameters. See discussion of data handling in the LOPQ Training section below to learn about loading custom data formats. + +#### Available parameters + +| Command line arg | Default | Description | +| ----------------------------- | ------- | ------------------------------------------------------------------------------ | +| --data | None | hdfs path to input data | +| --data_udf | None | optional module name contained a `udf` function to load training data | +| --seed | None | optional random seed | +| --sampling_ratio | 1.0 | proportion of data to sample for training | +| --output | None | hdfs output path | + + +## LOPQ Training + +The `train_model.py` script can be configured to run full or partial training of LOPQ models on Spark. The script can resume training from an existing model, using some parameters from the existing model. An existing model can be provided to the script as a pickle file. The `--steps` parameters indicates which steps of training to perform; `0` indicates coarse clustering, `1` indicates rotations fittiing, and `2` indicates subquantizer clustering. The default is for all training steps to be performed. + +#### Available parameters + +| Command line arg | Default | Description | +| ----------------------------- | ------- | ------------------------------------------------------------------------------ | +| --data | None | hdfs path to input data | +| --data_udf | None | optional module name contained a `udf` function to load training data | +| --seed | None | optional random seed | +| --sampling_ratio | 1.0 | proportion of data to sample for training | +| --subquantizer_sampling_ratio | 1.0 | proportion of data to subsample for subquantizer training | +| --existing_model_pkl | None | a pickled LOPQModel from which to extract existing parameters | +| --existing_model_proto | None | a protobuf of existing parameters | +| --V | None | number of coarse clusters | +| --M | None | total number of subquantizers | +| --subquantizer_clusters | 256 | number of subquantizer clusters | +| --steps | 0,1,2 | comma-separated list of integers indicating which steps of training to perform | +| --model_pkl | None | hdfs path to save pickle file of resulting LOPQModel | +| --model_proto | None | hdfs path to save protobuf file of resulting model parameters | + +#### Usage + +Here is an example of training a full model from scratch and saving the model parameters as both a pickle file and a protobuf file: + +```bash +spark-submit train_model.py \ + --data /hdfs/path/to/data \ + --V 16 \ + --M 8 \ + --model_pkl /hdfs/output/path/model.pkl \ + --model_proto /hdfs/output/path/model.lopq +``` + +By providing an existing model, the script can use existing parameters and only the training pipeline for the remaining parameters. This is useful when you want to explore different hyperparameters without retraining everything from scratch. Here is an example of using the coarse quantizers in an existing model and training only rotations and subquantizers. Note that the existing model must be provided to Spark via the `--files` argument. The model can also be provided in protobuf format with `--existing_model_proto`. + +```bash +spark-submit \ + --files /path/to/name_of_existing_model.pkl \ + train_model.py \ + --data /hdfs/path/to/data \ + --model_pkl /hdfs/output/path/model.pkl \ + --existing_model_pkl name_of_existing_model.pkl \ + --M 8 \ + --steps 1,2 +``` + +#### Data handling + +By default, the training script assumes that your training data is in a text file of tab-delimited `(id, data)` pairs, where the data vector is a base64-encoded pickled numpy array. If this is not the format that your data is in, you can provide the training script a UDF to load the data from your format. This UDF has the following signature: + +```python +def udf(sc, data_path, sampling_ratio, seed): + pass +``` + +where `sc` is the SparkContext instance, `data_path` is the path provided to the `--data` argument, and `sampling_ratio` and `seed` are the values provided to the arguments of the same name. This UDF must return an RDD of numpy arrays representing the training data and must be named `udf`. An example is provided in `example_udf.py`. The UDF is provided to the script by submitting its module via `--py-files` and passing the module name to the script via `--data-udf`, e.g.: + +```bash +spark-submit \ + --py-files example_udf.py \ + train_model.py \ + --data /hdfs/path/to/data \ + --data_udf example_udf \ + --V 16 \ + --M 8 \ + --model_proto /hdfs/output/path/model.lopq +``` + +## Code Computation + +The `compute_codes.py` script takes a fully trained model, an input file of features on hdfs, and an output path on hdfs, and computes LOH codes for all points. The model must be distributed with the job using the `--files` option. The script consumes `(id , data)` pairs and produces a text file of tab-delimited `(id, json-formatted LOPQ code)` pairs, e.g.: + +``` +33 [[15, 13], [0, 165, 1, 72, 178, 147, 170, 69]] +34 [[5, 9], [104, 227, 160, 185, 248, 152, 170, 126]] +35 [[14, 10], [221, 144, 4, 186, 172, 40, 32, 228]] +36 [[3, 5], [76, 216, 141, 161, 247, 2, 34, 219]] +37 [[0, 5], [205, 140, 214, 194, 39, 229, 131, 0]] +38 [[12, 3], [149, 48, 249, 224, 98, 255, 210, 131]] +``` + +#### Available parameters + +| Command line arg | Default | Description | +| ----------------------------- | ------- | ------------------------------------------------------------------------------ | +| --data | None | hdfs path to input data | +| --data_udf | None | optional module name contained a `udf` function to load training data | +| --seed | None | optional random seed | +| --sampling_ratio | 1.0 | proportion of data to sample | +| --output | None | hdfs output path | +| --model_pkl | None | file name of the model pickle | +| --model_proto | None | file name of the model protobuf file | + +#### Usage + +```bash +spark-submit \ + --files /path/to/name_of_existing_model.pkl \ + compute_codes.py \ + --data /hdfs/path/to/data \ + --output /hdfs/output/path \ + --model_pkl name_of_existing_model.pkl +``` + +#### Data handling + +This script also provides a way for the user to load data from other formats via a UDF. It differs from the training script only in that the output of the UDF must be an RDD of `(id, data)` pairs. diff --git a/spark/compute_codes.py b/spark/compute_codes.py new file mode 100644 index 0000000..b9c3412 --- /dev/null +++ b/spark/compute_codes.py @@ -0,0 +1,79 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +from pyspark.context import SparkContext + +import cPickle as pkl +import base64 +import json + +from lopq.model import LOPQModel + + +def default_data_loading(sc, data_path, sampling_ratio, seed): + """ + This function loads data from a text file, sampling it by the provided + ratio and random seed, and interprets each line as a tab-separated (id, data) pair + where 'data' is assumed to be a base64-encoded pickled numpy array. + The data is returned as an RDD of (id, numpy array) tuples. + """ + # Compute the number of cores in our cluster - used below to heuristically set the number of partitions + total_cores = int(sc._conf.get('spark.executor.instances')) * int(sc._conf.get('spark.executor.cores')) + + # Load and sample down the dataset + d = sc.textFile(data_path, total_cores * 3).sample(False, sampling_ratio, seed) + + # The data is (id, vector) tab-delimited pairs where each vector is + # a base64-encoded pickled numpy array + d = d.map(lambda x: x.split('\t')).map(lambda x: (x[0], pkl.loads(base64.decodestring(x[1])))) + + return d + + +def main(sc, args, data_load_fn=default_data_loading): + + # Load model + model = None + if args.model_pkl: + model = pkl.load(open(args.model_pkl)) + elif args.model_proto: + model = LOPQModel.load_proto(args.model_proto) + + # Load data + d = data_load_fn(sc, args.data, args.sampling_ratio, args.seed) + + # Distribute model instance + m = sc.broadcast(model) + + # Compute codes and convert to string + codes = d.map(lambda x: (x[0], m.value.predict(x[1]))).map(lambda x: '%s\t%s' % (x[0], json.dumps(x[1]))) + + codes.saveAsTextFile(args.output) + +if __name__ == "__main__": + from argparse import ArgumentParser + parser = ArgumentParser() + + # Data handling parameters + parser.add_argument('--data', dest='data', type=str, default=None, required=True, help='hdfs path to input data') + parser.add_argument('--data_udf', dest='data_udf', type=str, default=None, help='module name from which to load a data loading UDF') + parser.add_argument('--seed', dest='seed', type=int, default=None, help='optional random seed for sampling') + parser.add_argument('--sampling_ratio', dest='sampling_ratio', type=float, default=1.0, help='proportion of data to sample for model application') + parser.add_argument('--output', dest='output', type=str, default=None, required=True, help='hdfs path to output data') + + existing_model_group = parser.add_mutually_exclusive_group(required=True) + existing_model_group.add_argument('--model_pkl', dest='model_pkl', type=str, default=None, help='a pickled LOPQModel to evaluate on the data') + existing_model_group.add_argument('--model_proto', dest='model_proto', type=str, default=None, help='a protobuf LOPQModel to evaluate on the data') + + args = parser.parse_args() + + sc = SparkContext(appName='LOPQ code computation') + + # Load UDF module if provided + if args.data_udf: + udf_module = __import__(args.data_udf, fromlist=['udf']) + load_udf = udf_module.udf + main(sc, args, data_load_fn=load_udf) + else: + main(sc, args) + + sc.stop() diff --git a/spark/example_udf.py b/spark/example_udf.py new file mode 100644 index 0000000..1944579 --- /dev/null +++ b/spark/example_udf.py @@ -0,0 +1,22 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +import cPickle as pkl +import base64 + + +def udf(sc, data_path, sampling_ratio, seed): + """ + This is an example UDF function to load training data. It loads data from a text file + with base64-encoded pickled numpy arrays on each line. + """ + + # Compute the number of cores in our cluster - used below to heuristically set the number of partitions + total_cores = int(sc._conf.get('spark.executor.instances')) * int(sc._conf.get('spark.executor.cores')) + + # Load and sample down the dataset + d = sc.textFile(data_path, total_cores * 3).sample(False, sampling_ratio, seed) + + deserialize_vec = lambda s: pkl.loads(base64.decodestring(s)) + vecs = d.map(deserialize_vec) + + return vecs diff --git a/spark/train_model.py b/spark/train_model.py new file mode 100644 index 0000000..d31c14c --- /dev/null +++ b/spark/train_model.py @@ -0,0 +1,422 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +from pyspark.context import SparkContext + +import numpy as np +import cPickle as pkl +import base64 +import os +import subprocess +import sys +from tempfile import NamedTemporaryFile +from operator import add + +from pyspark.mllib.clustering import KMeans, KMeansModel +from lopq.model import LOPQModel, compute_rotations_from_accumulators + + +STEP_COARSE = 0 +STEP_ROTATION = 1 +STEP_SUBQUANT = 2 + + +def default_data_loading(sc, data_path, sampling_ratio, seed): + """ + This function loads training data from a text file, sampling it by the provided + ratio and random seed, and interprets each line as a tab-separated (id, data) pair + where 'data' is assumed to be a base64-encoded pickled numpy array. The ids are discarded. + The data is returned as an RDD of numpy arrays. + """ + # Compute the number of cores in our cluster - used below to heuristically set the number of partitions + total_cores = int(sc._conf.get('spark.executor.instances')) * int(sc._conf.get('spark.executor.cores')) + + # Load and sample down the dataset + d = sc.textFile(data_path, total_cores * 3).sample(False, sampling_ratio, seed) + + # The data is (id, vector) tab-delimited pairs where each vector is + # a base64-encoded pickled numpy array + deserialize_vec = lambda s: pkl.loads(base64.decodestring(s.split('\t')[1])) + vecs = d.map(deserialize_vec) + + return vecs + + +def load_data(sc, args, data_load_fn=default_data_loading): + """ + Load training data as an RDD. + """ + # Load data + vecs = data_load_fn(sc, args.data, args.sampling_ratio, args.seed) + + # Split the vectors + split_vecs = vecs.map(lambda x: np.split(x, 2)) + + return split_vecs + + +def train_coarse(sc, split_vecs, V, seed=None): + """ + Perform KMeans on each split of the data with V clusters each. + """ + + # Cluster first split + first = split_vecs.map(lambda x: x[0]) + first.cache() + print 'Total training set size: %d' % first.count() + print 'Starting training coarse quantizer...' + C0 = KMeans.train(first, V, initializationMode='random', maxIterations=10, seed=seed) + print '... done training coarse quantizer.' + first.unpersist() + + # Cluster second split + second = split_vecs.map(lambda x: x[1]) + second.cache() + print 'Starting training coarse quantizer...' + C1 = KMeans.train(second, V, initializationMode='random', maxIterations=10, seed=seed) + print '... done training coarse quantizer.' + second.unpersist() + + return np.vstack(C0.clusterCenters), np.vstack(C1.clusterCenters) + + +def train_rotations(sc, split_vecs, M, Cs): + """ + For compute rotations for each split of the data using given coarse quantizers. + """ + + Rs = [] + mus = [] + counts = [] + for split in xrange(2): + + print 'Starting rotation fitting for split %d' % split + + # Get the data for this split + data = split_vecs.map(lambda x: x[split]) + + # Get kmeans model + model = KMeansModel(Cs[split]) + + R, mu, count = compute_local_rotations(sc, data, model, M / 2) + Rs.append(R) + mus.append(mu) + counts.append(count) + + return Rs, mus, counts + + +def accumulate_covariance_estimators(sc, data, model): + """ + Analogous function to function of the same name in lopq.model. + + :param SparkContext sc: + a SparkContext + :param RDD data: + an RDD of numpy arrays + :param KMeansModel model: + a KMeansModel instance for which to fit local rotations + """ + + def get_residual(x): + cluster = model.predict(x) + centroid = model.clusterCenters[cluster] + residual = x - centroid + return (cluster, residual) + + def seq_op(acc, x): + acc += np.outer(x, x) + return acc + + # Compute (assignment, residual) k/v pairs + residuals = data.map(get_residual) + residuals.cache() + + # Collect counts and mean residuals + count = residuals.countByKey() + mu = residuals.reduceByKey(add).collectAsMap() + + # Extract the dimension of the data + D = len(mu.values()[0]) + + # Collect accumulated outer products + A = residuals.aggregateByKey(np.zeros((D, D)), seq_op, add).collectAsMap() + + residuals.unpersist() + + return A, mu, count + + +def dict_to_ndarray(d, N): + """ + Helper for collating a dict with int keys into an ndarray. The value for a key + becomes the value at the corresponding index in the ndarray and indices missing + from the dict become zero ndarrays of the same dimension. + + :param dict d: + a dict of (int, ndarray) or (int, number) key/values + :param int N: + the size of the first dimension of the new ndarray (the rest of the dimensions + are determined by the shape of elements in d) + """ + + el = d.values()[0] + if type(el) == np.ndarray: + value_shape = el.shape + arr = np.zeros((N,) + value_shape) + else: + arr = np.zeros(N) + + for i in d: + arr[i] = d[i] + return arr + + +def compute_local_rotations(sc, data, model, num_buckets): + """ + Analogous to the function of the same name in lopq.model. + + :param SparkContext sc: + a SparkContext + :param RDD data: + an RDD of numpy arrays + :param KMeansModel model: + a KMeansModel instance for which to fit local rotations + :param int num_buckets: + the number of subvectors over which to balance residual variance + """ + # Get estimators + A, mu, count = accumulate_covariance_estimators(sc, data, model) + + # Format as ndarrays + V = len(model.centers) + A = dict_to_ndarray(A, V) + mu = dict_to_ndarray(mu, V) + count = dict_to_ndarray(count, V) + + # Compute params + R, mu = compute_rotations_from_accumulators(A, mu, count, num_buckets) + + return R, mu, count + + +def train_subquantizers(sc, split_vecs, M, subquantizer_clusters, model, seed=None): + """ + Project each data point into it's local space and compute subquantizers by clustering + each fine split of the locally projected data. + """ + b = sc.broadcast(model) + + def project_local(x): + x = np.concatenate(x) + coarse = b.value.predict_coarse(x) + return b.value.project(x, coarse) + + projected = split_vecs.map(project_local) + + # Split the vectors into the subvectors + split_vecs = projected.map(lambda x: np.split(x, M)) + split_vecs.cache() + + subquantizers = [] + for split in xrange(M): + data = split_vecs.map(lambda x: x[split]) + data.cache() + sub = KMeans.train(data, subquantizer_clusters, initializationMode='random', maxIterations=10, seed=seed) + data.unpersist() + subquantizers.append(np.vstack(sub.clusterCenters)) + + return (subquantizers[:len(subquantizers) / 2], subquantizers[len(subquantizers) / 2:]) + + +def save_hdfs_pickle(m, pkl_path): + """ + Given a python object and a path on hdfs, save the object as a pickle file locally and copy the file + to the hdfs path. + """ + print 'Saving pickle to temp file...' + f = NamedTemporaryFile(delete=False) + pkl.dump(m, f, -1) + f.close() + + print 'Copying pickle file to hdfs...' + copy_to_hdfs(f, pkl_path) + os.remove(f.name) + + +def save_hdfs_proto(m, proto_path): + """ + Given an LOPQModel object and a path on hdfs, save the model parameters as a protobuf file locally and + copy the file to the hdfs path. + """ + print 'Saving protobuf to temp file...' + f = NamedTemporaryFile(delete=False) + m.export_proto(f) + f.close() + + print 'Copying proto file to hdfs...' + copy_to_hdfs(f, proto_path) + os.remove(f.name) + + +def copy_to_hdfs(f, hdfs_path): + subprocess.call(['hadoop', 'fs', '-copyFromLocal', f.name, hdfs_path]) + + +def validate_arguments(args, model): + """ + Check provided command line arguments to ensure they are coherent. Provide feedback for potential errors. + """ + + # Parse steps + args.steps = set(map(int, args.steps.split(','))) + + # Check that the steps make sense + if STEP_ROTATION not in args.steps and len(args.steps) == 2: + print 'Training steps invalid' + sys.exit(1) + + # Find parameters and warn of possibly unintentional discrepancies + if args.V is None: + if model is not None: + args.V = model.V + print 'Parameter V not specified: using V=%d from provided model.' % model.V + else: + print 'Parameter V not specified and no existing model provided. Exiting.' + sys.exit(1) + else: + if model is not None and model.V != args.V: + if STEP_COARSE in args.steps: + print 'Parameter V differs between command line argument and provided model: ' + \ + 'coarse quantizers will be trained with V=%d' % args.V + else: + print 'Parameter V differs between command line argument and provided model: ' + \ + 'coarse quantizers must be retrained or this discrepancy corrected. Exiting.' + sys.exit(1) + + if STEP_ROTATION in args.steps or STEP_SUBQUANT in args.steps: + if args.M is None: + if model is not None: + args.M = model.M + print 'Parameter M not specified: using M=%d from provided model.' % model.M + else: + print 'Parameter M not specified and no existing model provided. Exiting.' + sys.exit(1) + else: + if model is not None and model.M != args.M: + if STEP_ROTATION in args.steps: + print 'Parameter M differs between command line argument and provided model: ' + \ + 'model will be trained with M=%d' % args.M + else: + print 'Parameter M differs between command line argument and provided model: ' + \ + 'rotations must be retrained or this discrepancy corrected. Exiting.' + sys.exit(1) + + if STEP_ROTATION in args.steps: + if STEP_COARSE not in args.steps and (model is None or model.Cs is None): + print 'Cannot train rotations without coarse quantizers. Either train coarse quantizers or provide an existing model. Exiting.' + sys.exit(1) + + if STEP_SUBQUANT in args.steps: + if STEP_COARSE not in args.steps and (model is None or model.Cs is None): + print 'Cannot train subquantizers without coarse quantizers. Either train coarse quantizers or provide an existing model. Exiting.' + sys.exit(1) + if STEP_ROTATION not in args.steps and (model is None or model.Rs is None or model.mus is None): + print 'Cannot train subquantizers without rotations. Either train rotations or provide an existing model. Exiting.' + sys.exit(1) + + return args + +if __name__ == "__main__": + from argparse import ArgumentParser + parser = ArgumentParser() + + # Data handling parameters + parser.add_argument('--data', dest='data', type=str, required=True, help='hdfs path to input data') + parser.add_argument('--data_udf', dest='data_udf', type=str, default=None, help='module name from which to load a data loading UDF') + parser.add_argument('--seed', dest='seed', type=int, default=None, help='optional random seed') + parser.add_argument('--sampling_ratio', dest='sampling_ratio', type=float, default=1.0, help='proportion of data to sample for training') + parser.add_argument('--subquantizer_sampling_ratio', dest='subquantizer_sampling_ratio', type=float, default=1.0, + help='proportion of data to subsample for subquantizer training') + + # Model parameters + existing_model_group = parser.add_mutually_exclusive_group() + existing_model_group.add_argument('--existing_model_pkl', dest='existing_model_pkl', type=str, default=None, + help='a pickled LOPQModel from which to extract existing parameters') + existing_model_group.add_argument('--existing_model_proto', dest='existing_model_proto', type=str, default=None, + help='a protobuf of existing model parameters') + + # Model hyperparameters + parser.add_argument('--V', dest='V', type=int, default=None, help='number of coarse clusters') + parser.add_argument('--M', dest='M', type=int, default=None, help='total number of subquantizers') + parser.add_argument('--subquantizer_clusters', dest='subquantizer_clusters', type=int, default=256, help='number of subquantizer clusters') + + # Training and output directives + parser.add_argument('--steps', dest='steps', type=str, default='0,1,2', + help='comma-separated list of integers indicating which steps of training to perform') + parser.add_argument('--model_pkl', dest='model_pkl', type=str, default=None, help='hdfs path to save pickle file of resulting LOPQModel') + parser.add_argument('--model_proto', dest='model_proto', type=str, default=None, help='hdfs path to save protobuf file of resulting model parameters') + + args = parser.parse_args() + + # Check that some output format was provided + if args.model_pkl is None and args.model_proto is None: + parser.error('at least one of --model_pkl and --model_proto is required') + + # Load existing model if provided + model = None + if args.existing_model_pkl: + model = pkl.load(open(args.existing_model_pkl)) + elif args.existing_model_proto: + model = LOPQModel.load_proto(args.existing_model_proto) + + args = validate_arguments(args, model) + + # Build descriptive app name + get_step_name = lambda x: {STEP_COARSE: 'coarse', STEP_ROTATION: 'rotations', STEP_SUBQUANT: 'subquantizers'}.get(x, None) + steps_str = ', '.join(filter(lambda x: x is not None, map(get_step_name, sorted(args.steps)))) + APP_NAME = 'LOPQ{V=%d,M=%d}; training %s' % (args.V, args.M, steps_str) + + sc = SparkContext(appName=APP_NAME) + + # Load UDF module if provided and load training data RDD + if args.data_udf: + udf_module = __import__(args.data_udf, fromlist=['udf']) + load_udf = udf_module.udf + data = load_data(sc, args, data_load_fn=load_udf) + else: + data = load_data(sc, args) + + # Initialize parameters + Cs = Rs = mus = subs = None + + # Get coarse quantizers + if STEP_COARSE in args.steps: + Cs = train_coarse(sc, data, args.V, seed=args.seed) + else: + Cs = model.Cs + + # Get rotations + if STEP_ROTATION in args.steps: + Rs, mus, counts = train_rotations(sc, data, args.M, Cs) + else: + Rs = model.Rs + mus = model.mus + + # Get subquantizers + if STEP_SUBQUANT in args.steps: + model = LOPQModel(V=args.V, M=args.M, subquantizer_clusters=args.subquantizer_clusters, parameters=(Cs, Rs, mus, None)) + + if args.subquantizer_sampling_ratio != 1.0: + data = data.sample(False, args.subquantizer_sampling_ratio, args.seed) + + subs = train_subquantizers(sc, data, args.M, args.subquantizer_clusters, model, seed=args.seed) + + # Final output model + model = LOPQModel(V=args.V, M=args.M, subquantizer_clusters=args.subquantizer_clusters, parameters=(Cs, Rs, mus, subs)) + + if args.model_pkl: + save_hdfs_pickle(model, args.model_pkl) + if args.model_proto: + save_hdfs_proto(model, args.model_proto) + + sc.stop() diff --git a/spark/train_pca.py b/spark/train_pca.py new file mode 100644 index 0000000..5eda69f --- /dev/null +++ b/spark/train_pca.py @@ -0,0 +1,117 @@ +# Copyright 2015, Yahoo Inc. +# Licensed under the terms of the Apache License, Version 2.0. See the LICENSE file associated with the project for terms. +from pyspark.context import SparkContext + +import numpy as np +import base64 +import cPickle as pkl +from tempfile import NamedTemporaryFile +import os +import subprocess +from operator import add + + +def default_data_loading(sc, data_path, sampling_ratio, seed): + """ + This function loads training data from a text file, sampling it by the provided + ratio and random seed, and interprets each line as a tab-separated (id, data) pair + where 'data' is assumed to be a base64-encoded pickled numpy array. The ids are discarded. + The data is returned as an RDD of numpy arrays. + """ + # Compute the number of cores in our cluster - used below to heuristically set the number of partitions + total_cores = int(sc._conf.get('spark.executor.instances')) * int(sc._conf.get('spark.executor.cores')) + + # Load and sample down the dataset + d = sc.textFile(data_path, total_cores * 3).sample(False, sampling_ratio, seed) + + # The data is (id, vector) tab-delimited pairs where each vector is + # a base64-encoded pickled numpy array + deserialize_vec = lambda s: pkl.loads(base64.decodestring(s.split('\t')[1])) + vecs = d.map(deserialize_vec) + + return vecs + + +def main(sc, args, data_load_fn=default_data_loading): + + def seqOp(a, b): + a += np.outer(b, b) + return a + + def combOp(a, b): + a += b + return a + + # Load data + d = data_load_fn(sc, args.data, args.sampling_ratio, args.seed) + d.cache() + + # Determine the data dimension + D = len(d.first()) + + # Count data points + count = d.count() + mu = d.aggregate(np.zeros(D), add, add) + mu = mu / float(count) + + # Compute covariance estimator + summed_covar = d.treeAggregate(np.zeros((D, D)), seqOp, combOp, depth=2) + + A = summed_covar / (count - 1) - np.outer(mu, mu) + E, P = np.linalg.eig(A) + + params = { + 'mu': mu, + 'P': P, + 'E': E, + 'A': A, + 'c': count + } + + save_hdfs_pickle(params, args.output) + + +def save_hdfs_pickle(m, pkl_path): + """ + Given a python object and a path on hdfs, save the object as a pickle file locally and copy the file + to the hdfs path. + """ + print 'Saving pickle to temp file...' + f = NamedTemporaryFile(delete=False) + pkl.dump(m, f, -1) + f.close() + + print 'Copying pickle file to hdfs...' + copy_to_hdfs(f, pkl_path) + os.remove(f.name) + + +def copy_to_hdfs(f, hdfs_path): + subprocess.call(['hadoop', 'fs', '-copyFromLocal', f.name, hdfs_path]) + + +if __name__ == '__main__': + from argparse import ArgumentParser + parser = ArgumentParser() + + # Data handling parameters + parser.add_argument('--data', dest='data', type=str, required=True, help='hdfs path to input data') + parser.add_argument('--data_udf', dest='data_udf', type=str, default=None, help='module name from which to load a data loading UDF') + parser.add_argument('--seed', dest='seed', type=int, default=None, help='optional random seed') + parser.add_argument('--sampling_ratio', dest='sampling_ratio', type=float, default=1.0, help='proportion of data to sample for training') + + parser.add_argument('--output', dest='output', type=str, default=None, help='hdfs path to output pickle file of parameters') + + args = parser.parse_args() + + sc = SparkContext(appName='PCA') + + # Load UDF module if provided + if args.data_udf: + udf_module = __import__(args.data_udf, fromlist=['udf']) + load_udf = udf_module.udf + main(sc, args, data_load_fn=load_udf) + else: + main(sc, args) + + sc.stop()