From 65d8630c782ff123e9db99059ec722f49dd39bcc Mon Sep 17 00:00:00 2001
From: PrateethChagari <prateethchagari@gmail.com>
Date: Wed, 18 Dec 2019 19:25:52 -0600
Subject: [PATCH] Add files for search engine

---
 CommonUtilities.py       |   4 +
 IndexBuilder.py          |  55 ++++++++
 README.md                |  23 ++++
 Search.py                | 264 ++++++++++++++++++++++++++++++++++++
 SearchEngine.py          |  58 ++++++++
 SearchResult.py          |  21 +++
 corpus_processor.py      |  99 ++++++++++++++
 data.py                  |  45 +++++++
 index.html               | 105 +++++++++++++++
 label_finder.py          |  81 +++++++++++
 label_ranker.py          | 233 ++++++++++++++++++++++++++++++++
 label_topic.py           | 129 ++++++++++++++++++
 neuralnetregressor.py    |  71 ++++++++++
 passenger_wsgi.py        |   1 +
 plsaNeuralNetModel.model | Bin 0 -> 64297 bytes
 pmi.py                   | 111 +++++++++++++++
 text.py                  |  74 ++++++++++
 train_academicdata.py    | 284 +++++++++++++++++++++++++++++++++++++++
 18 files changed, 1658 insertions(+)
 create mode 100644 CommonUtilities.py
 create mode 100644 IndexBuilder.py
 create mode 100644 Search.py
 create mode 100644 SearchEngine.py
 create mode 100644 SearchResult.py
 create mode 100644 corpus_processor.py
 create mode 100644 data.py
 create mode 100644 index.html
 create mode 100644 label_finder.py
 create mode 100644 label_ranker.py
 create mode 100644 label_topic.py
 create mode 100644 neuralnetregressor.py
 create mode 100644 passenger_wsgi.py
 create mode 100644 plsaNeuralNetModel.model
 create mode 100644 pmi.py
 create mode 100644 text.py
 create mode 100644 train_academicdata.py

diff --git a/CommonUtilities.py b/CommonUtilities.py
new file mode 100644
index 0000000..400c41e
--- /dev/null
+++ b/CommonUtilities.py
@@ -0,0 +1,4 @@
+class CommonUtilities:
+    @staticmethod
+    def obj_dict(obj):
+        return obj.__dict__
\ No newline at end of file
diff --git a/IndexBuilder.py b/IndexBuilder.py
new file mode 100644
index 0000000..5536a29
--- /dev/null
+++ b/IndexBuilder.py
@@ -0,0 +1,55 @@
+import os, os.path
+from whoosh.index import create_in
+from whoosh.fields import *
+import xml.dom.minidom as dom
+import xml.etree.ElementTree as ET
+from whoosh.qparser import MultifieldParser, OrGroup, QueryParser
+
+class IndexBuilder:
+    def __init__(self):
+        self.schema = Schema(paper=ID(stored=True), abstract=TEXT(stored=True), title=TEXT(stored=True), introduction=TEXT(stored=True))
+        self.ix = create_in("index", schema)
+        self.path = "papers_to_index/"
+    
+    def build(self):
+        file_list = []
+        for (dirpath, dirnames, filenames) in os.walk(path):
+            file_list.extend(filenames)
+        
+        writer = ix.writer()
+        for file in file_list:
+            fileNew = dirpath+file
+            count += 1
+            paperId = os.path.splitext(file)
+            f = open(fileNew, encoding="utf8")
+            for line in f:
+                xmltree = dom.parseString(line)
+                if (xmltree.getElementsByTagName('abstract')):
+                    if  xmltree.getElementsByTagName('abstract')[0].firstChild is None:
+                        abstractField = ""
+                    else:
+                        abstractField = xmltree.getElementsByTagName('abstract')[0].firstChild.nodeValue
+                
+                elif (xmltree.getElementsByTagName('title')):
+                    if xmltree.getElementsByTagName('title')[0].firstChild is None:
+                        titleField = ""
+                    else:
+                        titleField = xmltree.getElementsByTagName('title')[0].firstChild.nodeValue
+
+                elif (xmltree.getElementsByTagName('introduction')):
+                    if (xmltree.getElementsByTagName('introduction')[0].firstChild is None):  
+                        introductionField = ""
+                    else:
+                        introductionField = xmltree.getElementsByTagName('introduction')[0].firstChild.nodeValue
+                
+            writer.add_document(paper = paperId, abstract = abstractField, title = titleField, introduction = introductionField)
+
+        writer.commit()
+        writer.close()
+
+def build():
+    indexbuilder = IndexBuilder()
+    indexbuilder.build()
+
+if __name__== "__main__":
+    build()
diff --git a/README.md b/README.md
index cff8103..94f64c5 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,25 @@
 # cs510-project
 
+Before running the software, here are a few points about the data files
+1. We have not provided the dataset and corresponding datafiles explicitly due to size constraints
+2. The dataset (academic papers) need to be placed in a folder ‘papers_to_index’
+3. Each document is expected to be an xml file with the following fields:
+        Paper  Abstract	Title	Introduction
+
+4. Some other files are expected to be present before the code can run successfully:
+5. docs.json: a json file consisting of all the documents. Each line represents one document and contains, at the least, keyPhrases, paperAbstract, title, introduction, docno (document number), numKeyReferences (number of key references), numCitedBy and numKeyCitations. These fields are used for generating features for training our neural network.
+6. train_queries.json: Each line is of the form {"qid": "the query id", "query": "the query string", "ana": {the annotated entity id and frequency}} 
+7. train_queries_qrel: relevance judgement for the training queries. 
+8. The above 3 files are from Freebase and are similar to the files used in the searchengine assignment
+9. supervisedTrain.txt: The file which is generated on training the neural net with the training data. Contains the feature values.
+
+The code is provided at the github link given at the beginning of this report
+The UI can be viewed directly by accessing the link at the beginning of the report.
+To run the software, use the following steps:
+1. Assuming that the data is placed as explained above:
+2. To build the index for the dataset, run `python3 IndexBuilder.py`. The index will be built from the documents present in ‘papers_to_index’ in the current directory; and creates the index in a folder ‘index’. Create an empty folder ‘index’ if the code automatically doesn’t create it.
+3. Once the index is built (might take a while), we want to generate our features for training the neural network. Ensure that the necessary files are placed. The files are similar to the files provided for search engine assignment and named ‘docs.json’, ‘train_queries.json’ and ‘train_queries_qrel’. The features will be created in a file ‘supervisedTrain.txt’ when the following command is executed: ‘python3 train_academicdata.py’
+4. Execute `python3 neuralnetregressor.py` to train the model on the features generated and dumps the model into a file `plsaNeuralNetModel.model`
+5. After the neural net is trained, the software is ready to be run. Execute `python3 SearchEngine.py` which is currently configured to run on localhost.
+6. Open the index.html file on the browser which is configured to connect to the localhost.
+7. Search for the query. Topic labelling will run every time a query is searched, and appropriate results are returned
diff --git a/Search.py b/Search.py
new file mode 100644
index 0000000..d5edd73
--- /dev/null
+++ b/Search.py
@@ -0,0 +1,264 @@
+from whoosh.index import create_in, open_dir
+from whoosh.fields import *
+from whoosh.qparser import MultifieldParser, OrGroup, QueryParser
+from whoosh import scoring
+from joblib import load
+from label_topic import TopicLabels
+from collections import defaultdict
+
+class Myclass:
+    def __init__(self):
+        self.abBm25 = 0.0
+        self.introBm25 = 0.0
+        self.tiBm25 = 0.0
+        self.abTf = 0.0
+        self.introTf = 0.0
+        self.tiTf = 0.0
+        self.abPl2 = 0.0
+        self.introPl2 = 0.0
+        self.tiPl2 = 0.0
+        self.abDf = 0.0
+        self.introDf = 0.0
+        self.tiDf = 0.0
+        self.abstarct = ""
+        self.introduction = ""
+        self.title = ""
+        self.paper = ""
+        self.neuralScore = 0.0
+
+class Search:
+   
+    def getResultDocs(self, query):
+        docs = []
+        indexDir = open_dir("index/")
+        og = OrGroup.factory(0.9)
+        indexSearcher = indexDir.searcher()
+
+        # Parser for multiple fields
+        abQueryParser = QueryParser("abstract", indexSearcher.schema, group=og)
+        abQueryObject = abQueryParser.parse(query)
+        introQueryParser = QueryParser("introduction", indexSearcher.schema, group=og)
+        introQueryObject = introQueryParser.parse(query)
+        tiQueryParser = QueryParser("title", indexSearcher.schema, group=og)
+        tiQueryObject = tiQueryParser.parse(query)
+
+        # Extracting features
+        features = defaultdict()
+        abResults = indexSearcher.search(abQueryObject, limit = 300)
+        introResults = indexSearcher.search(introQueryObject, limit = 300)
+        tiResults = indexSearcher.search(tiQueryObject, limit = 300)
+        for result in abResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].abBm25 = result.score
+            else:
+                temp = Myclass()
+                temp.abBm25 = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp
+        
+        for result in introResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].introBm25 = result.score
+            else:
+                temp = Myclass()
+                temp.pabm25 = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp
+
+        for result in tiResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].tiBm25 = result.score
+            else:
+                temp = Myclass()
+                temp.tiBm25 = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp
+        
+        
+        w = scoring.TF_IDF()
+        idfIndexSearcher = indexDir.searcher(weighting=w)
+        abIdfResults = idfIndexSearcher.search(abQueryObject, limit = 300)
+        introIdfResults = idfIndexSearcher.search(introQueryObject, limit = 300)
+        tiIdfResults = idfIndexSearcher.search(tiQueryObject, limit = 300)
+        for result in abIdfResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].abTf = result.score
+            else:
+                temp = Myclass()
+                temp.abTf = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp
+        
+        for result in introIdfResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].introTf = result.score
+            else:
+                temp = Myclass()
+                temp.introTf = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp
+
+        for result in tiIdfResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].tiTf = result.score
+            else:
+                temp = Myclass()
+                temp.tiTf = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp
+
+        w = scoring.PL2()
+        plIndexSearcher = indexDir.searcher(weighting=w)
+        abPlResults = plIndexSearcher.search(abQueryObject, limit = 300)
+        introPlResults = plIndexSearcher.search(introQueryObject, limit = 300)
+        tiPlResults = plIndexSearcher.search(tiQueryObject, limit = 300)
+        for result in abPlResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].abPl2 = result.score
+            else:
+                temp = Myclass()
+                temp.abPl2 = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp
+        
+        for result in introPlResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].introPl2 = result.score
+            else:
+                temp = Myclass()
+                temp.introPl2 = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp
+
+        for result in tiPlResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].tiPl2 = result.score
+            else:
+                temp = Myclass()
+                temp.tiPl2 = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp   
+
+        w = scoring.DFree()
+        dfIndexSearcher = indexDir.searcher(weighting=w)
+        abDfResults = dfIndexSearcher.search(abQueryObject, limit = 300)
+        introDfResults = dfIndexSearcher.search(introQueryObject, limit = 300)
+        tiDfResults = dfIndexSearcher.search(tiQueryObject, limit = 300)
+        for result in abDfResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].abDf = result.score
+            else:
+                temp = Myclass()
+                temp.abDf = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp
+        
+        for result in introDfResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].introDf = result.score
+            else:
+                temp = Myclass()
+                temp.introDf = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp
+
+        for result in tiDfResults:
+            paper = result["paper"]
+            if (paper in features):
+                features[paper].tiDf = result.score
+            else:
+                temp = Myclass()
+                temp.tiDf = result.score
+                temp.abstarct = result["abstract"]
+                temp.title = result["title"]
+                temp.introduction = result["introduction"]
+                features[paper] = temp
+
+        model = load('plsaNeuralNetModel.model')
+        for key, val in features.items():
+            ab = set(val.abstarct.split())
+            intro = set(val.introduction.split())
+            ti = set(val.title.split())
+            q = set(query.split())
+            val.neuralScore = model.predict([[val.abBm25, val.introBm25, val.tiBm25, \
+                                            val.abTf, val.introTf, val.tiTf, \
+                                            val.abPl2, val.introPl2, val.tiPl2, \
+                                            val.abDf, val.introDf, val.tiDf, \
+                                            len(ab), len(intro), len(ti), len(q), \
+                                            len(q.intersection(ab)), len(q.intersection(intro)), len(q.intersection(ti))]])[0]
+        
+        
+        
+        
+        
+        results = dict(sorted(features.items(), key=lambda x: x[1].neuralScore, reverse=True)[:100])
+        
+        searchResults = list()
+
+        for key, val in results.items():
+            contents = dict()
+            contents["paper"] = key
+            contents["abstract"] = val.abstarct
+            
+            docs.append(val.abstarct)
+            contents["title"] = val.title
+            contents["introduction"] = val.introduction
+            contents["topics"] = ""
+            searchResults.append(contents)
+
+        doc_topics, labels = TopicLabels.get_topic_labels(docs,
+                                n_topics=10,
+                                n_top_words=20,
+                                preprocessing_steps=['tag'],
+                                n_cand_labels=100,
+                                label_min_df=5,
+                                label_tags=['NN,NN', 'JJ,NN'],
+                                n_labels=10,
+                                lda_random_state=12345,
+                                lda_n_iter=400)
+        
+        topic_labels = dict()
+        for key, val in doc_topics.items():
+            for i, label in enumerate(labels):               
+                if i == val:
+                    topic_labels[key] = label
+
+        for i, result in enumerate(searchResults):
+            # TO-DO: format the output, remove stemming
+            result["topics"] = str(topic_labels[i])
+
+        return searchResults
+
diff --git a/SearchEngine.py b/SearchEngine.py
new file mode 100644
index 0000000..9fce754
--- /dev/null
+++ b/SearchEngine.py
@@ -0,0 +1,58 @@
+from flask import Flask, jsonify, Response, send_file, request
+from flask_cors import CORS
+from Search import Search
+from SearchResult import SearchResult
+from CommonUtilities import CommonUtilities
+from logging import Formatter, FileHandler
+import json
+import logging
+
+
+app = Flask(__name__)
+CORS(app)
+logger = None
+
+
+@app.route('/')
+def index():
+    return send_file('index.html')
+
+@app.route('/search/', methods=['GET', 'POST'])
+def get_results():
+    global search
+    json_data = request.get_data(as_text=True)
+    data = json.loads(json_data)
+    query = data['query']
+    search = Search()
+    results = search.getResultDocs(query)
+    queryResult = list()
+    maxResultCount = int(data['page_count']) * 10
+    for result in results[maxResultCount - 10 : maxResultCount]:
+        queryResult.append(SearchResult(result, query))
+    
+    response = Response(response=json.dumps(queryResult, default=CommonUtilities.obj_dict), status=200, mimetype='application/json')
+    response.headers.add('Access-Control-Allow-Origin', '*')
+    return response
+
+@app.route('/relevance/', methods=["POST"])
+def relevance():
+    json_data = request.get_data(as_text=True)
+    data = json.loads(json_data)
+    # logger.info(json_data)
+    response = Response(response=json.dumps("OK", default=CommonUtilities.obj_dict), status=200, mimetype='application/json')
+    return response
+
+def setLogger():
+    global logger
+    logger = logging.getLogger('relevance')
+    logger.setLevel(logging.INFO)
+    ch = logging.FileHandler('Relevance.json')
+    ch.setLevel(logging.INFO)
+    formatter = logging.Formatter('%(message)s')
+    ch.setFormatter(formatter)
+    logger.addHandler(ch)
+
+if __name__ == '__main__':
+    search = Search()
+    setLogger()
+    app.run(debug=True)
\ No newline at end of file
diff --git a/SearchResult.py b/SearchResult.py
new file mode 100644
index 0000000..a25e2c8
--- /dev/null
+++ b/SearchResult.py
@@ -0,0 +1,21 @@
+class SearchResult:
+    def __init__(self, result, query):
+        self.id = 'paperid' + str(result['paper'][0][:8].encode('utf-8')).replace(" ", "")
+        self.title = result['title']
+        self.abstract = self.highlightQuery(result['abstract'], query)
+        self.topics = result['topics']
+        self.url = self.getUrlFormat(result['paper'][0])
+    
+    def getUrlFormat(self, file):
+        return "http://www.aclweb.org/anthology/" + file[0:8] + ".pdf"
+
+    def highlightQuery(self, text, query):
+        keywords = [w.lower() for w in query.split()]
+        words = text.split()
+
+        for i, word in enumerate(words):
+            for keyword in keywords:
+                if keyword in word.lower():
+                    words[i] = '<b>' + word + '</b>'
+                    break
+        return ' '.join(words).rstrip("\"").rstrip(".") + "."
\ No newline at end of file
diff --git a/corpus_processor.py b/corpus_processor.py
new file mode 100644
index 0000000..97db47b
--- /dev/null
+++ b/corpus_processor.py
@@ -0,0 +1,99 @@
+import nltk
+from toolz.functoolz import partial
+from nltk.stem.porter import PorterStemmer
+
+
+class CorpusBaseProcessor(object):
+    """
+    Class that processes a corpus
+    """
+    def transform(self, docs):
+        """
+        Parameter:
+        -----------
+        docs: list of (string|list of tokens)
+            input corpus
+        
+        Return:
+        ----------
+        list of (string|list of tokens):
+            transformed corpus
+        """
+        raise NotImplemented
+
+
+class CorpusWordLengthFilter(CorpusBaseProcessor):
+    def __init__(self, minlen=2, maxlen=35):
+        self._min = minlen
+        self._max = maxlen
+
+    def transform(self, docs):
+        """
+        Parameters:
+        ----------
+        docs: list of list of str
+            the tokenized corpus
+        """
+        assert isinstance(docs[0], list)
+        valid_length = (lambda word:
+                        len(word) >= self._min and
+                        len(word) <= self._max)
+        filter_tokens = partial(filter, valid_length)
+        return list(map(filter_tokens, docs))
+    
+
+porter_stemmer = PorterStemmer()
+
+
+class CorpusStemmer(CorpusBaseProcessor):
+    def __init__(self, stem_func=porter_stemmer.stem):
+        """
+        Parameter:
+        --------------
+        stem_func: function that accepts one token and stem it
+        """
+        self._stem_func = stem_func
+
+    def transform(self, docs):
+        """
+        Parameter:
+        -------------
+        docs: list of list of str
+            the documents
+
+        Return:
+        -------------
+        list of list of str: the stemmed corpus
+        """
+        assert isinstance(docs[0], list)
+        stem_tokens = partial(map, self._stem_func)
+        docs = [[porter_stemmer.stem(token) for token in doc] for doc in docs]
+        return docs
+
+
+class CorpusPOSTagger(CorpusBaseProcessor):
+    def __init__(self, pos_tag_func=nltk.pos_tag):
+        """
+        Parameter:
+        --------------
+        pos_tag_func: pos_tag function that accepts list of tokens
+            and POS tag them
+        """
+        self._pos_tag_func = pos_tag_func
+
+    def transform(self, docs):
+        """
+        Parameter:
+        -------------
+        docs: list of list of str
+            the documents
+
+        Return:
+        -------------
+        list of list of str: the tagged corpus
+        """
+        assert isinstance(docs[0], list)
+        import nltk
+        nltk.download('averaged_perceptron_tagger')
+        docs = [nltk.pos_tag(doc) for doc in docs]
+        return docs
diff --git a/data.py b/data.py
new file mode 100644
index 0000000..6eef8d0
--- /dev/null
+++ b/data.py
@@ -0,0 +1,45 @@
+import os
+import nltk
+import itertools
+import codecs
+from toolz.functoolz import compose
+import _pickle as pickle
+
+CURDIR = os.path.dirname(os.path.realpath(__file__))
+
+
+def load_line_corpus(path, tokenize=True):
+    import nltk
+    nltk.download('punkt')
+    docs = []
+    with codecs.open(path, "r", "utf8") as f:
+        for l in f:
+            if tokenize:
+                sents = nltk.sent_tokenize(l.strip().lower())
+                docs.append(list(itertools.chain(*map(
+                    nltk.word_tokenize, sents))))
+            else:
+                docs.append(l.strip())
+    return docs
+
+
+def load_nips(years=None, raw=False):
+    # load data
+    if not years:
+        years = xrange(2008, 2015)
+    files = ['nips-{}.dat'.format(year)
+             for year in years]
+
+    docs = []
+    for f in files:
+        docs += load_line_corpus('{}/datasets/{}'.format(CURDIR, f),
+                                 tokenize=(not raw))
+        
+    return docs                
+
+
+def load_lemur_stopwords():
+    with codecs.open(CURDIR + '/datasets/lemur-stopwords.txt', 
+                     'r', 'utf8') as f:
+        return map(lambda s: s.strip(),
+                   f.readlines())
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..f230000
--- /dev/null
+++ b/index.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<title>Search Engine for Research Papers</title>
+<meta charset="utf-8">
+<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
+<script>
+    var page_count
+    var ip_address
+    var URL="http://127.0.0.1:5000/"
+    
+    function relevant(relevance, paperId) {
+        $('input[name="'+paperId+'"]').attr('disabled', 'disabled');
+        $.ajax({
+        type: 'POST',
+        url: URL + "relevance/",  //add endpoint API
+        data: JSON.stringify({query: query, relevance: relevance, id: paperId, ip_address: ip_address}),
+        });
+        
+    }
+   
+    function search(){
+       
+        $.ajax({
+            type: "POST",
+            url: URL + "search/",   
+            data: JSON.stringify({query: query, page_count: page_count}),
+            contentType: "applicaton/json; charset=utf-8",
+            success: function(results){
+                var resultLength = results.length
+                
+                for (var i=0; i < resultLength; ++i)
+                {
+                    var paperId = results[i].id
+                    $(".searchResults").append("<br>");
+                    $(".searchResults").append("<table>");
+                    $(".searchResults").append("<tr><td>");
+                    $(".searchResults").append("<p style=\"font-size:18px;\">" + "<a target=\"_blank\" href=\"" + results[i].url + "\">"+results[i].title+"</a>" + "</p>");
+                    $(".searchResults").append("</td></tr>");
+                    $(".searchResults").append("<tr><td>");
+                    $(".searchResults").append("<input type=\"radio\" name=\""+paperId+"\" onclick=\"relevant(true, name)\"><label> Relevant</label>&nbsp")
+                    $(".searchResults").append("&nbsp<input type=\"radio\" name=\""+paperId+"\" onclick=\"relevant(false, name)\"><label> Non-Relevant</label>") 
+                    $(".searchResults").append("</td></tr>");
+                    $(".searchResults").append("<tr><td>");
+                    $(".searchResults").append("<p style=\"border-style: groove;font-size:14px;\"> <b>Topic labels: </b>"+ results[i].topics +"</p>");
+                    $(".searchResults").append("</td></tr>");
+                    $(".searchResults").append("<pre style=\"white-space: pre-wrap;\">"+results[i].abstract+"</pre>")
+                    $(".searchResults").append("</td></tr>");
+                    $(".searchResults").append("</table>");
+                    
+                    
+                }
+                if(resultLength == 0) {
+                    $(".searchResults").append("<br>");
+                    $(".searchResults").append("<p style=\"font-size:18px; color:Red\"> There are no (more) results to show for the given query </p>");
+                }
+                if (resultLength < 10) 
+                    $("#more").hide();
+                else 
+                    $("#more").show();
+    
+            }
+        });
+        
+        function callback(response){
+            console.log(response);
+        }
+    }
+    $(document).ready(function(){
+            $("#searchButton").click(function(){
+                    $(".searchResults").empty()
+                    page_count = 1
+                    query = document.getElementById("querytext").value;
+                    search();
+            });
+            $("#more").click(function(){
+                    page_count++
+                    query = document.getElementById("querytext").value;
+                    search();
+            });
+
+            $.getJSON('https://api.ipify.org?format=json', function(data){
+                ip_address = data.ip;
+        });
+        
+           
+    });
+</script>
+
+</head>
+
+<body>
+<center>
+<h2>Find Research Papers</h2>
+
+<input id="querytext" type="text" placeholder="Search.." size=80/>
+
+<button type="button" id="searchButton"> Search </button> 
+
+<div class="searchResults"> </div>
+<br><br>
+<button type="button" id="more" style="display:none;"><b>More results</b></button>
+</center>
+</body>
+</html> 
diff --git a/label_finder.py b/label_finder.py
new file mode 100644
index 0000000..fd2a664
--- /dev/null
+++ b/label_finder.py
@@ -0,0 +1,81 @@
+import nltk
+from nltk.collocations import BigramCollocationFinder
+from toolz.itertoolz import get
+from toolz.functoolz import partial
+
+
+class BigramLabelFinder(object):
+    def __init__(self, measure='pmi',
+                 min_freq=20,
+                 pos=[('NN', 'NN'), ('JJ', 'NN')]):
+        """
+        measure: str
+            the measurement method, 'pmi'or 'chi_sq'
+
+        min_freq: int
+            minimal frequency for the label to be considered
+
+        pos: list of (str, str)
+            the POS tag contraint
+        """
+        self.bigram_measures = nltk.collocations.BigramAssocMeasures()
+        assert measure in ('pmi', 'chi_sq')
+        self._measure_method = measure
+
+        self._min_freq = min_freq
+        self._pos = pos
+        
+    def find(self, docs, top_n, strip_tags=True):
+        """
+        Parameter:
+        ---------------
+
+        docs: list of tokenized documents
+            
+        top_n: int
+            how many labels to return
+
+        strip_tags: bool
+            whether return without the POS tags or not
+
+        Return:
+        ---------------
+        list of tuple of str: the bigrams
+        """
+        # if apply pos constraints
+        # check the pos properties
+        if self._pos:
+            assert isinstance(self._pos, list)
+            for pair in self._pos:
+                assert isinstance(pair, tuple) or isinstance(pair, list)
+                assert len(pair) == 2  # because it's bigram
+
+        score_func = getattr(self.bigram_measures,
+                             self._measure_method)
+
+        finder = BigramCollocationFinder.from_documents(docs)
+        finder.apply_freq_filter(self._min_freq)
+
+        if self._pos:
+            valid_pos_tags = set([pair for pair in self._pos])
+            valid_bigrams = []
+            bigrams = map(partial(get, 0),  # get the bigram
+                          finder.score_ngrams(score_func))
+            cnt = 0
+            for bigram in bigrams:
+                if tuple(map(partial(get, 1), bigram)) in valid_pos_tags:
+                    valid_bigrams.append(bigram)
+                    cnt += 1
+                if cnt == top_n:  # enough
+                    break
+
+            if strip_tags:
+                valid_bigrams = [tuple(map(partial(get, 0), bigram))
+                                 for bigram in valid_bigrams]
+
+            return valid_bigrams
+        else:
+            bigrams = finder.nbest(score_func,
+                                   top_n)
+            return bigrams
+            
diff --git a/label_ranker.py b/label_ranker.py
new file mode 100644
index 0000000..e61664a
--- /dev/null
+++ b/label_ranker.py
@@ -0,0 +1,233 @@
+"""
+Reference:
+---------------------
+
+Qiaozhu Mei, Xuehua Shen, Chengxiang Zhai,
+Automatic Labeling of Multinomial Topic Models, 2007
+"""
+import numpy as np
+from scipy.stats import entropy as kl_divergence
+
+
+class LabelRanker(object):
+    """
+    
+    """
+    def __init__(self,
+                 apply_intra_topic_coverage=True,
+                 apply_inter_topic_discrimination=True,
+                 mu=0.7,
+                 alpha=0.9):
+        self._coverage = apply_intra_topic_coverage
+        self._discrimination = apply_inter_topic_discrimination
+        self._mu = mu
+        self._alpha = alpha
+
+    def label_relevance_score(self,
+                              topic_models,
+                              pmi_w2l):
+        """
+        Calculate the relevance scores between each label and each topic
+
+        Parameters:
+        ---------------
+        topic_models: numpy.ndarray(#topics, #words)
+           the topic models
+
+        pmi_w2l: numpy.ndarray(#words, #labels)
+           the Point-wise Mutual Information(PMI) table of
+           the form, PMI(w, l | C)
+        
+        Returns;
+        -------------
+        numpy.ndarray, shape (#topics, #labels)
+            the scores of each label on each topic
+        """
+        assert topic_models.shape[1] == pmi_w2l.shape[0]
+        return np.asarray(np.asmatrix(topic_models) *
+                          np.asmatrix(pmi_w2l))
+        
+    def label_discriminative_score(self,
+                                   relevance_score,
+                                   topic_models,
+                                   pmi_w2l):
+        """
+        Calculate the discriminative scores for each label
+        
+        Returns:
+        --------------
+        numpy.ndarray, shape (#topics, #labels)
+            the (i, j)th element denotes the score
+            for label j and all topics *except* the ith
+        """
+        assert topic_models.shape[1] == pmi_w2l.shape[0]
+        k = topic_models.shape[0]
+        return (relevance_score.sum(axis=0)[None, :].repeat(repeats=k, axis=0)
+                - relevance_score) / (k-1)
+        
+    def label_mmr_score(self,
+                        which_topic,
+                        chosen_labels,
+                        label_scores,
+                        label_models):
+        """
+        Maximal Marginal Relevance score for labels.
+        It's computed only when `apply_intra_topic_coverage` is True
+
+        Parameters:
+        --------------
+        which_topic: int
+            the index of the topic
+        
+        chosen_labels: list<int>
+           indices of labels that are already chosen
+        
+        label_scores: numpy.ndarray<#topic, #label>
+           label scores for each topic
+
+        label_models: numpy.ndarray<#label, #words>
+            the language models for labels
+
+        Returns:
+        --------------
+        numpy.ndarray: 1D of length #label - #chosen_labels
+            the scored label indices
+
+        numpy.ndarray: same length as above
+            the scores
+        """
+        chosen_len = len(chosen_labels)
+        if chosen_len == 0:
+            # no label is chosen
+            # return the raw scores
+            return (np.arange(label_models.shape[0]),
+                    label_scores[which_topic, :])
+        else:
+            kl_m = np.zeros((label_models.shape[0]-chosen_len,
+                             chosen_len))
+            
+            # the unchosen label indices
+            candidate_labels = list(set(range(label_models.shape[0])) -
+                                    set(chosen_labels))
+            candidate_labels = np.sort(np.asarray(candidate_labels))
+            for i, l_p in enumerate(candidate_labels):
+                for j, l in enumerate(chosen_labels):
+                    kl_m[i, j] = kl_divergence(label_models[l_p],
+                                               label_models[l])
+            sim_scores = kl_m.max(axis=1)
+            mml_scores = (self._alpha *
+                          label_scores[which_topic, candidate_labels]
+                          - (1 - self._alpha) * sim_scores)
+            return (candidate_labels, mml_scores)
+
+    def combined_label_score(self, topic_models, pmi_w2l,
+                             use_discrimination, mu=None):
+        """
+        Calculate the combined scores from relevance_score
+        and discrimination_score(if required)
+
+        Parameter:
+        -----------
+        use_discrimination: bool
+            whether use discrimination or not
+        mu: float
+            the `mu` parameter in the algorithm
+
+        Return:
+        -----------
+        numpy.ndarray, shape (#topics, #labels)
+            score for each topic and label pair
+        """
+        rel_scores = self.label_relevance_score(topic_models, pmi_w2l)
+        
+        if use_discrimination:
+            assert mu != None
+            discrim_scores = self.label_discriminative_score(rel_scores,
+                                                             topic_models,
+                                                             pmi_w2l)
+            label_scores = rel_scores - mu * discrim_scores
+        else:
+            label_scores = rel_scores
+
+        return label_scores
+
+    def select_label_sequentially(self, k_labels,
+                                  label_scores, label_models):
+        """
+        Return:
+        ------------
+        list<list<int>>: shape n_topics x k_labels
+        """
+        n_topics = label_scores.shape[0]
+        chosen_labels = []
+
+        # don't use [[]] * n_topics !
+        for _ in xrange(n_topics):
+            chosen_labels.append(list())
+            
+        for i in xrange(n_topics):
+            for j in xrange(k_labels):
+                inds, scores = self.label_mmr_score(i, chosen_labels[i],
+                                                    label_scores,
+                                                    label_models)
+                chosen_labels[i].append(inds[np.argmax(scores)])
+        return chosen_labels
+
+    def top_k_labels(self,
+                     topic_models,
+                     pmi_w2l,
+                     index2label,
+                     label_models=None,
+                     k=5):
+        """
+        Parameters:
+        ----------------
+        
+        index2label: dict<int, object>
+           mapping from label index in the `pmi_w2l`
+           to the label object, which can be string
+
+        label_models: numpy.ndarray<#label, #words>
+            the language models for labels
+            if `apply_intra_topic_coverage` is True,
+            then it's must be given
+
+        Return:
+        ---------------
+        list<list of (label, float)>
+           top k labels as well as scores for each topic model
+
+        """
+
+        assert pmi_w2l.shape[1] == len(index2label)
+
+        label_scores = self.combined_label_score(topic_models, pmi_w2l,
+                                                 self._discrimination,
+                                                 self._mu)
+
+        if self._coverage:
+            assert isinstance(label_models, np.ndarray)
+            # TODO: can be parallel
+            chosen_labels = self.select_label_sequentially(k, label_scores,
+                                                           label_models)
+        else:
+            chosen_labels = np.argsort(label_scores, axis=1)[:, :-k-1:-1]
+        return [[index2label[j]
+                 for j in topic_i_labels]
+                for topic_i_labels in chosen_labels]
+            
+    def print_top_k_labels(self, topic_models, pmi_w2l,
+                           index2label, label_models, k):
+        res = u"Topic labels:\n"
+        for i, labels in enumerate(self.top_k_labels(
+                topic_models=topic_models,
+                pmi_w2l=pmi_w2l,
+                index2label=index2label,
+                label_models=label_models,
+                k=k)):
+            res += u"Topic {}: {}\n".format(
+                i,
+                ', '.join(map(lambda l: ' '.join(l),
+                              labels))
+            )
+        return res
diff --git a/label_topic.py b/label_topic.py
new file mode 100644
index 0000000..9692fa4
--- /dev/null
+++ b/label_topic.py
@@ -0,0 +1,129 @@
+import argparse
+import lda
+import itertools
+import numpy as np
+import nltk
+from sklearn.feature_extraction.text import (CountVectorizer
+                                             as WordCountVectorizer)
+from text import LabelCountVectorizer
+from label_finder import BigramLabelFinder
+from label_ranker import LabelRanker
+from pmi import PMICalculator
+from corpus_processor import (CorpusWordLengthFilter,
+                                       CorpusPOSTagger,
+                                       CorpusStemmer)
+from data import (load_line_corpus, load_lemur_stopwords)
+
+class TopicLabels:
+    def get_topic_labels(doc, 
+                        n_topics,
+                        n_top_words,
+                        preprocessing_steps,
+                        n_cand_labels, label_min_df,
+                        label_tags, n_labels,
+                        lda_random_state,
+                        lda_n_iter):
+        """
+        Refer the arguments to `create_parser`
+        """
+
+        docs = []
+        for d in doc:
+            sents = nltk.sent_tokenize(d.strip().lower())
+            docs.append(list(itertools.chain(*map(nltk.word_tokenize, sents))))
+        if 'wordlen' in preprocessing_steps:
+            print("Word length filtering...")
+            wl_filter = CorpusWordLengthFilter(minlen=3)
+            docs = wl_filter.transform(docs)
+
+        if 'stem' in preprocessing_steps:
+            print("Stemming...")
+            stemmer = CorpusStemmer()
+            docs = stemmer.transform(docs)
+
+        if 'tag' in preprocessing_steps:
+            print("POS tagging...")
+            tagger = CorpusPOSTagger()
+            tagged_docs = tagger.transform(docs)
+
+        tag_constraints = []
+        if label_tags != ['None']:
+            for tags in label_tags:
+                tag_constraints.append(tuple(map(lambda t: t.strip(),
+                                                tags.split(','))))
+
+        if len(tag_constraints) == 0:
+            tag_constraints = None
+
+        print("Tag constraints: {}".format(tag_constraints))
+
+        print("Generate candidate bigram labels(with POS filtering)...")
+        finder = BigramLabelFinder('pmi', min_freq=label_min_df,
+                                pos=tag_constraints)
+        if tag_constraints:
+            assert 'tag' in preprocessing_steps, \
+                'If tag constraint is applied, pos tagging(tag) should be performed'
+            cand_labels = finder.find(tagged_docs, top_n=n_cand_labels)
+        else:  # if no constraint, then use untagged docs
+            cand_labels = finder.find(docs, top_n=n_cand_labels)
+
+        print("Collected {} candidate labels".format(len(cand_labels)))
+
+        print("Calculate the PMI scores...")
+
+        pmi_cal = PMICalculator(
+            doc2word_vectorizer=WordCountVectorizer(
+                min_df=5,
+                stop_words=load_lemur_stopwords()),
+            doc2label_vectorizer=LabelCountVectorizer())
+
+        pmi_w2l = pmi_cal.from_texts(docs, cand_labels)
+
+        print("Topic modeling using LDA...")
+        model = lda.LDA(n_topics=n_topics, n_iter=lda_n_iter,
+                        random_state=lda_random_state)
+        model.fit(pmi_cal.d2w_)
+
+        print("\nDocument coverage:")
+        doc_topic = model.doc_topic_
+        doc_topics = {}
+        for i in range(100):
+            doc_topics[i] = doc_topic[i].argmax()
+            # print("{} (top topic: {})".format(i, doc_topic[i].argmax()))
+
+        print("\nTopical words:")
+        print("-" * 20)
+        for i, topic_dist in enumerate(model.topic_word_):
+            top_word_ids = np.argsort(topic_dist)[:-n_top_words:-1]
+            topic_words = [pmi_cal.index2word_[id_]
+                        for id_ in top_word_ids]
+            # print('Topic {}: {}'.format(i, ' '.join(topic_words)))
+
+        ranker = LabelRanker(apply_intra_topic_coverage=False)
+
+        return doc_topics, ranker.top_k_labels(topic_models=model.topic_word_,
+                                pmi_w2l=pmi_w2l,
+                                index2label=pmi_cal.index2label_,
+                                label_models=None,
+                                k=n_labels)
+        
+    # if __name__ == '__main__':
+        # labels = get_topic_labels(docs = [],
+        #                         n_topics=10,
+        #                         n_top_words=20,
+        #                         preprocessing_steps=['wordlen', 'stem', 'tag'],
+        #                         n_cand_labels=100,
+        #                         label_min_df=5,
+        #                         label_tags=['NN,NN', 'JJ,NN'],
+        #                         n_labels=10,
+        #                         lda_random_state=12345,
+        #                         lda_n_iter=400)
+        
+        # print("\nTopical labels:")
+        # print("-" * 20)
+        # for i, labels in enumerate(labels):
+        #     print(u"Topic {}: {}\n".format(
+        #         i,
+        #         ', '.join(map(lambda l: ' '.join(l), labels))
+        #     ))
+
diff --git a/neuralnetregressor.py b/neuralnetregressor.py
new file mode 100644
index 0000000..66449cf
--- /dev/null
+++ b/neuralnetregressor.py
@@ -0,0 +1,71 @@
+from sklearn.linear_model import LinearRegression
+from sklearn.neural_network import MLPRegressor
+from sklearn import preprocessing as pre
+from math import sin
+import numpy as np
+import csv
+from collections import defaultdict
+
+def relevanceScore(intercept, coefs, scores):
+    relScore = intercept
+    for index, score in enumerate(scores):
+        relScore += (float(score) * coefs[index])
+    return relScore
+
+trainData = list()
+f = open("./supervisedTrain.txt", "r")
+for line in f:
+    words = line.split(",")
+    trainData.append(words)
+
+trainData = np.array(trainData)
+trainRel = np.array(trainData[:,0], dtype='float')
+trainFeatures = np.array(trainData[:,1:-1], dtype='float')
+
+scaler = pre.StandardScaler()
+trainFeaturesScaled = scaler.fit_transform(trainFeatures)
+
+testData = list()
+tf = open("./neuralNetFeaturesTest.txt", "r")
+for line in tf:
+    words = line.split(",")
+    testData.append(words)
+
+# Train model....Got good results for 3 hidden layers with regression data
+mlp = MLPRegressor(hidden_layer_sizes=(3, 3),
+                    activation='tanh',
+                    solver='adam',
+                    learning_rate='invscaling',
+                    max_iter=1000,
+                    learning_rate_init=0.001,
+                    alpha=0.001,
+                    random_state=0,
+                    shuffle=True)
+mlp.fit(trainFeaturesScaled, trainRel)
+
+testData = np.array(testData)
+testDataQid = testData[:,0]
+testDataDocNo = testData[:,-1]
+testDataFeatures = np.array(testData[:,1:-1], dtype='float')
+testDataFeaturesScaled = scaler.fit_transform(testDataFeatures)
+testDataRel = mlp.predict(testDataFeaturesScaled)
+
+
+testDict = defaultdict(list)
+for index, rel in enumerate(testDataRel):
+    temp = [rel, testDataDocNo[index]]
+    testDict[testDataQid[index]].append(temp)
+
+tf = open("./NeuralNetRegressionFeaturesTrainResults.txt", "w")
+finalDict = defaultdict(list)
+for key, value in testDict.items():
+    value.sort(key=lambda x:x[0], reverse=True)
+    finalDict[key] = value[:100]
+
+for key, value in finalDict.items():
+    for v in value: 
+        tf.write(key + "\t" + v[1][:-1] + "\t" + str(v[0]) + "\n" )
+
+# TODO Add evaluation methods to be invoked on test data set results.
+from joblib import dump, load
+dump(mlp, "plsaNeuralNetModel.model")
\ No newline at end of file
diff --git a/passenger_wsgi.py b/passenger_wsgi.py
new file mode 100644
index 0000000..8056eb0
--- /dev/null
+++ b/passenger_wsgi.py
@@ -0,0 +1 @@
+# from SearchEngine import app as application
\ No newline at end of file
diff --git a/plsaNeuralNetModel.model b/plsaNeuralNetModel.model
new file mode 100644
index 0000000000000000000000000000000000000000..555fa1568b3b1386dc40ce22e22c76d0a153b8d0
GIT binary patch
literal 64297
zcmZVmcRW^a;0OLA$qdOTvWaAuD7lnXX5EA+vdZSzGSYAhAv2?l?7jE6aNF~~Z+k~+
zsAxz;`JMOe^YQ(CzTf)~4_?nRuXFBmuIoD25p=-H-NDhu!p&L2*~Zh&!qME>#>3mi
z%|XJ+)6v7u(Zbip&D_<-&C15r!_CE+R!939#^#=zjk~*x8!bX6`Vb)xKqy3vY01dQ
zEUY~2yevHIT%3^u#*}E_X5;9IP#ROAzq-3PdfB)k2T8ewwS^NxWqk0zc^qABEs#SR
z;K}_(jgq5&qy<@8cv#t*yW8EjL8$j;@bqv&Xp9-q8A#h|=X}rH&BDV5p*5!dZz0we
zuIQ?55IW-{|L;l6?VRmA5PA)8TyE(zmx{RYA#@5?7jJa8JP-yA@CN-8{g9U9WZ`3O
z=YekKVI8KEq!l~5xVs~a#`NeE?ue&_n~k+ODPkh6$kxu<+Q!*@uaN%RTz7<7i%*LU
ziyR@X#@*KQ?p;S4gykt|32qk7)-F!w?jHYb`B5!0;{)hoJzN|S*8jV8XhB)JxZ5CX
zp`_CAws0fO<mQ2}hwg2si<6DBho=+5p#i=O{P!6oZI82!JG%8QUhd{j|2xI8r^biT
zwV(&s(HC8_i>oVIcF6HiV-_^^vT(GsCLJ*IyKes-J@m+NYJmEr|31m2<7R2&VPP(f
zaA|<GlB53}qW>qAK~A9Q@1ZHS5HZs3+PJ#gIiic;)=*OOja-s?cHWo?U5hiROwFBL
z%&lxKobTBnJX*A*9dkB!@$_)@^l&#vc(uq$M;AS+|9@q~XXWha<mxN27fP7B+F7CJ
zDy{kdOhqb4b1RSk&P0CF7U&pAD=5gGLj-~mK@1`kfC!6_7HeteVSdlX*#<plT-=b8
zwgRMcZ|^{e1R$a!q(k9g<BOcKvUjm`w6l~Tt@(e~sQce$Tyk@>@HKL?a7Dikw1^mb
z>Yolk#6?I)#NE@<%FzNnS0wf}nbz6b;=jph5y`z(*;=^TAW}N66v2ozDRFVLMo%;u
zV{){FR1jHGX6@mNPIcyg7D4)p7CCE7fllIi29eVu(;^Q+&S4OF5v>EH85I!)U0q$x
zUGzixp+)9_oVVQjGCDfCc$+&pS-2t>o_bo2EcTtDmf+NS9N1spt>bVfnwKS{R#}LX
zT-9w>BA%6E%7dDpKaY`)c@mS_U~4e#!r5{1yA3gUi^`-X=Z|W<CHQ?miNbJnhxYUb
zJ?BHJXU+v_y5aj%v7KPj%s;&~oDTOoq$LOQRbE}ZHjc~Ur0@61KwD|I?#~`a?it9{
zM-86qN!zG6J&?M4@8{%I6{+t7Yj+-$d}$u(x=hCzV2%&IDgI6%<K}pYh5W%Mtln=O
z-`M>_wBAf?ZyWd>LO3kn>T60$Z&EouydRAr+`dE4_|uk_RVivr(~UFp_Vz|z_hOc1
z=vU*0m`Zk*oG<hrrcBL(9`<DN8x0k>nG%nxbhut%rE*+7EtzB$-B2{-s!Ux+6RdJj
zf9qChd(PXeM736m`Pr`v0+;5JUn*SDH*^fih#G$^A@WG5gz|E(gKw6p%npB^?S}VR
z!SCm3rLmJ&tlm%-u|5yFT`{Sm8yj;jIGkes0`CE4%q21&e-&2!4tur%g}WCHhvmsI
z#SbpzG>z;qwn3_-{7~=wATjg7Z92y=&2Pv1s#X5@2To~gW^o2Fnc@}O7jUbp8*2rJ
z=h=eD+D<u(%*13=pHtA5vK8rCORVuL@={W-$sOk6_|ql$p#P`D?D{EWM*OhQvS{Tk
z4pm>}WGS~lOfAx9(#}TA=<_-7XUBw5=Ge=86W1$uI5j5tU^I*Mp5i0>(D#Q1ooMwA
zT#-AEqaK-^_da2|R)7>?T}`N8XO0S#>4$<tg)hvuQrEAw6dHL}>rXOhz2FG89M~B;
zHN=@bcW5c)nAXi%@_2ju;{=KrSuWRTm+mAnstS{;gu2%6yekX`oo}3%5nj>5SWJB{
zCH^>Mu_N(H)xC;O=&lFPLw6tVtjr!6GYUL;<MX-K72*>ck$q_v?hIzM3rETm^T|cR
zz_kNa6hq01);6EBPblwl4UC8;_DcVhh;5XP7;|_%K~>T4c148eK=6^5)N|pt#t!om
zt_vt{2fi>kPhR3bzQFVxnU{YUuh4m(-%IoRAB*qUYSGLdt|ZIc7-q%Y@K}dmf3p20
za%#Mz9816LCQVt*zc~NN=KP<xXUP*vPp2gaEhP7D@%nzr?r4wFZ|3;9UMb^jAO0|*
z>_evjeVT-icJ{01Zza<YCS6Mi<myvX{lUIsA42n0nD&)b?>D#UJQo8y>kY4%<>L;Q
z#X9}6r1V!~)xZreldVT1MgD(S+JtzIsCLFZQHl!`V~Xa4TkUMk^mRj7s$FBgPsT(S
z|Aq`QEqu0|_0+F7J>vdj^QF_PMcV4ankHXlo?^ceR1GL_aj&$Ng-mAyDfQc01k^t$
zp7?9(r0A8Y{>E&OwJqQuSJ-a>;hakMqV)rsl4*B;fsYr2XK-Ko-bm$d*y7}UHJeJt
zh|BJIqL*@vP6S(<_J+$-)Lpn9O0RhSr+pDe{V9w0R=YiGkr(UY8QqSvN~FAVnOWl<
ztXy9?oS^?9^4F!M9{unSE0?mWPIxkYr#nS&7kXmqTwkuxnT4-qC0(o7Zmqg^G`YOh
zxH@%hxIz_aX82p&|Gt-@qUQ^hkoRlU1EIUpl&j>{ALK5+S;lI;S+P81JzEa48^}36
z>UZ|W1l)6fi*uu)shTwzUQunoDeSzwy)K!+t6QAJrT*n=XJ(joCz;06#UslREq4wm
z@I2CushYlaxpB?7D@;DDRXgoZ?5{(=<?m=zvIRa#CfDcITPycFLiJ%;VeR^&=#MC$
zhntCQaTB*(yF)uJeKtFEb)ECa4&mw{P2YK-%Q{BYrBBmWKk0v7J&imuX-o6t5dVQ*
z*|jgWtHb1m&zP5+t=R2*es6SVKbecCd#ahe^~oMD+&Q7D>nCJ*mTD|q=4TbLL-Fz_
zLGGs!MTw1DBMsxF!db3Azbf?7zBwkpvzp|eF>1OjZ`3An`WB7XW!Bt=zjQGUFJ(?Y
z6F$NuLvB0FZa;F*I4gW+{RBCugUW-svNcAFlO_kRzshb+TRPDoar|&?MCVk)<rH?o
z_1#IKu)N?;(Z@F3rRi`q2D3PUq?26KQntx$@t?bTw#KjtN~eThCbH}OuFCUA8iL7I
zX2d7+*;?~121iEt=XXU{DX-aDxd>&d4<|lKIILmqRB}T|KjrP)N9P;`UTUYw-D(p{
zv3?}J_@)5A-1W<IvhIeil*EG#Yo-L}jFQn!8b8)$v2Bf%o|-RuCJ$WBmYWXpi>f>;
z`J1k}`i{RvUSZ56dX6KUL9`Zfhc1@ehXLicG{+kVnXl<IT^vN#jf0-PY*dJ_{!0@J
z;m)nG`3^pz^;7d(w%jG`iDeBPNMHm1{N~u1(wSYZ_IaIS?N_Zg&kJ4BGkx9eLwC@_
zcf9josg1<r-7|RlKr#8_V)uF;39QmQ5Mq-poXCBCs=fGfw60(fWEjzr`6KAV@|>QA
z@?=tr5Yun_Y3d59XzM!Ft5roGDtc7c-OtEtD&;DcsFDXhd&gI``(cB+G9oAFr^o5H
z$1h(O`xBoxxNs>~l4m*JEux|zdsNJipxNFxbo)uAfsdEM#g~#EG20z2+3b~_b|s5B
z=NlJeE<XObP}6X`%wRRNMCwlev&{MD-*!?AZI3>Ev+%JQ6Z*#4?oqgS8h!R3p0ls$
zW$8~bg*W>X5aF$rfpX0CgZVZ6RnOHH#7Bc0zp3wDNU*<wJMeeb$7+csvUXO#Kd$MZ
z+U#|W)^`)dQhIBz*h+68{;%kjb?FTskWbz%(QGoJ)OkKc>m24Z)kc((l+)91H_xTq
z$&fo-;Q1$mRkGN|+3TA^mjBt=g=E*8Gnxf|;ET=^srMvO6V*BIkKMSD<o{J{_myB2
z_MfdM^Nq2J=TUyv_MI{}toi*uJQKlOnT2mH)L*|T6!f0@`XtWvc)sjq_Dk~%?)@3s
z_lRkiXXhF2r6yD=H?Acs(@$o66tOJHn&wGJt7h;>th{z$wO(09ceX1l#Zf!%-AUwr
z2KNnB?Nc(xP5As<;uS>Be?Vr0(y!F-8mD~ux18$kYD^o(e08ZQgOh65@$WsClGV;{
z*=2K9KOPa@5kLC3@rnwZZ;`oanrS~Majh}LCx+?li><3}#WE}cB{XY#y(cz)am@GR
zUVmy7UY<u_V7X^a7p!1cen|XXW;N}#6FxfAYjvD1I&`-f>bO4o>KWba(C3TtIYS=S
z$No&CZ2R&xwgHKt->)^V;MH=>>N+d^zKIPv@E@nty>oekirMG4*+Oh){{3nFrjZ=k
z@0Pii)e%n%sq5(mSr4vta_dcbF&%3ZB;#su3eXnF<i?lW6zr?hWM{mX<<si>@k%1~
zli9RoVhV`UHI=K&axT2}#gaolgr<a4L3MR;M-+9E$UTi|&?=#=g}eDZ3s1Dtc@b?(
z{MVS;`%6g!{gw3bv?SGv|EB_u0G$?cQf1P^98uO><v&73TF(D8V9;9lB`r#0Ms!ZJ
zT5kUTH0I2a%cMH49op9Mu=DUmR7h<AD;Jx)=sc>o5j7Fp|7yeP0f>f(t%R+lt&|pv
z79SS5Vk=`SYkS6auLgTp9=W<#gVn?!T4)W{_WxUV)wZ<#ud;fOXI0%Jwg^Q1?o}<|
z?Etq+9l1U9i$J7YTNWqnBd{hLRguG9203iHUoT7I0RQWp$G=SD!AN(an^zki{5<+7
zEFpUYG(Dv{V4zhBdqPd^w3d4T*?a&~wN)zoz`$*N;&cX7s5joY&rAfna|b^~WRJk5
z+5lDQsbOe5q_HexH44=Q&HkD`>;`sv$!_{t-N5pB*~jrCgFtMiy(+VW2wYUc4L6O3
z!MWdGY*@Ccq3w>*d;Pc$!2kG&l4t}0Ufp`<f1$V*#Jn@NRC_uC$=A<#e@MjxpNzRW
zO73`IP^|n!br=T=677W|k90uFF@5zsfld&dm(H#xR0lO?uWsp4wFA#kHpOH|BIMmj
zxF<J~4P1(On;(BH1}_dqThpx815W7|C)n>0fZn_BJ^Z}ofK_&=_Gn}+6jRYVoO;m~
zjyG(l8cer=&1fw#if5y+(jiks)Vm1CDL>2&l5PgCi`<l_1Ule@PhmlgHoZ`TKj`^p
zTO$;S*k;-Zt%R);wWIE*V}aY)Ki2z2VSw91J~PE+4m^2lHgTM65Q@}lzj*IZ3ClW0
zBqaxWVF$6_^}0wUm=xBVW)<#)CUmb_^HVcm%%T$KZEOQ%rLzo;nrVd}ORt~BJR5=c
z@keipUg!oaTw~8xv_{~x?^&u|IXrAha&L{MtAL-598|;hbpRie6Re(wM0h=>B4{pu
z7?h3gNHZM3!+_+2Ar~}9K#H?K+bgq57(9hPDk(7nu*vZ!-+pZaf9e$qA`=GzJ*Gp)
z^4}Q51U;nWC>nw>y0%1`za#MJZLAjOu{`()=CT?$5}`y#<k`cs5T1R^!gKC>6^wcq
zXdrWQ3?|Y1l=+qkfhY|_)TPZPph#sykW@nNXYCtvYd*Q)vYet_JnIk?Vi71lBw7oY
zj-C9_xkUgvGEO>_#CB-rke=E3x*9M^1j#(9=z(%OY7s@V{ou3R(7CY6a#&Mo<~nw>
z2L`^fvTO{?0-?)svA*QvQ1|TMreG`q$iA;3?{6G|OxX`r$#$ziT>^KN4re!PjDHff
zmDvvxxU|)6-i-m)JR0OmR4e?Q_4`n1cNI7Y0+E5#YLIZ;Kk4U04G=f{)5veu4!uf3
zDGAXdAe^Oujr@5PEHGwJptb4)!$(Vw3KLs^mbzoQwm~N}wzcxU_i+fEdo%Z3=TIT^
zIajZ)G1LRR7ti|fwB&<~I?i{#tldDn@}3S)OdE(Xvpi^ftsDH<E;r&4&4Q9cpT51H
z83xHAJIrmfg&>P4A^sw+7Ba{uIY0a!2zFH8#CV%b!los^jKd!7P+-*mZ)t1|_`FWJ
z9G%||*XM+J>s_0GS75S?)und$i$m_8S6VBi{B>@?xTY1d{<*@S*cAlNg+^Ao?&bpN
zk;+qQAF_a-7PFWM`u(YS+jSr#HxHP!c21T{Kp11p%|&Dy20^o?H)xM`z%W2Xn06?D
zT;Dl9d$jie-**aPue2c84htk+FK-0>T%Um=<rw@{-<R~c6c5a06qqDfI{?fed;Ldp
z61W&I78{-)hXTbuU2p#lf<X4~DuS2$;TyK;tUQ)Bkh=B5wy&@sQXf1f)`J}c9sb&u
zjng<VZd3iTODPRRM%B15{J_E|><-sUb%^l(P4<e6%zDtwYclnFHy;*InKJREw}B^Q
z?BPsReZVIAGs77PJOCl&rt*UQz{Y&w<NBX*NLTY_@L^XT9QzVq#y8LjLJx9_Q`Y05
zh)h!!@0A!RVsKa^ijN3Sj#$!TQ+l8inf%kA#|iM7lfb_Rf7(GZA3O1V)et;=WiX1@
zuNPYTpZzhfG7849Sz27}t$;2Cig*$xpuE+^2vwaC*vryeLFriq-d3(}eCR9(A;c5c
zoV;=H-8-chpUB6--$GydTJ?Hh!q(KV*;NTTtM4A#&Zq~cbc{qa*xNzTquYsTylrq@
zWBL}YS~Yz9VKbtevJ2j}y1m3=)e8xw_+Z-4<KW%tC-c*BU7-96N8}VJhw55}^$(-^
z;qTYo6oh6Rto|b7o-09woyQE2!MQ@H^F&61d%FYXlY4+aD~;eDxyo!?X#)(d)mqXh
z#X+}mbMEn20$ivtF1j#9fL750jUAh<fCvkY${EJNf&7cQ?(vC$mtMA}u&Ni(&3YLc
zXA?ka@V~&C{6VP1!W(9-SPc$r3I$lPLdaM!q0e+@6p~@vI6FmK!Oe4{D`2`A7B?Id
z8x(4W&mNCYB)eyW3UR~cSBLT7afrTLu4oRRGWWabJkbw=8Z=Xqe{?{vtDj#JmAb)E
z4cjd;^g47ktwK}71`otS&pZxIfuOMlr<lY<gck;Kz2~ou04g(s3n2q-pqX2zrlevJ
zo|BAwc%Zri=3Lt{56X-IaxwL*Jo6LqZ_BvW>y-gGFPNeFk1`v6#d>?Iir2&Ka1S}V
z*RSE5D||b}{OEPl|J3Q$sCKXwBr$TSt_xhdLM|AVF#-drIj;Vk&w>nMrE&=uy5Z#S
z$YTX|-QXXw&*O(>6>##S3Yl@J1}C_pzE7jqW5Y<(2ZQH_q1(S00qMLPK+7A)p_5Sr
z-?splgpLWoYdf&^LLUzfyz2F6U>*Rs$?aXmy@r7Ky=sN>sxDX<mPB^4E(NL{(D^aj
zJpfAc`foVAp9c4DI%jVgcfp+3CQXSuS#aVP^}9Dkqfq~wuPXESIQ$grdQ!7=5*)=-
z_aAS44xhU3Iy9d|k84a(pr>{#jE-muyUx-C*6v8Wk-kBMcbR)RTPe$7yT^KWS=KnP
z3~=b2_nrnKPQS-pdSd|9uhP|uSAF1!D=|myHxXvL2T6DI4}yej&y@q6^Wco!S4+Z|
z4p7J(a{IAbH#p|<)AUh09vsTmitn=<hQ%*zl{AV6z)0H6(hNTiP%q{)9_br}<xXyc
zcGhWd(2uWmQg#5I`E0R%s%{cow|?^RNpcx%(Cy_GZcYV4pSBpV)y}|e@_sGvcrm!l
zMq|f%qZQHz{xqdm9|6j*Ql;aXTj2w325-CXp>X4ek`$(_6Vm4_JkfvN3i)%!DhGH8
z07E{Pp!<aY=E^_S<Zcqc(4iM@Dodka_Gt|v8$JIR^Q41X)~X<l<k;cYkMWRpw9wEd
zIvZN_xe(_g+99*U9A`*XFO;RnOVy+000=XkRh11OX1-9S*JBb+x?iGSuk8Z)wnUW^
ziUi2BO%~xFfrnQZoCMC);GrYE!P%&)BCz>dp*yR%6Z-tCX=gpw3S}L5B=Rq30&nLK
z(|cM(&~ir?cU-Iy1Tc&x;{NtQ=X0N(%5Vgz)ZEpg>M{m<@lP73zV*T#A!7T<>|RJM
zK=JuW{cC8^Q?7sZd@W$$^Q>Zg3qf$8h75FRfiG85g%m$_!H;LQXsJA!;e-0~@;@Z{
zfUkkmIVFX0c=cyv)2dn%Fks72y=d40a*M|jd>03ROCNDyCutB`_Oy4p9>l>}0}5XD
z=5!!_&Kif|7=n?jlVJ&NRgfvB_gKqK0zAX@m+!(@J&deZrC$Cy27SkZg=x0BL95(*
znM~;sSbpM0`PJB1;6Q=fs7Wk?hBt2Os!^sxZ_~TVrUN5Dso-BQ&1@5xfBW_9q$&}n
zhksp(PJRK$w1V)LPep_0ma=#B{1Nb_b_7$gRu7h}w<@QvmO+|=zcoZ;5ONB2?`kYJ
zLO;ogp2iR&Scp4JN4_`!b^E@*=~Ti4kG2(=#|KhjoMFiopXoHHAf+Dv<jNo@_IW&=
z<PZ<KS}y(Al<xv%AxJag*bI+#zc=rj!~>U&7YAn1`cuJ`g;#SB2Lu;q54Aex!7qC2
z8_zC5(84Z7=l7`q+@ATu?$$(r*?IEMD~fyJJ<eJ)nUOA_+MQ8l`T!4vluG;qzd`7=
z9DzC4SOuhWBBs7z@PKmX<V)7%Yp^7RZ1dTTaq#Swsr;esN-(h{d&_Ns0API<hg-`i
zz_=`(*6`~FlTuGh<+Fz1FU6U+ip^Ev=PBa-hlh&c+&8Tf^?wBD?<?(*&JhI0j{NvA
zme2+(bhbmv!n%NeZ4^c(V+@LEo3ivFqrh1+5GkJSfWcTFar$4)aANn~N$Zm%P;Jrk
zochB~XsK>p#1rERx+sV-?z#P-;G}y6!K@!%|I9!2etZz<@(-oWt4{zakBH#E{e`f(
z+zeCSp91Npa<_Bp`(fx8_Qb!x+rjAfPrr|G48ljs7Y_>E8i8ibO><Qz1Hi6)?7biA
z1bA8uR0#aR!I@w;@2u?}XdlA!uq$T(iU%qZXCDniHh#*dX=M{ITWdtOcZ&dNG?-}@
zM{0nHSU0YvXawA_X3|k>BY^b~`;9Nk?vVS9J4?%>PWUbO#N%&|%0S9j9ov-?Uf@nb
z$lol-QGmVrr3K-`!?)7ct(^@9!0uY?A-}sdP&)NO`1U#hehj=NL;1P_(1%>k%x>w2
zp5=5h+d<ucQpsAJTRsO=zj3-2Vvq*jwXgmdan6QK5w%NK{<gv;C4J4!%qqaF_^Qp~
zNhNHlKZ?a5t&rQs`I%coGpLt0Iq&|W6VUL71?M`Cz$xYDXNKSPz`FZC$Tx7cFuMW#
zoKEcqjJBmaJ#qtJ%FWU;=w}-^@#xo9G2a*%esY{{NxTDof79d1(l`k&)BL{p;aEFl
zRmC~<I}jkPJcF>*JQ1u~-1O-{+bxH@d1@NUI)N#S&imqt-WOCZy%yx{1G6mNDS00|
zfjj><bIO)JNd9IfcmipI>Oy9hcLZADPR3^!>a1oE?roM!FGPUisu#_6fAm7581<co
z>Bn%eqq`_muoixAK8|=E9tFQ%s*lKy<iH8NqxU5R8-Z;+MmUOf5KLoQsW#-B(DGHN
z;PYz+Uwx42v5iqsl=s=?U`Q93YQhdO^FScNANys|4G;21JG<$lCctNT>KbQsd)DU#
z1An65C-?W2y#<<;P>+(Wbz-|4>K%;|{6LWcK4bn_hZf?XyMR4>D_IG3RO}K9Kh?qu
zc62vivk<}QKV<ru+XN73LS|aZn*bhtyz103&<d*@tViqFn!&r^2Oj1Et>E=hYpOnn
z*WhpscN61sEug%R$mi5N1byS|pWlsY1)oG)Gz-srgS8g|Z#W&h;a`pmRiX@4@cr?l
zr;f*zpx>vC%HG=@(7ESzegIty)S!KBZ@pUzbm}Iek&tX?mGtxkdmItGG(0Enc$@&-
zB-DcI`^KS_@hoSEWj1{G{iC&oT`lZ<`f2xq=>QB>+&tDVP!G=~9danr9R^Z&x5qKA
zdEn}=V(V@m2*s6P^V_;?Ao2HQMXG5VoDKl=$0S=I_#)xd^RO5EgqpV8ibEj(<4JGZ
zlrpgUXDzV!Mh`3w+|6LBT7=e5Q(P}iK%i!Ne1UR!1`3(S^nATp1CQ@^rGN0KgfzR1
zi8{Y~fgDxTmP^u)77OD~&s4S;o7px$lO4{~f`bmU#WT~E%^#+3P>!}sHgA<>su9_i
znqx9`?Y>nSC>@wdyKz2XSV=a)khO7UOOa<pDE+QvW%Ka`#fX!0@y#3+kLg*5q`^7u
z#gcXYTkzSbE14oU6`PNF3>^Fw@=dYKJVYMRrEm5;trj#?qYR2|jB5SAUxy`r<eS0R
z#^yko-?fX`UP?lVY%2A+rHZ(`g&+Fcj?G<St0IX6gO>7}&+h$Dm}nvEkOqwd?-Z|9
zQ#`-DS)=&xUS0dDVOI0n!y~w3zH2Q@nxeml4l*chUp@I)Rhs6a521fUjSJWGvIXb0
zOg-3==_38_?DpB_7YCA+Y?UX__Jc%6*3C(<fV)r@dT$ska`Z9fj;BH41dGpIR(Q~`
zR<L*!^9s1%<2!Zt&LrUdo0inw+6{Eury21o!(fxrz%ZM89PqSebdvX=?VH@APshXh
z;P2~>ztlu(;UMkqSy}89;P2ye^fD-fN@-t9M<qL<y@|l<ODiLQn109X_8l9*7DP*T
zI+h5&z3e=+9XbkcpNYFk|1J|uh9tZnOUj1DSFP%JpN_)Z%$vlr#X<0H`)tp)-T*w5
zg`0cYJPyMef4R|5_rPOaZ&M~8PQdS1bzd866F^&zuG_0yeSpHVu&XQyg8Vs8njZ_5
z;I2kmC?l~Ha!yjv-@9A@Dej(8`f4=}Om!MxCVlLKL$kg8L*oq~mi})a%Wx4ixG?qX
zTj)3tJNQJJC9eh=QpU@D@~nhQo43x<PP7C0KjleP8bp{HMn~@xSOtEiKAjmp^a_^L
zJz1u?R}GW0KK1M>5uwx&&B!Icba<`gYVh~)8fbFsg6i+(a!|mZpQ=aI3DqoDxtdhQ
zK&nw*HLKqkJYpL$E=M^4HNNrBvba~lpCZl$i7AzU<x%{_M>aVivaW*F>1Y=`JLFN_
zU@!#2XG>(e@8jTtc1&<y3ECdZNXyewDF@Z(xGCN&;6Y7ma#~l!Bp5bgH0qGVgNjqk
zeijw|fYi;W6M*RcZ#N%ltb^YFbn|JE`tBIInvGv&w?P_b2VrIMVW1Qp@-xJq2&uAp
zQcgZ-hH^5k_g}A+!oboXQTe|@@P577q^(FNSkvR2%S)+-)s*bS1xzKNm#tN}$lU_8
zMZ+owA9}#5lG3zC&4tjlt|oBsYB4ZKzZvp8a|Buoyy(4^&=1m6971!=Tfs`6JB#ia
z9Jq}%lO^1a2Lq8OetUcvhGvQrI#Zwmt}cXDJ#=e>Psx<o1%w-*MBdv|hkNnRs9TA<
zJiGxEQQOCvqxb6YcmCgN7Fxj}20o55FLe7n+ambiX9HOdO2xa#5U}P=Q-0qy4s9My
z-V5072c0)Zu6sC)LlZ;1IYWFJEZ`~H5~56oDgpyvtR`PV@g?=6w>Wbl=hr0fSUCcm
zOM=ZggJUpY_BylM)pBq#T6WNXYXDHJG$p2ejEBjw?)*Ddt<Zm(uek3`Cp>6?A01J|
z!}ZgNoFAk{ppW1Er%J`c(6Rr_1=-bB2n?z8x!8xHl2ozo_yqza2t3r<2tx0POs7SS
z(7Lpqg8iircKOf%7m_;BMF6MI%&>j^R0W!7%@4X<?1yh}V|$RG9{BWn(6G$eVmKKi
zr=cOL1LmR@TU4!Epmvg_iGN)W=;u*vFFV%;d<9;2P}`t=r+jZsj?EBZ+~)aNIu-)R
zCg<ard|L*oxb?2G{lSCN-|uFU-{=I>TiQ&?IlW->!8_A<*-r5CNKKs3{XD3}p?!?!
z!6QIn{d|`9ZZo8h;=ZIVSP#R~PGYY8LhC{@qW%vBYoI?xny}1pD+u~vPa9g+2qh1c
z1&TYBgEP67K3la!$o`w7M)VtckGn0?KEOhRtSXX5=X!~N@&1iyY;8A`B)g~|jkeKW
z?P8qVMw>wQ=h-bAN&+0dWE9E0g$LOY?Y9{zIsq|+ML`&hfQ-3YvxzrC;pB-FUcS*5
zI4N_zHSWVpz%%&o>ii}iSPP$RtQCj_zh-z{S<t?=fU^>T_U8y-amnQSien{wrrvWh
zT!H`#f5x95x!DbU<PDKRjV4%`FuvfZJp>s4+}9syt^m~z41j|z6TbM&wejvO4w_5p
zUOakt9H=r~&pK8+0`RIS%Ui3XKxOqITe49TtQ%lZqq;s0PQSP)#fI+%n5)+5fr;&4
z=?%_xFt!#>k-hEntQ~^e5;VTa8+}l~)Od}rY6P@I2A;(p>w>2C_YUloVd44MuS`{(
z8Bmpq^P@~V4s@m6s#-hK4bI>Cpz@$%48B(@n-*5^0e|OS41GYlz>SUcD<U^>KqU8H
z+Cs$$q&cabd?FhU8O|t}SOwu=Zm+jN)Tv46?1%HJb1i^3mX;Id^!njNIx&%%k5$0T
zyO8IAYAfv2k<V-U*#u%36L{`v_dsj>ACrV30uZL~xU*$53Xp`T)qhISz$N|zr!!wa
z#2#*!YG}kmY#DW42U8L3$&tFax`kfzQZ(3)W@UjCdgbH8Xq#9p$c>s;_Yq7cgtDEk
z>IAnmJ)ZlhRRW#gI<%9|@ZdhnF^LBvt&prb&T-{u4^Wy)41ZeE0(!ZeI*v0J!Y#oo
zmzNT7Kpyi-b@_1}T&&fqGTIygQ%7T!pWh#bMf7XGvL3WSMd6h9<MUncP0m9s?NTUY
zQlYJ1I)evqo+*7Q^p6J=2CYtP0!?u1&H7Urr739VD%fUvs}#0`hTvnO2=JTN$@#LK
zPIyY{n3Xrh1fa32QBn~aggIMDM-&xWfi_vooyn`s;L|z}SvnQkw~pMgN{%UjC%+$F
z8W`w?q2F(X(S1bwM5vz*7|nNrNZvla_xA^YxXfaC?1cv4e4mZ3Jc$6VO;^`dAw(Ee
zF>W|@wh88e)z#HqbosBo{fNGp1?6*Q%2*WpU^Amp&br$eyvp2lGb+6os9vcNndeD|
z>4!Bk#sBny-qcXNzS2?<5xPX1>zxNz{&xLg?yQFLbw!5-ICDY0tJejcK=k{-q~cdQ
zi32w+b{#`zMj;Z~GKt@41x+hDAo2~`=g@l8DXqL6oIed(JtKSIO2nsz16G;v?%bes
z#`#WY#zjsy^)?2)QRcc-P(=Vq#=J_xD|q0*lUg5;Q3WNK?Df`^I$;!b#C3n!8n|XR
zt2*371Ttm+4vo?hp$UCWw3%i%^pw3~z~Iyas<inC+O4mFbHS_1bjM*3b2$HbNY?;d
zqR#w2e=q@L$JHN`uf{=&W7oo7nsvZut?TP<3a_E{xy^r4(l6mV{%<kD#C+J~!YFiM
ziwKI+Q>>}`x&eOH)b|)qB}o4zRe8p-3)1J6hrcxG0X5EU5_Bnjz|s2d4{47cF#Ps;
zi}$HE*vG8>-FpECSR^zgZ@uUP9IB7hB42mFtQn6r+oz)-_ft;_S5-KiYQ;SlW_SYC
z*ad{*+{<9RVAe0K@qDmb+;dCjQ6<1rdtEjbkA(|yyLt)YLx4_198Y}K2!0s7TK|+&
z3T^5d<i)bGA^QjY$xt1%{GO8CT9<Ex<d05KDj)BJbO(uP1HD9eZLRtn4Gs^A1U!v0
zgT{fz$0V+bl5XhyY;5J1R0F)+ZPz*<*9)hv{F@*7jE5--aZ^WOKVZ4`QOV#r5o+>m
z_CK>Hf(OCk(g9`zFvf71e<ZpCC?35dFtqIl0aMQj%eEHi)BHm77kwL)4l-zJou~)O
z>Gwx#>Du6zo_tsHqE7JG=vv7`d=+>__aOZnQvvv)7`MR2N(4q*K@)m2?LZ(REHd%r
z0Fa_SBk|-^1$b{fEN~RP|2aAiaUKmL06(S~tGAa1VF>x};-lY2L9E@y7J8-;s3hP<
zQD@u+<C(bg*jfm1g3*La)-ebEURLG`ksSpZ)zuB2WBt%zA?z<FW*BCa91K5rz8y}#
z>}`y9?f~T!21Nee9td+UW9rrWp%Q%MKcmtK)7E^0WHSdq6%V<VV^}$)&CC5pDVz(l
z&n?|Oztjc9JoNAD^B2OmspQqwnmF+Fnx&I3`b<WIzUzhh<8~OjYS(=C#0VJAa|)XX
zn1o%_a^|{do9yU6*W<(Tcqov`GZ0VH0qKf-mLl$y!dB(piS*7AKpy--`j=ZjNW1e<
z>6hFH2-IJ>cf+O!#y1Qe6;pf-vU)xW{firdcRvb!^HNF&tbw+cPbRCNy1RzYS&n-6
zMOj<coFy6X1kk#qPj>=m@m)EW{2^GJ`*w)G91q-^)DkL7aiE0nW?M)m4${6{pnD@f
z2G$&-e3ow)gSztIMCXc8aL6H%Xw%&fpSJfteb9#XA%`8ibTF(7UTLf>GInW&H1&CZ
zq=sujtO+(e@%RvUDZsdJZn6^`j_ONtPj3OahsEo(Q`5m*r(t)lIUcxmvp>{dkA_~C
z&MNg=62X_v)*Cv~c;HB9Eme!wAB!knVJ2b+fOR90`S;CU=yleuIrMua#Nl|%`6BwE
znr}JFHJdtM(1d%EpEL-PdR+hJJ;KBBvP$Vxv`;GXZ-i!)0|9K`S~)|35a5qHe&?NR
zOM!>DBc_e52srp|Ty9IoLwnXo&dO<3&|hT7F!(?fknLy_8z789s%n`TKJo#OV(@XZ
z$t)3SMQkw*hxEfHKkkJ3gH7;UzpaE1`vh=1)*5b=S_7`%o^AfL+Y7mdWY_4;@sMd)
zsDI^p8nk>xkwY&z2=ml~BJ8<q;7;z9iBi3Exc(OF67?Y$YT{Z%dnpTmsY$ORRlpF4
zIrHOqbWRqOC~Y48XqgVoIO8M_xIxgyf8f_&v~M}FfPUAbqYpegX5rmD)em}$ui1pv
zJp`L4d5)(Z>4hGwadF<-<G|mN%c#V>2YL_r7g87(!i(dJaLi>CX2H|=tExn>hCBDk
zXN~}l7D*XlL%P97uRn>p8g1~WmU$5NMIY?$=M6m`-3}wG4k@K_^+3GtwfXcD32-Ec
zc|go~1df%?Gk2oTt{+{yBq8mR25S2LZe4lS2Cj?I*d?=$Ls{u<${K}mFxt8PD9pJU
z2vWxmP&fAg?D6jWAfYEPJCLy3Sc3;<?JqZ(EIObv)=TBn=`lFu5G4&+6QFd(@%k<F
z8A`2LCoVx14-AXAs2^CP*QrAn1T#(g;U}&$?#N&xIGOE@=o}aV4~|Q9uGcp~g1TOh
z*i0QTqNcz#<mJHT;eW?IiHt#>Oq&HKD?GdxVOf5IJ`n^^-^fV0G770-#9BAD8#Y*c
zG7{>>1NCKQjcpAg=yXVny^Z$$^5pcf+YjNOm_=zX721N(+E_>xh-!yiKPmovvqrCH
zn)1WfL-WBQvdZUn3q%+#>hSCROfNjD=+dO&O#mwf=3ZZDYXC*u*K0zNFW`H+Q=2<9
zO%SiA>_YXu6|nD?-D?yeLOB<u0-NeF&?Pvp+A22&bR!+_2NX5{or4lb4xOKXn=Ez=
zlJqU$muT9bUkn5gyDl-n+Tad2b{H7F@U<|982k%oGYb3ag!CU@7zFe66>+y3h~N*k
zdzI%vC#;F+`JyV010D%e6MpZ<Ag#>5*ykJspl;lo)!Ts9&nB<84d_aNNa#b!Y~2p~
z3nboiD|CT8HQ|byrABCVbtJ)JqYI4ooj3n%gw{tpqbO(I_5f4g3lCnlMZiOM4kPLq
zJbWL1w}(8-AF%#OB5PPqgA;7D3#tM`@SxS7=Wn-j;2%@lDZbWn=r7*(mf><IOs5c}
z-4?Wm+E>KQiSKbxS-3nzb@wq0=Q7O+$iYLMJNT0jeRllLx^J6!r56Z)K5V<alMVRn
zMaTuj`hen+!=;)Jqky_X$Mgd_7J(!0x=IXV7g(M-n6~IT4z931{&HYv3_MAh?OheD
z1XEukCPEv#VB?{Lv5yzZL7aAmf!WR&G&!bg;d^xe8sAWm<%XkR%ej}O@NYE~PK@3W
zA}a?J1dH{tsy6uf);S&dkP#5<q^ewww&_JQ?3LHN8laWOO_iJ5-QbtjGZkODemK(~
zYZ2d63_Vir{{5HG12e{M75(OD0y^9dxU)XJP$2gJ^`u5Mq<`jPpfETL`{K3YL|X5_
z*0XUVLC?^7h~cvDr6+BGo;zRl(T6b@_<7Nz-Z&C`*w(40`bvN)210fZc3NTjG3Bi&
z+g>0L;BbrFc??9gL>1a0I2d?ka><Aa?W@*)vT%oI2=*QkrQ=I12i9T7m8MQ~gVNE2
z;$s@kaO#}ngAb>oq3Ds55l9;m(7cnEfR6^C=BcO~$~gphI3n}&6nb40*`=X<|0)f#
z|Fj&T9q$I=4gaLBF^m9ng@0H5FEql`h~IvdPwHTa)nfMWW;s+jno7HA7zwP(WGC1z
z5J5-5It^D>DG;Mj9q|6#4mTbg9@ubeg<~u++>+=si<0{q(?zrB*o!<m)-6LEIFJ-a
zhcU*34w=BUrP~dFe`0>q^kqFXqPFH1Ju?Vp)v;f?qD$bE^YZEvTJB@+?eSxImEirc
z(Cx3^dqKwgL|eImejxg)maHv*0t{uz^)Igyzy#k_V3P!4ybvFDH6;&H9T*hma4v?o
zD9k6+<}1MU@lrMB$8CVkBx>ew0}fI%o{aTD%Nu{-#_}PHLZ}to>~tKDgXx<aGpbH_
z@V+Ew{$)5i7Ueot^c3|lh^P+K9z~x`7+Tx3DT#(dNv`3%@Q-=GU$#I|g#*2xTy!26
z@a=%f*;8jUd;5XFUu1OV0(u|1M{fCSst+W|oWi)_>p^yfQ`xhrG3XaJ@-LLV4II9l
zJ9aNA32xEW>ub*SL#x>uZ!5oHI5=5Sry1G}Xs-@=RhCu&Iv<-a{U7n5tAU(?ADI9?
zFP?-}6VS0i-g@ChV|aM3zN*w)2YuFQ*MhCQJ^-m))XR(4B0%yhDa&Lxv|T^Re>p6&
z18O}~9uJ(X0af40`Z4<5&@sI<{#3yr<W|%_S&O!7U-F-B>`$wJnnUJ{c|oJlB0h%d
znpZ5GJ5{{f!ru-K{;AOI8YzayL!Qb1NbiRW!8##yg6OlssCj;)mR9&#r~muSx<QEJ
zf1%`gVif4jJ;Cj8^+B~t_B;jhF2G#$<<R#UBd}rrkXB6U6%g~Oo%l!b5H=`&Ol_Vd
z0%PkdrT<nkfuXqBcvx^hcs-$e$u6=Kd>y=anKdIFvd{{CyMtaAJS##ePj=RW8?{EJ
zPwx-FYgn-?@~w2p`qY*uU6KHJG+eqK3-^JJNAkDl&@mwo6X{twh7+ORl!ijCOgmuI
zFg$VcbTd3ZDa#pMo(`4qSHrWXMu2-==y?U!Nl@8k9(|Uq8+4Grq)MdggtmuIss}F*
z0KTb>{#S1+z~P9uA^$e3&@n<ahiA_%z!MRL?J`(A7}KAyqt7h?^}dWMgVKFKD^rp?
z-lQJ{Fv~5<Uuy-U@og1M0-X?W_&V7Owt<o#V*M#Y*&sb~N~%?>7>={3=TUie0qXCG
z%kjgruq{B8_lEr_{FS*SShQ0EBXa0^J<P^IOV+wadTBo#e`v;MQq>M$tx&p0y~o2N
z68hDzem6jeo~NNle%8Y0yu<cd%wvGv$@R40(g2X;^t=w#YoXAA4Tt#B8F+D#&FVEr
z4*)Oqn|0?K!3vX6O{)MNUc(D=te=_!l&%>iGX}%3#nEE9TA~v~b5EX;uff5i^5*Tv
zW+Bio^4#!?XWf7=wKC7zA06XhTrU_VISRN5!n`G0CBX1h&@&6KKKS1A_$pmTKZusS
zR+)|rfLm-%D~6&i0DJE-t~PcIK4>f{WNc`FEDz;+*+=ov_n(mZVG{!QT0E`7BH92H
zq}yJV1QUR=>Rb5i_87Q)DMECerU#t-<iu}xXdD>RgcK3qw*iTuTjePdW3XzwILeEB
z98Uer7gjulgUqKS+xe@800YmGI$7WVP&V;9HDHJbw~mbBw!Zd5N7E=)VZRZ`NG~)c
zYCQrjpDEM3LfrzHnhi!}*~)<QN^OP7nGw)sCF0MANe4rJ8P(*<J3;S1&(3M)Dkxbj
zNrg)t0>W1dMyy`G1cn^^mJNbk@UOjY9hDF|^0|-Y$655gxLtgrq^7F^tjR@|ZHB)9
zu?Kz`dDjgCyC;m7k0=j=yo$abFQ4N8(Rs1zyE59J=oI79^0gSI%t$h-w0Xj$Rq-ym
zL;WCb^?dAyjYnwT?)AKGqbx|%Vm_ip9s?OMUuVmVabT_J=5rnUdf?e<p&}JI3~ThU
z1=1F1f8JG%7otBKpd5qcboj#oV7uT=l^fm;IE%%bFMO{9?{#hv-dIAA&1q-B%vcYX
zXL63VKg)nKj4=g#$wh#vb=;;;rybBO`ae1Hg9xs+g)+IcwZqE_ZEfEo@!$sj<+8D7
zJ-|Nq#P#}y!MYtpo#}K3U`B`TR+kX~lN<$Q=$%Ojngvbk^v0mw8)vDii9XP$L^<*&
zXB2)r6t10CPzlY+`tr5V`z^(y@M585CuE!m!_e4Q0HP$8VZd<$o~nKyS$}B=keObS
zFnaYAFedn3NcvR_I%+9ygqarX^=V%VK(7C9pEjEoITpeEpFZuX52EU(nbUx<v%|*i
z779WO>kOGn#$g-TxrIK99$+JI8j-6V04#N*jxokFaG;vz@`Hw1$V?^l_Nv<gG<Mps
zR=PO`S%U5|1z=|(UC=K9+JaFS6FC!^cm%DxdS#QXsm;PGlH=^N=yKWRJYRQyo`%F&
ziwCUeH4zRC{^<<tg#oL<3f75pkTc2WBGzvdC~dsIeYtTFJRdyjh7G6${dk=AS*tl<
zV@!vR;<a-|Uk3c|y-jqKuKsPrU~iP}jR3^(e|MEH7%}=kJKCnotaClC5WFuP_#D4b
z1@j3EDUgVEmea_&@c0s8;~&Ebvcxusz`HgJFKXbE?b04=vjTA9;{(PCyCI<Da&3dJ
zeH;)TnQaRa(1E5{dyf}j7%n9g@y;ne1KjmK_Mf}^fLM^Nor&=f7-E!5(vO)0QS9Ph
zX4l6-<@1L>m!@lA0Cm3Mef|<?pV5=CRgH%pCz#8;1O`BVAb)KZS_!sO^^E07MhD8S
za$N|`?t|3r!_HmFkKhN_%Lja<s{oVK*3MaU01~5%vR)240F|1XYI!zy1l%l96zxF=
zyy5DzG!k{wfy$)Jxs5I&WVsPF=tc+!Ui};xhw16PlfpOvx%s~(#TJa1{GXDFD`3vo
z>SzSXhO#SC^0ko3tmdTD(h$rkWJqw6sfQ^8uf-y+b-+xI^si@5mjI>tr5Z6?2rvD;
z&;iIs!1K*_X%Fv@fSp=3o!G=8aN<t_TboNZK+IF}V=5=WFY)awe<cS1!@A2i2a#4_
z%<IxU`2z=o`h{O>%r(HopLMONU<jToqV10~c?EdvM!u$DCc*keiz0Aq6kH0IzbkpT
zA8u6CmkgubW28qnrUA&U|J@_<VC44y*`trMkm5^V8QeR0{(H;S+{)9<%f=iryN#HO
z{LibHRxWNfd#?uXz2E3=W#MRnz7BcER!fE2S?*qA>+&yvSYQ!L3}R(VN;{bpw#{~b
z0>m1N*kF*md+AH%!^K#=9{{<BMQkyM-Co*GQ?jMk`VAoVSi}K?IPRq<a=$*(Bdh|%
z35z&m5SP7lLaleXB<u@7T(Jm(LEQGz{#AvCO)pmf;*Lc;Fo@?~+LpK0CjavzK)kSs
zHwN+9OEGd^9zMwU01#g+avy{E?WJ;}@6YKxeh(0TEE0f00{2q)R=e>}Ny`9vfJK5Z
z$iuyK?yTa=GxP5N@(7DO#vo7j(!NCQt4m5~8jMBIfe;~ksklk<3xInIkf&HA6oZ8A
zrO!9N3`Ac?({L>E41+}MrC%lxzn#@3fJ9=EC=Bv^FNGn8X@gVHG#ZP<V363o)JVzt
zW`;JJ#$l0o43e;ycBrKF4U?hi3oMd|L6Y{;l!^H2rTRsHBx8{j4Dxa>9luUkHStE%
zS6Cz!gQV@HOaD@TGM-1%bS#pAL0<2rxko#=$adcVBom8dVUX;-)Z(2k-$oCb=3tRr
z43f8(+I**ZSDS#Q`B<a?gB0$i%qlFuqwLVM2#XYBkdnPr{=kOv9VIj^#Uf=Gq<k;M
zB?-o4vY}}O7OBJ_RePzQ+YF!H!U905u}BRDsohJ1x66z^zC_bHEK-j_8urrFz$&dd
zQ#5VFB25^ic`qHu(RM>VG;P5m5FM(rmm2uC-FUV#50Ex2(vCrJd#Omc-iT@qns#83
zP7Knumy&zTDy?AAv>S``V36Luv^3Ib0Ng~=J}lCYK?e5HySPUsT@q+Ih((4l$naj;
zN87Uc?EsqMu?PW!5cg8$9*sn+**SoWV3AP_GPaji@m`NvtV7drEHZ&XCihZ?2$%P#
z6VY@Ei%esXnZ4B5`0kr%A2glCB6ApIelHd5NooIg6HOPe$Qul@xR;(@(2w^}MAIcK
z@)m=<+e?4rX|w+Fqv<jhd5=Lp?4{Q$f@RLopy@{}@(F{i?4?Q}8dnm(%>v{z7Wslf
zzV4;+o9@Hz(`dSiMb<FLx4o3pC9IIphNj=K$PWzib1$8yEPP{Eh^FgU<QE3n*h>vV
zmzF{j(DXMJ*~B1Qd+Csxr!hqkn*PBe+ZbeLFWqu3rPpvo)4y2c9|qaoOLZx)Y>gYu
z0yi@B9c!!`Ir<ahMuGm^<9LsaCk1LKA0RO$i4X4cTJ<p54_TC{NPLLI)cefSYZc=p
zfHDn<X-Q1C&oX)Q0xB#h)03Ei#E17;EH~ML{~*eYBxWKp^FFJ~vqrA`ngMP{NX$aw
zqx-BSrWjEC0cBPavyqs6pI3uk7-6ST<{<Gg5+C1Zo&&`UO@k<Nl9-FcC-#{E()t{0
zLz$bzJS67b=ZDqM>1H*`d?e;4vA{liRC_207oaRiVj&U>@3Yl|`me@mD4!&;2#H1a
zIn*qdAuA5$QzRB6@#%dQH88(a7>2Sqi6uxZxzERNr=`~)pe#jVX%frqGYe10fV&sU
zvLrr3;<Ni)6R;+H%L!#U5}zZn{61eMYQEvIMp=Qx=Sh5FpRK4tF{dfYiX^^BVx@g{
zNiFoeg+UpRSee9^_F1<vHQ=fy%9ly3LSof@E@h-|GrNqk8j00Otg+88>}}=)&ZB&V
z#8*kIxzE|o#|xHZP}U-`Hi>oid5U8hePD{RE{XL>d~Khfd~?uf6h!$ti7_PB-{;rJ
zY@7f$$_6C9L1M#wCJLTD=){h)5sCl%p9L7VoBPasZZmz131t%!o09m}J_o79ur1J{
ze4E5(BsSk?Y`u@1EhWl#NNho3%YA-xp=@{a?=*0;BC$1zZT8vLi*ng)6Xm-kzDHu)
zeP(f2Ec^2VWjhkvlh|RORV6H%!oH&HNMa`vJMZ%%`kEf{5oH$=yOJ2$XEWSSZH2ce
zyOG$P#2)*s_0<3M${fm`B=#b)_dZ{HS(NTNg|ZKceMx+OpIwSPsh*Fb>_=jM5(n&a
z_CKUFe;DOJ5<ehu&_1(K%_*k#qx_J>k4XG@pO;!%XT`cvenR445@YxI$c&`ePCLpW
zBz{Wb(0#TWqOgu^K{<@X;Us>x&tu7Xq9F|^N02y@#8La4MC}!1RfF<#5=WCbW}kg|
z?Z<HyD94gGj>Pf%>`BMu^SuP+1QNd>apFG5Jj%YKUx;!NiIYj3vd<^gxXQohp!|}=
zuSlG_&(|LFy`?}Kh;C^lPA753K1-25TMS7<`8A0%Nu0IM{nNu)(<vxtlQ@UOx%+&1
zig^T+h;kl@^GRH=&yO{fncCw}E+lagiHrBymFyq0P&CRVBrYX!**=3Cmg21uD3_DC
zg2a{kT>qG&G&2n4DiT+dxMrV~vaJ;_grHnY;yM!7@3W1;!ob>Nlp9FgNaCh_Hn>f;
zVH<>UGl^SB4EOn6^zX9s0VubUxQ)c^`&@SGig%|k$~Y2tkhpW7BY59nbG%UQB5^l~
zd-mCSW^KsN9pzpU_mQ}NpZouR?A>`dRp0+VeDgeIO6GZ<=kYA_JkLpHLQyHwAd(CX
zG9@A+Ar(?dC5sXjX)qSa)L=>|Lb%s^eXigBzQ3RAy8piK-*xrRcFumBbFOP``|N%8
zYpuPWTF2}K_aW}Y{0egy!Fs%ZdJQ8GzsCFq^IL+qj5G+kh9iE5xf^p2!KrT2T@xXQ
zdolN6?kAXkoJ>e22=M^s_n1Er{GwpHpRzyVLCiy#KN9S63)0H@ARfm23G)cSS4KDo
znRg%_#XN?2oZzF^9ZV-Z5KmzKjQI<}GrE;}cU=*G#rzHPB*FUGm+L<|A)dlKjd_M(
z37K*I8hgaEn7?D5BUs-jj<ebZ@ej=Nm=_3k9R6hf-V*U5=AW2<5gheiqHNp@@e<}`
z%)bd<`J<LCVuE-D^D5>wg2~+CIA;LjKbY4sZxH;bUQyiI0P!YfGBhKPa}+sR{^zG;
zn*ZC#3%ZCY(5mDpO3YLQJEylhrfMOk#!Q2mmf)hC4V7wj#B`YHF*6WMwsGjRgeqc2
z%uJY>2{!M(8A73im<2N{W;TNNn#?*!%Ol=`nH@6+!DEvTTmxhfb7JPg%uVo-iqx}3
zl8Cor=E1y;;FgzutrX&jc`@^0<|o+w)ebLC5yS$R1u+W|TxXxCX(WhP7_$gwQG(<1
zR9lYlAr`|dj#+}>Qtt;T4|otuVwS=zP4JdIJI3d@5X)ee#Vkj#J;jCFF&v2HF)LtJ
zB=~6m_Vjc%#7dZzF{=<fD47+^#)4QCvl?b~f;}GA9p`35tbth*vlhWOQ<&eT(;?Qz
ztb<vXU?mQP)mdu9dYJVw8xZ_*sp6(9C1Mh0fZ34X*(+s917wJeFdJhwAy{=HI$mev
zJ%}>JY=+sK;0*J|KbO}KTVS@tY(?;E-mWf%6~xw<Z7|ys%q&tWnYx774zoRG2ZC9B
zZ{4u{iP#ad6J}?EtyRdLRLmoG!R(6Jjo>^VQX0z~Vt33QnE#p0;6Go%`dCKFp&7*6
zF?(U&LGakkLy!5V5PM_ZiP?u>riv`((yxepG5cZmC-{};Y$W$*!~vKCF$WRMzJ8F^
zXAE&L<`B%g2tLtiCPXuWI23aj=5T_SlSUuh{fKxs<~^7r2>yL2HS_BS#F3cyV%|rv
zRLCv4^Z~^CF-KuOKya;yAC+kz;)9r@F~<-*fARNFWH;hNm}4;?CYU2rZp7^^;v<-k
zVm?Oj2K{+ii!Q`*n2%$QC;0PhRmIj$#0i*BU``}Bxc)m$@e9N!F(+X@MevR9iJ!K%
zBTmMgf;pAo3^ra)zgEO)m``IqL$F|GbH$q$#Ah*|!<<g=ks$$vK#2G}<_yf41b@$k
zizAJQvoL34&LOzJY@A8=A>s>|b1`2eIO`(^#pr#+d6@GtUm}<+xc`y-J;Vi=3o&0N
zxV&T2Xsix#5$0mdR|sx=sHGQOi?{@HDdwvL)6h&xtlvUhhWQ%ia)JxS?#=MsKwN>j
z67zL}M=#|_W>zAu!h8esO@e2f!%w`whWHldYRokRCzWr7fmadNV!n;Jj$kY5TRCU1
zAijh7F6Mg#Gq{y_PG3e`kNH0427<$@cW-ejK>PsnL(Go|&KRqkzM6-)5%XirO$3*f
z%lc8|B8HfoF}D!hKA})0n2q=e=BJoj36?u`Xgogy@iWYAnA-_v>WQxSd=Bw*%r7v%
zBslr(8><he5qDtj#QcijI?n?c8!3ppFu%t9hTvq0&mVbDA%2Vb9p-L=gMLz;ElNb(
zgSi)TAHhD$9qZ}wi2E@QV17?9+pCa=TjCIZz&wa~h~U)dn9UPM5P!rxjQJD6zZ<Q?
z`VJu;!90q2jNp9}F2Rh^h{rKcVE#<7MP&Uw-zdaiFn`7Tjo?U?YOcn;h$k^mVV)*9
zaeT_xIs)+w=2^_&3AQDfHh704p2PeD^E|<&E>+X-cOhQDyomWH!OOlM1k{5O|H8b4
zd70pXbF|M61|a^8c?I(-!Nx2ux-5Q(*D(LVyiPFPqUt?~orpIuZ(=4xGlBl+JjN@@
zl$7R$m>jK2j-o(I=O{{oFV3y`KlDIMg_#;N4Z%;hlG!S`A*RJlhnb$>OMgDr)H)+(
zz|4r5iD2!BYmBCjh?z07U}hya?77gvcss;wn73eNCwM?_OCFC6Vh+rln7Igk+W+89
zf+b>Z%v&+@5PXmIu-;uW#M?0QV&)^bB$wQf(F8F+W&zBC1jm^EQaNadSO~K)W)Xsy
z3o^-QNQgx-i(wWgcrN<blihlVB``~3mLj;nEIs<OHezYaGMHrvb{}bVKctCR4zoOF
z1%e-$23+~2hFB4^5@uzBUpAP>9#BE7f>{-_8o_Xxso;PTVs*?Km^BH`sqV`BD34eR
zvo>ZOf&<B^R`;N>8Bw~J^)TxbY|3Eu-BKE{0cH|rKyZ6KZ^SPN#D<uSFdGw`oYL<R
zCx+MrvnggXf<qSHI$Me$Hpgs%*^*!*sVvP|LBv*=tufmW9Hb{JeuW>gEoM8+_5@$5
zeLsJI7qJ6oN6bzHm$!%HU*$pUjM)XVE5VemCwSSo5xZe_$LvAy%7%9QGY-W6%(;~u
zwH>n;!Q#<jWwBcj@4)Phc_+b#A8=c0u_E@t?2Flt;Kdi2Tr|vx{V@k%4kUOLI0~Wr
zAV5?Q=3vYr1g{G`=V7BqybE(E<}iY9|H^#6n+9<>=G~b05FD`fr7?{PaRlZ_%zFuz
zr>XkjN`ZJE=KYwX2=3X)yC+SC_yFdEn4<|kD&uV_yfFZxVlW@V97}M4V$X2R8sfv4
zk6=DZu;PqN__Gzn$1ulXK2Go+k!-K#WyJBA6EL43cvpgs$CY1*6EUB}oJ6p4Xf<p7
zBH~k+lQE|doHLMeyLBFMD&{oIrwMLdZQ5HkhxiQUvzX5j?B=(P?BFcobj;^5XAt}`
zfNUvY8gVA(EX>&ik8|JpvuhG@4(1D(a|u2r`DY;LE8>fo^DyTVjBaID(*KP366ONT
zg#^FRCXe$QM|>G`5$0loFC419EINw#3g!~br39<SWhnj}Mtl`>8RlyQPii{TGkru{
zj=2JJCBc`&3JXOC5nsn#h4}`-!gZTWQv--^V!nmBnqZmctM<?O5Z7R?#eAFKQNfYu
zk3ER%FyFy^m*BBZF}lcii0@&p$9$jQ$)%w4QEw18V19u4A;C&c(;0GIh#z5Y#Qd1x
zD#qI>@|}pAFhk7E1Q)i=xX`^s+=BTD=BEU2I#O+^Y)9OR`5ERmg1t^isO3LH+>ZG<
z<`)Fdj+{65dW!fZ<_^rA1VhUG5!o$>Ut#XT{F>l%u3P*9AmTTe-(r48aMPwallx=D
z-I#kY_Y!<{E3J_ZntLIt4|6}}0fK+s`CO>`0P%awA21IRoYSIVuXrEv5ay4VhY7y9
z>!Ht{dx$?_9>F|HFzYwn0l7Pf$1sm$o*;P6EZD*5Hsa5izhM4KFiTe3nQSyGP}Dcf
zlbELnE_q$L#pf2{Y0NX2X9=DVnk&9?1MzpvbC`b+EH4JKTCXFX$Gm`fk>DI9`yUGx
zh<{@Kg?WkKlY1I{D9aHqWB!eKg<x&Vs7c*2#H*OsF#jRA$wqOTc`4#`%o~_D2`0rP
zp?j+klcBlzQU5nuqW*8R{Le4Vf*ZS6=86zgVy41OO>mn`U3J)H#59;`G1C#;s&>HO
zV*z4%%nX<r36@XY1BUYvGht@N%tEk(%NOOvi-=h<vtiysu)URMK4UIocFY`@ISF>@
zOh}x}M$Cno8}n9z)jwwkc%eaMQ9PKpVdf=RVf?Wl%s|YCnIE$N!AX5bg%{Eh3t|?+
zEKG3!QNO1b(XH`OBA7)nixHe{UYU3G3}SK25|||kR%z!TXFiQs3bQn38G@(ZdBv@!
zAeO}}hgqKB$AK10<EIcSU{=JeL~xkp^pWjJh?Oy`U{)nq@5dX*)I`K;nAI_B5WJfE
zi0X9$Vol6in6(KmsR>;;8IM>8vo2;mf-4HEXK%zI*2ipsnMCmC!WUA{jv@w_4KW)L
zJjXD2UEv5~W6UO)O$lb*-kv^x2(cMvbIcY5lZ%2Str*0Xn5{5d6Fi-l?_PZnu?=Qh
z%ytAHKGT22{QzQn%nq0x2^LvZKb)~2u@h!z%q|3f`~Iw?buVI9%x;+73BI8JvP(S@
zu?OaVA|+7&H^I?IO3F^{LF|Qj2WD@A*T4FQJq<^^6SEIyUxF9bi`ync5&L2G#~eU#
zR9d?E>Mq2An1e6}6KuIvhHW_*aR}yJm_rG^J)+&w8H6|tb2#SR1V@@pt84}!-h(*;
zb0opFH?^9S{SoiQybtq!f(02rQ9Ju0j>3EZ^Fe|WK6mes+le?Da}4G~1k0&C@^tk^
z9E<rd<|72>9oPIp@<MzR^D)eE1hcA{$@qFAK8`saa{|FX6g-@J-4UO_oQU}(!8uk>
zn3>!VCt*H?Iho*k$(Mp{E{Ibwr(#Yc`0)13fMqAdr!k+we3sy=E(=eABjR(I(=neX
z*!&z*qP+v+49uCBvk1NqtE&=i5ocr0!F+*WuHU^*AFUDRV!ntukKpG{<OPOSi1RUD
z!dyTwNjBukra9t5%$G425o{P9I2>b!xES*l%q0Zd#qFZ;F-2U8`6}izf{heQIxUS6
zU&CCExq{%+#j2jIhKMUMU&mZU@Byhq!owuQH!$DCe2d`B5yQ9^1H{#sYcSUm9KHHJ
zi&7u)ZOnC;?+{#X+sxswi})_)dzkA9Zf<j3Y0yD@A9Dld2LuNgb(HScLi`Z(Bg~Bi
z|1e@Ns?|XJ7;_V5NU&Du(cvp<h?_CDV17dI`O()i11gB0Vs6F!j9}M*fIwqq#BG?{
zF+V3bW*}YWwIbpdm|tS<Ao!7$iKDFo;!e!3Fn19gZunJji#+1jnBQQ2OK_ZuyudzL
z#P2Y7W9}jNQRbOl71D@%G52BaCwOG*NUqR7Jb?K<<_`ojjOPC2lt4U)c?k1Ig2x?m
z_Z|~NJdF7h<`II!r#LSD6hS<Sc?|P7!Bn1KUYr$1Jc0Q$<}U;n_izia3L*ZA`5Wd*
zg0~5A>R%B+JcW50^9;dGZ!|x(@FAYX{2lWg!5IhFJOg+U|G+$td4XUx)u%IqJct)D
z|HS-@VAqFR`kl5SUc$VL`8UB<GZA@Lxe%{lUd6mda7|NdtOY0HKbY4sZxDRff^Dph
z9q}e+GBnvZ>i<T||NNA^q9J2uwFNN+T7~+*F;fwoXe_no1uJ4|%ruy32^KrFk-3Eh
zF&$=l%nStUOa#r`VM5G^nF%v9!2_|mNh*wpSunF=W+T{<Mf>dwdc<2Wvt#BU_*ACh
z5)U0>PRv}Gxd|2;xM0Xhi+C$$9?aVap4$G+lA0PZFJ?Z>`~>sp-e-!TL@a<=5VH`$
zLwBE8QBWWj#w>zal;Gk+sr+>0h{Z6AW0oNJz0pGQmQ8g3xg=&O%+dt=+73^buOpVh
zEQ?u=U=ds03XwmE<uNN@RwTISTXp-$Dq<zf%9vFMCNBz0(p*8TidhY_I>9OJ`<tFD
zBi6vIiCK%_Nga#jD@%yAG3#K~CAiJUhkg1dVm-|Im<<Sa5=mgpTSQF43@{rKJmS+F
zC%=H$2(vL}6M|Pdb5ET3f!GwY8D?{WBQmZVa?ByNz-)=xir`2Cn~nHc#MYQ?FxwLB
zSV~bwJA>E`vpr@9g5O2=ndD6&cEs$2*_mLs3PTIpNyILgT`{{6yq(J>Waukmcg!A`
z|C#mwKVLzzvPZ9ueL=h(vlr$a1V5>L|7>;wu{Y+On0*KqAJ89R{fB)q`(gGc_@cD_
zW9~7;0hj|Z2NC?~v$o>x5yZimLon|mxcAXbao<mfLotV84kx(1s)euVBjVkd_h617
z_-<OSnAZ^ENX&aN?<3eZGB@@42gLg^M`1odaQ1e&?wjusAH*DuIfmeyVm;T^`Vk+(
z9E<rd!FPm|8{YOIK7#ou=3@jO4X}Bk-HSL5^Ks1a1c!{aHjZ>7PQZKub0Wdd=O5io
zeTVoY<|NFg2&VPcI5qVKaWdu<%&7!F;`C}e_Zo2;=F^zZ5FC4-zd*YS@mb90FsBo2
zkV<v#V<+PCm@_bE5?py@`%rWT;w;SBm~#jYx+dWh`x5a5%(<8^61)(d%D?d(aUSM;
z%$LwI*pmz0miA8&Tv)_D6jK-cPl}yy;NIxSgHZ*@Z5aQP{)bZRaLS%<R3Ta${?A)6
z`0hn1e-S8VFDmLXTKE4JT<D8p?oiU)frDXz|MQLm3`Qt)(Bc1l07Fy}`UII1QN`#_
zT+|h`l*XwR`~0K)BMwBBpcOl#O8;5?&z)0ISMkfr@bmvkxQM!jR{XD<sQxBb{7tU-
zn_TfXx#Dkf#oy$LzsVJUlPmrvSNu(`_?ukuH@V_(a>d`|ioeMff0HZzCRhATuK1f=
z@&8S7MN~O1;#K^si1$x^MO5W~7xDf^Sp1E!_#0vIH^Sm?gvH+oi@y;Te<LjZMp*of
zu=pEc@i)TaZ-m9)2#dcF7Jnlw{zh2*jj;F|VevP@;%|h--w2EUzeHF>UB|`ks(%%?
z{|T~)y7Aw|?f=`dEb#E#o49ee{?#~<Q&H9b-L${y75|f75mkdXS?#|z8TYBE+yA}E
z{%?%2h^oU)yYsK6k)MjX``=Cb_J8s$LYXW8ujbe^zllO9!+-9UNEaRB4pinvGen?w
zB}dhxrE}E%e?EcV0?bu&9vQW$4i!KTm%H9`r^tm0T9C{Rr;jP0)%8RZfRY`IYCx~G
z3*zxavp12U;o=X_)16Td(egjT#UG(ZaZ!!<`M-vXKSnG5*KqNmqx%nUu@nZs<vkDV
zxhMw{t=id=_p3qGr@m_?Z;ip(*3O5YgjIo8iPXTAEDFdQ@M2-gbt^EI^^@(L+yyNQ
zn?Ewm8Ua5tRk{4RuBLl_JGUDfi-82^T@LogE#SNHf{)(2jX<-JDU+OmD0J!^I~<j-
z53}tTUaBalLj&%WxWRmN(9CmDt7%Xl^tL(I9HTb^Qh`J7^cAImut72HAA42s#->Kn
zuAB>|K8<@jt0xP0{mJ&;@rf6VCjJNr8<Yci*Uu~OQz1cZ57tbxt7`Cl-Av%Y;~H=S
zURifIE&|=}lZo0h?gWW80yms2jevR3L$hTAUATHrxDG7PLs#+H<3@TG@LGiHmy?$K
zaA$J0S#FFP+)?m$laI+1o>Ws5iD5H^j6HBz_ly`Q^NU^jL}L!#l&jKvjc|c<x1Wg#
zijGjT=B_l!(-6jgU*iuCPz7Tzo+q`~8Gwd`#HEZdCwPm`@6zy99e6r4=wMHxI*{@Y
z%Bz{OfM><VEklP{V0s_9*TP*D;Fcgx;eSXSoFd!!`YzZ3?)!LnaV=`T>GjFf``ey4
z!0{8CD(6u9z8@G<cjq$$Kfje5yGAHOHX7sZ_T$QM#Niy@_@)}LVIt{l4p~9LQukxF
z3^pL4<H36Hh6S{Xq&_6N&IE>v+L(jpjKJ~nj%Gsxd)R-yP)OLv9O_%Wwf5h`3)zY$
z(kcR%NV7VPveP%oq4-HFmJ>rdV7O4xl21?p^oZyyE4WJl-6v`F1(Qy|&V}YS9fLBo
z`|)9^f4dUwpKvYa8xse;EEz}SyjTERTd<tr>Q|DXzBKEa_#!DcQ~R`wE+r67j&P><
zvf5M<)qbeH(gg00@T00wG69AiWNxgw%OqP{&#6xphLEjC(6+ApS5t^;w<h~bBaj<v
zq5o`$79_iVnXPhTy@}_JfAOY>8wi+C3H1D_3Rle?xXpHvgFb~IN>d9lz+dwAM*$@x
z%mU1{DaOL!+V@0Op-T!-H|_`=Ow$FW<!9g8S3AP{-!B2G5OsK6$josO?Ju55hjeK+
z1mLsq-Cq(8Izz`v+XcQ%UFcpP_hcqT6VwSLEwIiRgM>$qSr1X|flO5Ada~Y0Kp{2e
zKiX-+@W-dU`6@cXz+ji107JL|T$(8u72;NbEC;>yj~|zVo=#3pj}w$3{f*zGc||GM
z<jL=o^Hmrexk?eGSt5#V$vhpLAEE**@3lWw`DO#`q;q%P8kU0PrIs<WSLaEZS!FC9
zJ=##*!oo`;ga+tFTVz-8X@HoAKT1KnBAOy3kGaK44JLg0BY$C=A|!9(sw|Syf{C@S
z{7tFV!LI!wF$!ousG>g*C7EXnO<#mr%<|iT8sA#xYuA-w(4tpEQm_Ox;5!t0g;E=`
z+=xt0);5K`-<+l{9y0<#{fxVWZgBu7t@d-gL;CQXpt|T%r6ahU{FZ0GmI!oIDT$g_
z2B4(;PLdP57MM6KW~aMD79<IfTkWoNg0E)YXMA5;YhqhxV4Ha(2R^mOiKlA{Lv!-f
zbc-4%ppm&yy?4JJylqK4d3DJRI3^0@SLJDdX1hojsb~#X6k{F=ENDV;mBam48g$`M
z|IO#Dj(i~Z6XWMfeO+ioKjM~rzzA$vxR&PRqyqP7UgoGJ+ayUHQZOIP(FR2+5q<X-
z1>o{xy5n1Yefa3l(oV7_88GlXXhQoK2k^Kcb%(dX3VztMvaY?$1cQ8~k28Fh1AN}i
zFGXl2QJ6S8RpL7mXl<N(#eG%>E}nYxZMV2HbhvP6q`EH{blPPEtI23XGUdnj?+7bE
zOUkS!dv+0cmO(s%<AMq}rZq~}Wv&D|S+s=rZa0MM)VcJ5`693b&IF`x8i1S(!F^F>
zX7HfI&tNKNeek{O!_C-yH$W5gWbUcb77(zJ&0WD^1b^4d^m@olkVwpe=@HjVpws7{
zTe>J@z@-8y$7oq)uzDir$z547_^_Nzouz~cUJQPDNn2S2T)Ce&TC4wo<Tz)sX%?~-
z>@~a`9cHQw#BExgw)txUbyxScUHeSo)Zj;N2W3w1{C&QDpBNWNusShQWg-D@#i$7c
zUvvO;`AVkcuS~&~f`g7X1{k11-xEK<Q@Y@Zn1vMCjZipR@Gk9GGzqXtdYe?eTW>m(
zY5R%1OBmGZ*h<Gm>%iKqEBe{&T!1Ox_Fk_S4=~)v)fs(B2C`+JQTn>52YF`dJ)5_w
zfXk$=cxCjxi+rlU=wD|D9DL$7A3I9{V`{2b?-HbeDE)KcbJSuW_w4UOD}&0=%`NV3
zToMnQy^^9h<E{#Rin+hv@<0qWWrRk3>EnTezj76NuIa#q5-!gbKR!5XWpT@EU<c^;
zki37M)(9loo@0$=B|*b$Q7_B6WkKRG8EcU#doaiAT|e<i5@a?~kL>3ZhD@O`$qYQa
zFz+$<b{cU#=oi}(H1J3oO41w=I(Sh9(r*EbVXV$@vD){Cn7<d?ctqu8p)3!h**eZl
za2SA*8`=I^#nPbXp`}BlwGV6(J~nLBq7LPC`1sT)^<m3YR0p?@8|2Yd$-noS1P-O{
z3zsvKgdLHuD${m~gRSQ-xsDe)0a3-b*5PrM(6c$-gQd?9Uaoy$muRd3Gwy3AP?e|y
zod_LzEpZQ+9&q#;$0;o!?(ZF$F=7pkvn|iaKD2`eUhI=Ix@rj;r@xrHMYzC1`;7K)
zno^)mMs0G8O&2(>hO0A~Zih?*CVDUA1%TbuG4(_mbwHVT;Q)&rKkyM*<Oz171o{!{
zUZ1uI0E^nA>Af4u;M=?6NE${X$Y4J5TXoq8reqGz?E2vfoh-P%O)e;clX9g8qd1j8
z_%DBzco7L;I9$-eU+4uHv}!2k=gD9f+pW+{CqqbfSHEt8%MjK)7muThR0j<dms)N5
z4Zv1y1%0s23+x^aiRXuz0DfDA_-1cAc+Sr~t)$orSn{0U2)(EbKM2{J|Hy9$#uI#3
zQg54s6M>wO4c)qcdjA-^mc0=i2zS(z?^1<YYE#?S4V9sk`}^)EFAd=DX!*s37t*kT
zN%;r;VR67zRX3lI9t3q3`gu|VRbZMB^&9;jeGvYI_T<Y<c9_7EpkGvC1CO^vnOqAq
zg8M~mbl-`S0LR+ryK*m$0WkQuYP5eh%-a^gUX&&Tjh_mZvu%-rFK<>Je)nLV)VtN!
zgtHr+XE^LVr{aykvo}RyqNNIOaPhf=!L&S_cxTCH9i<GnHI(~Mxo`kIvdx#uq0)dv
z_VCczpW=Y-YU1)G6%+77d{Q!wZJ8u$o;Ji6F9HjsvQCvhK*yt~SGOo>ctKgs#*xHd
zvVf*K!aJ>A6f~@J$(7WrKv}9n@H$2pvczt1+6=2e<u|krb7WRv`_9W8zHuhNtG+Jz
zbfX>I<ZUbQ=5qoRkJ|#<UdVt#m8{tOehDZM^=_HD$_NUNiEnwyr3;zaYlC)Jn1f8W
z&KSodoY3-Wo)dp0Hy|bI9`Nei0Wz~+JdQ4t2K#uo^5<n50fzMh)E_g%fRNtRv*q0q
z;7ZYg<*!v^X#av)@S@%t$@U6KtWJ#{sJiDKKdCDK_q9(1SXf$sobAqMB?YbFhw}Kj
z2{UUjzPowdyxa^V?Qf<~GO&WNYWs_aLPX%t7dx*|M~J`^gLEk)ONP)>E%$?cn+24)
zH){0!0tu)FD{=fN(uZ3tvtHOp$ipax_3h<oKR4XG!&5g-4f5;m-!9py2ir<1evKQl
zLb4tX{>xKbaB^vLM>x9!td43JTGkSPZ-(k_D}{4{J~zJuWTM*8TFF3m<*F(8T2Nnl
zWWoYg(m!l>e_{)T{E|pxL&1P-zuL7A*@n<$?RZEuwJKECF87w9RDq;EuIpV?>Oj;W
z$$0RzJV?}5d9d}p2GsTasYtCL0?pZZG9uqwf#W~3{RM8jLIZx&ZwmMHfp}TEuZ4{(
z_(Ja7dYeZZ$h#ZJp71q?IcGEzHIA$_No&(tCazjQ9<C{#iw8+Sj%{dR*h3BQz=8bQ
zw_1>j@!ZxME1cj7?Wg;n3gmz%10x-G+I$nqMy!{ajv3l^GYR&-(E!`1Wp%Y2WWl0S
z^!oWT>QLp@<Sj;8O)&YPFmd`5Kcv$fOH&C^f`-N*bIIQs(v1ZL-#KUqd-%+cYvo9R
zE3FL|?fj(S=}I1n0!}AjIPy4P_>KppD7T{t+(r*Ag06bqyJZap(?th<g{neo(I1Vl
z*cE1EGcZgGtHGq@uj6y~i~y%4$>|3d8_YRras8gB5Ga$06)AtG2wgTM`oC|O!k1gr
z*32Tkft;4oMP>s>(B!UugZrrwsBQnEW%$4tRI4(6(bKko0$W_753PuUC$79R_A-`W
zBj<I?@hB~L`SLSEWoAQ|6O(x8mY62&mGZSJe6$0!5Aj}5CO3x)9gWB0Uc17c0-Z3f
zy;6XVZ}f%S2LtFi!ADOTF$06oD#lK>^F#Tv6!AzdBjCto7V^bf74rO|E+eIH0T;}d
z!qfBIVDwh068UvaXkIAnbLyuq?56yhXyGXh7{R&2C)MphS#t;Ioz#KepI74ThB-lf
zlJ(JrPz@+AdC}RSOAHp4T<7YM(g8Ow%o%hS27q?f=rZq90)V>Vw_=fp0C0T$R+Cyj
z81Qg%`E-ObL4nIM$%SSnVBYbi;7W!D6iBn{Apc|p@1N;1Pj7I7I+ssGuLzq%_X7;y
z?LBNDr>mrh-O37yCqL55?1epOV$-D(UvdU@WJQOn`-I^))<9$RV04}29dYAOp$;^-
zuAlG6>jxgXi}ik>u!Xuq>U5F)=D<O(t{4njL+h$C(8x~$-@_^EM4sD0MSZXHv+`)t
zKv^dlnMF60vq?X?)7BK4S3Vl!uayTwB`n7*zN<n(wLKTrZA5^B(~E$}58L4a;Qesb
z)DxaqVR;?W=>)}DU!O0tRfm>WB9q(VWMHYYA;;)E03P*Glw5P@_e5~0m%x1oK>0^D
z;m<{PICs^0v*jo`Y^YoH8Y(x0&xT6ITxeWDXqeK{^FjcBZ11*hER%zs*BEXE=n4Xg
zpRXH1q6rL23D`&(<N%U7=j>nXK<EGb`jW<TE<k^=ZsOI;9gx{ZPvc#mAG{y1d&@H~
zAE3U~qE7|5!wJJ?x``$kc=z6&b2I%`aL1qsdrdey@Q#o$;~b-hTrc%)vo1J5L!U&e
ziNmh2KI?+q^}AN^%5dpOlgIC-gR$$n7uKA>v0Vy|!M%>~`|X*%?e_(s<{-_n>)C3s
za^sQc-7DO{`~0&mURrgi5Ayb~cl&`Pvoe)$)8g>8ozm?4GItPNY`lB;=pK-IvguVS
zNg3=E(Y@PRCl4bz=nMPV6kzyo;mF^|xk1<+^Q&e8F0ec?z+6hp6nN9i?Yekh3l221
zoSZfFLKAumr%Re_gJ0(}R>ddnz!l4+z`Im@AeDCf;s}iyps0}IbRTtvW2{tiUL@J3
zhi=c`*|5;-2dnH^tyqt0>Z}@*b-Iw%$Tr3o|HsQxfBz4m{J@kseV&6|5w?`J21`{Y
zDGgmck58XZx!!ttLvLuUE@Ga2pt0<iO8-F6mBzs*mw6&acaSI_U;ksqo!E4i^vbyD
zMtozeLfh8mcp3dV>Kl`^bYzXy@qW7m^+QP^4X2b%>0_JZUuU(kat}0CUWgu*i@I#^
zh5vk}=BFgR&Vt+K?4OJD-;g_3^4>LkEZ>`1te4H#<O_|x=pDc6C8XUoeI$49Q5kF9
z330=|#&ees*(T*mHI+WgDrVE#VW5+s!|Z#Cys`AAjVR*_C;he9PvU(HlLj7Ia-}2N
zOd7ZTGWedhwg)J-=+|flt3y8T62J3qJdjOvEaPG50!e7<`Tg6uy1<<2X~2mAYEX0Z
zT8?kO6MP-p$eCl{52X{+3S>;|flXBDoqSnwSj|~?$XCc1aPJ*mp}t`O+eg~f%tI`|
zqu$=I%+F?U)@8$5R?-O;n6(V;$})wDS~Dk{igkcG=cNKMCk0r>DcaDorU#}4<Q8^b
z+W`U|nj9_O7X+q6ROcTqnE}~^Hrg*Ms_=rfzHl3>8FU=7pe$owCV5Rd$~9*=fiU|l
zL60d*_*qRqMenIC$eilFxNM*V7FbOqYLu)%(k>@IF57vMu+6-|UOQ3H%Hy3eFsTZD
zXP@T!mSqU@1AV_W9(9B&*8IKY)q0>tN9kc}@>Y=YPz<=-6axwYq}2o$eR!pU>@=OS
z8?5w@(tMCB4$hqE5LGj<0G*CH?lvtNpwVCOcCL;INM*`;Tlx46Y2O3hRL2P$@M<GR
zVXv4lxa{V!^!TYO^e+>jg%M8hXE4KA{cIy>x!azO#bBdJ$?$1fRuVcd9G_~a+^GSs
z7PXVU4G9Bk8p#Ywbwi-fmuxu~z7=rNjY?^3{Ah9&N^q6fv<0<ij<FT+kbu{P4|mBP
z=mKt?!IXj(2N?fxerlhGI5^1#GY`tSLy-uZGWtwMSR23Fbxgz%82<Xu5z(QC^M9k7
za1jswtB8k(i#Pvw5wBb-Inhwp3p}vo`n*}k1jJvFl@^g%f{vAg@khUzgWj<F)YN?2
zLEVqOl(yASl<<B1`D>FMP%-_0*b`b`$SncHH+O0Roi9?wYHfyaWBa>)-OHY^D+$C1
zf7uE6weP9Ro$!aDemz$A3*8{^YU{1v2?0<_ymnT1X$MrhpJ}Eq=mv+k4o9D($RN#!
z9sb2T<O4WN9=YGC-VaEdCdY0piGnlvQxf43fk2d#Osx8;Dqt@>>oiz2PI@nKcS9>n
z6U3*b<<dDDK*!%2BP^zWNb`d8Cj_RnA;Zh*{5yxGzz%8Or(`ExfidgoaF2@|NPa8x
zoz`Cpcv2ZG?QJ&%3TE@}M{e;$OOFpb)E7x$Dxvl4&$UHTW+G#*Ixj!it>;2bUIw5x
z*}?nC@l#C|SM6frQ;lFw(C?|vFay{JXx+JM1YqeNiR37ECg>OaCp|rk8Ok)fBxT-`
z0>c@@OS|`fZ>rs6Xn)8SfQb`Vhh@#g;q{54dyE$Zz?;Xp<Z<m>;JTdsEY%Y-m~i{*
zI_-=H81=Xo|L~DIu#|H)x?6`1qT7NbBJzYlQkaQK&s$co(c^b4e@Y*knu6Ziczx(u
z;k5JeNdcIZ$i{fpn;q7(scgA4&j-D>TKr5qB?6{G_Nt50GJ(6M??=f!l)?GnP^u%R
zMIdw6++5BmJE-)%86=M`KE<=hi$fH?lFmj@9VK59gC1VYg3_tVP}%#owRo8z{OpqX
z-o4%$xV$lG`Tj~ABuTnfenUHLM^;AM%xP`VDsJeep}+}Gk6lWxqR@oLier}h>GYu0
z*wA~L)5@TC&&*r3mvYcHPxo<QF)Gfg{z=S>r-!2g2NxH8RpE)s_f*MBhF~o7ppxQE
zWhne8Dm!A;9Cq@E2WQQHA!%DDx9zD?0il<+UEHQR-qc$r@k~OrtZCh-{=iobEAZ_b
z<;1YNIE*lNqIB?41V&kFoV<M&F!yfNir9<{Ncye*w(5o~SYMGI*5xyU5A4+sa!r`R
zz!>JNtQ%TD<*kpK#2g8FZCA><>B9(O#ngM3ELC85Fxl+38DrSu@sq>POb-lB6+GTC
zFiOflUS^-rDgl=+G9NwsRTSn&TF`BhoIr#+#eu>=R_N7mq^SFz6;!{Ry)U9n38<I9
z94Yf(Xfh5E3LoF94d_G;y)-_h4780OQSd(z1`oYoIb2s$g+_uDvF91(VXFXF%f_%C
zBr7(`Jo)nzDQM?jl8~M<bUe%Us)orJ(7oW1*~Mf4Z7v7hCjSY*@eH=2fgCO1RxAJm
zZixZmwsE)BJ|!^z>qaokiYnkqO$<Dm&I^v|Pu`z8t_!9zU!9>h(uN&8?6cjK%wR|4
z+m=tCxIw+CU4~7kI&3Oc%_KiI)3n(CerfTT3fwMszsKP*FF2F3Gm5&J1Vx(9ElwPD
z2SDL2S;2%O@DIpaEdRp+a{3L^cHGebayhF9_`3x_{ma}tANp(ojUroVu!{wx@{jwJ
z@=^=9@;sqQ3zh|aY|RC~{m_1O@2q4fnKDS6JMxDg6$#r9xqTf^6#(8lJyj@Y1>sol
z#Q7N-A1Ho*a*=gH5^h76EwphCFs$MBW707mU~zb$T<qjXQ`&;^D?XA0Fde==ADAW$
z8*WsF^Y*a9eNHRFOT9~^9emO*b@e(RHtXCCCk;iQ=U(L$r>h5t`;J%s@sxy=qhI{9
zCJdp{O@0&R^TJTNW9$llrzjL%o{P|+RX``zjXI$zXFzuEQ?2;JucVQ!v9n4%RG_jt
z(0Qg(1L#iD83-pC!{ZaS{`<7dVU(NB@cim;5^uQY+mRe+Sa4VTN7beYT-fXEOFh5Z
zlo9?k<j6KV5ZUPq)^iNu)_XhK*siI-@*}D0JVjPeG-g7D&5{x*SCfCJTXP4FX3tu?
z-q1kP!?YCl^i|>Kliv@^+^_?hI@fDAQ_bK+zzMgkKUSc5Dsuw`_cVEbpCmVA*$VR5
zxIY{3&;%=*vyUaSje*P=>TMbHny@?E`q?q`<B+Byr3rm^U{mW_1m!MHcs$H%F4oQp
z9w(O#xGZE0HDcrLNL5TXeUO?mBzM+=YAPS~j+Lu|?Rk3u#Ud*>{Mh=-43#a2EbXae
z0A|49q{54u&+71J!hCX~Hwmmc%xgJ_j5L)8nz}YDe<2O0`pEcO(ZWEXKh96;RAG3@
zRsD!mW|*>agIeUUBK*1;w}YEj29&5NHT~A60E#MV&7FeM;AHnPe(F%v*~9y8?XZOi
z<Y(^gn&~Hp_3vtav0oJj+&&?0`FR%L+B3F8pIMek!q2SIU%c0XdsVnuz!ef~D^sU%
zj1_^?`Tm-9b;h9HKrn7)-3c)65ib1LYz6C2Bp(%F(}HK8ef$Wwd~H(c2)TCp776OA
z?&P>*tPFX{SH>m09l^rB4NGYwLHIIggoc4_opj8j^oWG92;AHCDJ8&P1uRoN$gjDm
z26FFGuhdT(LYcPpAHARL;m!**W-qU^!t(C!;>T$k@LFv5lWtCVsHr(0$)PjRR5N+H
zL`mHM(3F(s4Kx_TiSKS_PCU|pnR{Z=$@s)zV5I0NatlMyJZiLqqgxRKn7_G@-YW>$
zt9aUO(h0&Jkwucpix-<r#($)h^=iXS{z_F1=mdVc+>OW#RsnA{(*(zD6+kNU$vvEd
zGSKkRykF}L9nd5S$kHFEfDfk9p08e+!GO~NVhy%#uzgf5hD=TzCUlLK3?=D8CQ3)A
z!U;Ecc=eQByy!BC?iBBHvIa>w@`p_6m9{usdq#eFeq9SToR|n4>#+w?T9M`A`>bI4
zcw@ib@s%c~%vEpuhgwkL{zsd&UMXl2#2&pa!Ve2VGquiavw>gU=vjucYzNNV_hY)=
z=|Hw+VVTEsmSDG4Lh?3MRS;d!7<?yD5)Lx-UpKtT4_dXm3%1WX!`?SSvqLk!P<d~y
zn!JfPR4QfPe`>E6wB};nS*2qMzroJ~LAO3O`9J>s`AmTv)J*rQsHNnAN0vw|_TyVX
zt!4D_FDpV||27lthrk4NGI?0`+-xQFea*VH>zF1G<c^C=&C~*`9|~83e@em|2jJ~b
z#wyTr?H7;5ZYp?rqoBXH!UARr`DeT9qM|}K?Q5k*bX>N$uv;pZ5r#bqm^>P*14rj*
zK1WOeNM_XdUUt2+>6EV8MrfY|tZ5$$A5J5|vt1lD?c?M?__yg-m0T*odGmd#XR!rf
z@$3xx>?aG!8XZO_)apn|re=#pzw9B^f^Jcwga+vGI|QBg>H_um0(65Jx}ag7xiZgV
z4ya_?n+BA0z*ADY#&L6bAmEU_OVd{r#uU{&>u_^|FM3?pid0RYYR%OrVsSQbd)B*q
zlJ5HOtb#?-v0aj&oP%%6ixN3d<&re^o6Hc%^2DZGSN+y>TG8y#CmlnmU#2Lps&524
zdv=!^4L)z8=z7HbBv1#qan%_qzmtQx&xXe5+D+l1*zGpWs55r!RhcN^eJaq+d(ZN-
z7c%gDb@4TqK~-?9x6@*4j5wGOgh4=B1VnHf_O0*MhAr|hyVJNGK<oXYm|J;U;IZgw
zXYpztXnN?qYvCSA_(!%m%%XTJG)_EiE3PF0$#3to1_~Ci=R~1KWs40oyZ7~JVD%>H
zti|?g&n*n$85fJdVPFBwLmFs4g^{C4LVv-^3@PZ^$9r$3O%sd>(P>6yk-(xiM=#~H
zCp@EHmC@a;2Hs0ge_w5Iff-U>^e;;_VbwF?;X{dB=wh`$K3(i5X(>`@=fW0!;K<Z>
z>Ya@>l&*!<OSO6+H)NQUonixJ9Q4#5tsB7#dcMPb{TlET?Z_a7mj>7j8+m!=su}oE
z<Ybhc-~?J*c*pkfd~2e-K3(`M#R7!;DAVttae-8QdOa}*zL9LLuQ4!uQv<R`67THb
zRD?W&6k%k)Ea3N+rK6%nHXzmUm1~6zFW{Frc~Nw~Cg@?*CmEd91AL;2joFU?WYHuS
zq*7;xk?DNSDVLq0cFn9u>kcU(k;EhtpQH!Y@4PvGVpS0g*X(T-R&YR_<>~HLY+ssg
zrD<*7Hn~J{R^g4Cf|B5#6`k%Y@esK6cSc2znF=(qy|RzTO&wZ=`5t`8X$=l4T&Y~%
z*+Fu5*criqI)h9)p58q=WdNM!nP+av$$^BfGgq?X`<pVa-Po@PWg&y4wf2i&k}&&D
zhk?YF<)(uLkFP{PA(&Lg^Louz83yn>b5xV0fxPu)H9mDq_`EWGdfQhsNIqjsKgVhT
z!<}7KGLj77n}8?X`<^R;gca6XeQxxSuQy%q@lH|5km;VoYc2+grgo-fkodv#F5`1e
zUW(wrmW&JTp8(91YP})h@SL<p|5WN`p)SaMny}7oU<6FP!i^rH&X$9G<a-n@o5GJT
zM5Nn3nnAaz$pcE{hCuK_zfF3P6{r}Z99qdWhWsIRC#C0A!PnM_U(<p5aM1IQRTsN6
zD7x3b?)h0A90Yt*v71(?cq^uUpveSIw>D%3>q$Z8J?|+a3RU29Ujh2*rY|IY&i89&
zd0T*O&GYHp6b<NAp{(-K$`smI^8av{P=yUru4Yo#Ka);9Fg8?dLEodkg){qYltDjF
zrhw~n1^8vqPpHFz4r~)^f7C-K4r(k#=*i-QfoitpN^c^7!X}oN-``S(SHD?w@A6Xw
zqVd}@s<X9V%v?v*(+B#H&iQnwVw*867UMLIJ|P0Xi5E=&pjHL4i;Ybh^%CG~j^Gq)
zr2$+xa^|P46B(4W-vj4rEnu}bTj$_G2^fCa?1N1mfX_RcudT;u!1GSW1m{`Cnp~M5
z`R6EFKr(6{@$SRg@c4BLkMf-&kY~?G$DPjtKxR<vn|LG%N`yi6$DVrdOYnij5B{o<
zsnty1{<Z=r7t!-%KWPcyl;19<k6vtwsE-PsxT_D2ACR}+oDqj3PP4J;C7RH1#6nO-
zoCF8>(ih7P%R;HbtE;Q6X0Y&Ku(rtyZg~G{Nr&$f4d`J~@tjAU3p!q4ABcEw0`Bft
zGj{J&gHwGAxo6AfNv7vGp50I}1S_HXsbwxk;D?y8>67ouaBqJC=bLxNFgY<pd&hSz
zK=!ljrjbfgQ=aTOt#NHNz!bOGVNa(42Tyix-DS-QT{Ge>;;%D9yYrV~_cq$Yl|My`
z3)Z&KVc^~gW(6}C^tR~AO@3<V_3?1QGZAeV$ksM@<j0?;<mvB{tJabrYxG?NbEgEX
zT+C=Tg4<x3*=h6N{Q5xfZ0ziX6cXwjxo7t#vw}odFY%1rh7R=U_%q$7*M|#Fwr!_L
zRfKuQqse0a+;E8X>;w8&j8JCd{TJO-c2Mn{b}d>|ADrf@aMyAHAgDZvQRk8*FuX3D
zv-^$?q%+aevlUf=(NcRH6GAk=z7g_r=T0{G%D$#Oh1vp8y!d{h?~gE$Sa7^(Hm3#d
zhEF$dvKj(MmVp@0H8;o^`sN+eD^1u&|H(fq*a`&BWKvyhQ-O5HGOt;<kpZ8b2bG0q
z(DkuT%y*gTl_txczO!VCYOu2^qrQco2d2Dg<0_qS2Gdefj2%U4;M=Lm$uMDh^nar{
zB;v;dXdkf`^Eqn3AxQpX@v#K7t>FCauvHc8X?UeheI6Bmbd9HeH=06=T;aEeJ9J?X
zZ|UrCFFW8o%5o`HM+c;PAAOOqg7$;mDo>P0RpGvob04HM72%IHhXL&-Lnz?=;Fn~m
z32?LxwV2-XndC9YFJkG9260?Z=<-}s2P~gj44aGizzeD_zXT&T5c=%*bmd7y$m?|W
zT{}82=a!~MIw`9|$;Os%i-Mxi{6uaYPq`6TjJ{IMY{m&M557;neM}nOl;}|{q}mFe
zx%lW;zq5oNYgS%~P78vJ$Ng<y%|DuWnPpvK<R~ClolsAn00~}a&<Rqc6@Z}^HAde&
zn<KHf(pFZ{iNj{q?dXi$PZF{4(e&T`j&!}c`TfUWbi8QzurK?p0oZZTvEkA%E993-
zv)^<U1a9@*ZQeUoL1Ho6%TgLE$otGtLM}xW%Cc+g@dth-F^QU(zUb*8B~!rH#@bw<
zOoAm}eTfXt7|6lddKKU!Ir@%C$qaIqjd}k-*NHUA_rhNKa=?@yf;VEcev&Ga=}aHl
zN`j&D4_K>}<$+RSYMC|%8wh9GzEen+8=O?Ru#HAb5gt4zPCcf-(lq^CPjXkc7uc*R
z4ryD}fa~gU);G1RV3p}rejZfpQ(}B;Zad`;nMRsA?}doMl3^WRlMqc%G*2sEm!b@=
zy}a;9FwhtdbD#AJE75{(%Vnv1H7!9+QsKp$Av)koUWMaB5ffPEYB`mK2Bj>_=F#1o
zk%CKA<*}DDbbw;~mD4%8=J4IN&$B<C+rT=}TYT(H^sqc(eQ41}4(4a4o!tIe4M<l>
zmYbqMNnJ;h{rYNb!S&~HpAHD9KwYxmIWPKU!378XMA1-t*qghW!~RDV80+3HnPk%f
z3_n-4grcJDw!}9rYnAByASyd^G*uLO2F<A)<xl`8-*Wg;Uvh`{UM`fmd6_^<tH{#U
zDLuH`3v{{Z^FfOJztbY*)FE{-k8#$XB~n(mx=0neehDr=r(JbSAM}42eh^W~i#jpV
z;%>cj0tt*iA}qG+K_BV!-TU~Ln_hj-<1l??4|eG9^YK;J1*=~&UsdX{1N)UAw>@b(
zP^js?qo0mG)b>6-C&g_7OFu8H#M9}(M1}RT$wWmM7~q%wN5&c?+G<3%p!%CL{d?tD
zV^j|u@b5S^Vh*0Ele=(78iV9(FnI?m)~dhv<mq%21oNeV3RS4c|L${Qz>QEj$otJ^
z)uKidvRP0YbGAqU?bH)<9e3>@>4(zdt~a9K%2x%^n}t23Z{l>t#WbvNB=bvyv5^M2
z!A#k7{{B|bP~&8`8(kOa|4Mwa{9Ox9c*~1O-2yQAdvShSyCm58ZSxmZrz9-Aoj;@0
zOA9B)?yZp-@B)ov`Qo&_a!|4+$Ehk*5w@Nmdex$70ad!hq-bl^;H~NN2NPu!VE=6X
zUE1S1@JsXQu#`A@xK-q__=i9Vkd){ldg3=PQ1$w$o1QNPNyFOfwZIh!$~7r{Ncv9F
zOmyDGOfC*Y(nFrN@ESsjpYj?)^a8M%VP<>2s1E!vXn!U;MFLFh?!HsQ<PIsS&nacy
zaD!weCJ%&+n89++H4fvqs<3QY`i~%mB($DM&@Sbc2eU%;Z*4@xz)kJL<Bp9aKoKUr
zIX)-?9tGXID`B=qy4u}xV^vuY&NFpBt@AK|&Epr^J&yb)DbY`Uymoq_X}Y9!yZDkM
z;MBf(`eLy?yzwME_|JoGl6%zGOM9-$gA2*@3O>1-@Zj=`_LGMk;lMR<iHZtya5M2_
zu<$oa5TCRylk&G37%s|V@XS*MA5BTSots606YmUtN3{pAKSePX_R|?k3>lsNp6L&6
z-F<yy($Nd-@5$gd=x_pD6;+$?yFJ)C$g)Jm=m>PT-w>%zlmZVa7)-*bo#B`8@oPQ}
zA@Jc(u3NwQRp3-$%=yL{FNjA}x8UOTlYbSr@o@2{|6SZRY!eQBad;mbxcIfhIhPOI
zJ?#C&r_vv6B)uD{ACHDo+zjfBC&FNSyUgoJ`CTBsR`m-bQv{g%<bQm;b{Cp>yEH)b
zhad3O?F^RXu>qGKRqgvF;twpYb)43m35DbG<d56W1cNm81;JJ{a=`s{KYwoKUT~|X
zg^B7~2#C}#xfi%M2#Bv-;{Ph(4|a`+>+4F!!j1wFqn@d7sN>jiWcGF-4AIwA>#5lF
zpT@P~#y$I2<3vtHwf%R~A_P}^y2Ul%pGTn=_4}>i4hjwXm!Cx-cpr1hHWCfWIcV;^
zB&Gv%L>xjQ?uf%niq&d*;Vy9X$0N5)5o6%P;~w1i4b>r*?hftVVFYxdGS=r;Okjeq
z>}#g7cO>CY-Q<CNo<Q;F++=*3DijvE{3YH|1s<z1d(DoHyB}`zR10J&Kpue|<l!0A
zFu1K@#7WT@1Qe9oZ>||Zu?yd`$9%NG@3gOq-8Q=5*ABODDidoYNBN)aD|gV~GWq0h
z$`850#}V0hUtxZ*u<0oAiINQ#a7t2>Z8rve86rFes8dEWHuZF#lMEQY);k#EPX(MT
zEtIWvw?cteXD^Jpg#Blewc}0p{9l_44Htj$-<zy!BBsQT$`G7~mu#}MCP*<4Ih_+x
z34E!dSY&oU8L*wx5bsBaBBPtZNx`g+(06D4u`yL`AWFF_!{oa<+_k5^!1sp>e5$NQ
zs~sW?Ey|+h4z(D9oQq?%tFh|v4PVTrt`0eTx?QLAjgU0_8pg!kIv@p1q$ful7bM^V
z)0@e!)VaVq=m<`JYlyl$$PV{KYQi_AOF4R42C$_i`s}Z4PB_t-pKFRryLeZBiJR8(
zucncoit7CDrtMNFaogWz1B3CH?wA8nuh1)YMs?x0{WAslYxF2C>J5JWuQA<k(Te{y
zrkl+-Q(dTp1Ki6#EcZ~E9Oz!V$*)%NhjhiyI63LADllu!^7=l?1C-N@#BH}x0j&pF
zPtF-}1LIFM1!Z}rAlq|wm91O^Xm9hmBvAc@G_|u%J>=XH>Cvt;>OvPe0i8f9#dBj5
zFy^XshC^hD^l^{Lg#jlzP#eahnBL6*nk1LB9z^hhXD=UgwqIlij9-O4MH{F=rFjtL
zUMo?MC~GIu?4See{Z9s@byI*^eXHj;S13TI!Ajy?TLCb)`<DJzS8AYl0(`7I!v?Yh
zGL~a0wgBJYdmsOlu!6DTiJ08y8zg7$A+rJ-IdBxRXC~xRg0I~<oNn!#Bs24`(GF?y
zK)zw{)b-2!z;#a~d!OYp>5B)AX7M=+5Z_!sTvaRs8c96?Ze@%>(1Ko7v;Ge$<movt
ziBH0S$CSHj@RcRlt{UJ`q0R?pojylCcrZh{Z*o<l`xPZ9_7Lt4G2#VlZ)gn<x*CGG
z+<j&vO=~1suV|y>UPhp>k8<0<GB;>ovgZ7FRt!X(5lxie$_To6Ej|0~*uW{l<wB#M
zT)<U^!%#{>73}EjaTesH02K}0)*VWWAm{wiFH#JGVDZS!&AwhHFhg7Rf;o&16y3V6
zz)Qvj*vMJ*-=?qt*~)nsdkl30DQ!J6+9>+}boJFyQH9;t3^8<fH%NCg&ykW+lu}TU
z7EuWa10+RSKuQHs6j4+V5CIVhkp@9RDQS_A?h?NHy}y6n_5L$!X6{<E=G=Rq^PIEK
z-uuE`qnnZe+j#KYTqomsNCC-G!Zq?j@*rAcX`1JShbXa#sGn&o$fZ5@uADCkEb#nn
z=RRbFk5vKhoULepb}BB;qzDHk1)d)Dr#OIefBI5?mnQH$^3orc5`wbY!SEAmD`<g2
z#WXXC2y*0a6I+P<LwX~bui7fKKy>c>yd*I(1YIr@5h2Gz)^Zx@)*Kz2{1?)1*1`j?
ztK?G?lo`R{PN~(CYz~+s`#xFO&H#29Qk))A3P5v(*oLZq9nBv6UC+&>2T`_-<Ez`8
zU_^TV&8zctaO;H@O<)H#j0cT)ht<%)OUgiFmwrk(&GR`f18t-4f<8r3{Q_VUy-Yj*
zbpzS3U$J$$^cTI5&wp*$APUPm^}p8_7@#Y#WxT+N6sX8NQ#85uQ0(`K!cr~`pg9wy
zcVU$WrY=ikJTWrRRLgC%>}7__3t{_P5(c0nGnO^jFAOJHqNI6jC((sn_PU0~b>tI;
zJ5JQV3>MstR$GS#5N1Fv8!5SkNRQ@KK8PiQocrv*=~gM=j~mHLUmgx9ZR0ck5WoUB
zqL3FFy(Hj4(OXyADh8BmR5nN+0fpipKl3<J5Ez+%^1y}|1RuPjdH9+i9#*w&-KZsj
z#Xbvzi%bkq@*u$d`92*4wAe2`QfGj}C+U0+oY;L_nqt<9ZR<sQX3x0QNnreWE5+j?
zB4~{Nvclpg4Z1ysh41tjz&-HY+`!`<B$c?e`03v&VmEW?<+y}#V@qdjRQjmEexb;E
zo`edlUPh1y{^f$~-QBj>Z35ig!X;)_3P8V>6H_Vi5=tZ&)M?J#K#!*uW1?Irz?jt{
zVp_u(QeIx}(s$ZI$6|!X#c-rxP0=fq`kER%)Ui>}oB+6;8~8MwOa)Zh(vQ;=_tAMi
z^Os2%1mJ7iz{{7Q4Zq{k+V(_Az*Rb4|J)c6=&+|2-T5U9B8)d`n6(&Sd|`G-H%AZ}
z{T_+lccp>aldaMp<?+xlgXST87rlJo;m!F+4RmyRUsTKNqn3ZpsRAWr5c}$P@2LSL
zkbhk3=yQ!2bQuS$=u}tG%?~$<CGrkY9>2CULofqyR5X|vNaNrgZS9B1h6BVJ!mwd4
zzz(n1-5c}XsDuC2(?MTt`N2dqB-Ubk1$|I#Umo;4K<<oIiUJ&a=u({%-?tPE&}8b+
z3qAJ_;s1(%Zahj1FSF^iqOB>QhenOv_AEEts@C(IsHK4-QvEyGKQM01D>{8~GAdAz
zpi;~rmxknz^SV<jTj<Q)v%i<zXh6bez`f!!EByHxRqX3Q2}UNW-$xY~AZomce8L=C
zA5EG=54Fjm#qZt1OX79Jo}HlkB3KZ9SbVj8Q2GzK+JDg6iJw6iNL?tBn1n$3y!$j6
zff;mPq}4jr;-JY=X7bqd9x_jSN1z;5gp#s*h7Er>Al;5%Gk%N&JkNhB7!zd$Dd)Qb
z^@;jG_%NP%f>Q`YpB;>KVdLg6aYx#d$v08<@hB;8eI}SF{j7h2Umvcx$P8S?ZK7(`
zPW>}$<lq-i8#1a&4F-9SrLL!N0uLEe7Vk|ah#XofE)60B6`L8}Z~j8?eCr8M7p*3I
zwGkQJ{7nMnX*nXC0>rR6+8Yt6$OFTo#XavQNC66aV&aP#q2R)Hz_l9;5GukwO!$f2
z=hAH^PiptkXzq9oW1kHCIwEcq=|lqjUXPnOU5Mbwvi81ykrWtD_sLv1#slRCM8r`+
zn<#u{fZKW&gY82unB4M~0DI3UURxGw5WZG1I&=O26$+0i+=&tZ;9d%;<}(KWIr4(^
zB3?MzI;fwJxrFu?n%-!ok-#UK@8xpDoZ#Ai-^PVS2maWN>zSO}MP8|{)DND~L7kyu
z?fpYGuqmotrmPVL3x&jp1Wqb=d7^UTtRD^hkT;1`|H1*{^z+Xbgq6X}Ur|v{k{Gm3
z259zIQo`Mor`#UIys)8_+!Z~_0IsQ@Im2eDAR^#$v4<}k_Kfo2PUd0;P#y{%_EEvs
zv-0C7^|!?!ekZ`}vpXK*FNL4DsYC&_dk(|&mI^@5{Mw)(n;L!@ix6VhS5RrXP2&q}
z{Vu5f3+TVk3{SZNsn6xmf~#5Sn$BmezGzE-Zf3~=+v1tMuk;Bpn=3E1<}L&db2)Kh
zqsyq{4gOf@J7OsP(P&6jLIeV0SJJ~YwII0OY4Wrt4%p07OZI0e;FyAQUgZ}?IDGCj
z8%4wqrO&zF>2K2mO-wkqQwA%fThNz1t7ihubbp=BSp}dvVQJA~zk#f}?ukbS&_YT9
zdvXyo2TU8Ng&m1yfa_&KaXso(Aj;IL@MeYq#w_1u1dr3fj)VxGY5z8wyL#Gr-x8aV
zeH?B(5x0g$4Xw!(lva_y$bOabm^he{7d+Uzz=%DW&r5CnIthFwk7#?_vWwbi6|D>=
z)SyNpwI@4=2b?F_Th_0VLC5@Y?eoek;D2s62b%TanCwS$YbId`tIeM1E15(d|2p3_
z?X9C9wq!eeoy^cEm6oo+X8_bVE{=obO%zRf&fTts4CcHV^nwVKa6$B}5Zh@EC{8$)
zYRk<6LAThfawmvkg-Pi<v#A)|zPGIV17q@;6~E+)-Xnnz9>1jTx{|<a5hICr^!$)k
z#~e()O#&18)Z{c#43NZ8_027m9w@%FDP~U6!`s`fagU?-kztAFGj%FO=w5Oh@fsq5
zkDlOknvn=hoS$XJQp-R?b6xbCH99z`I4gZaeHSHZ{3)%ETtyrcVIni+7+i*H(BBoJ
z1^pwfpmLWAM80zs%$(xH@MzO@IU5A7V&vkQ`+{)3^2HIpfCaQw(PrLxu!j61YqlH{
z@sNUQ%m*%;KzYZ0kE-w%68BQ;{DmilpOVyCIj&Ujjo;a^%3c6``{t+&aWsH`zMP)&
zavMSW?Et%6K@eCfI)6MI!G~<R`8aY?aE@r_%l>eP%9#|27$ii%1A;UDoMi;swwnrT
zp+Yd|l}s&JK?7%^Q;7(V@IYlaL$Xl4jj}({v$p7KLa#>@Lrc*va(Ama-q?$WD)wAn
z|C1_Ud-T56lQt5V^*egY%XJym2OmDSts;W<up|wIEJje?688;%iv!d5HLb(=Lu4kk
z<L<!94!t3IRV;rsAmEnWXMqNOxJ%{vuZ3<EiEKKxF%<tp41BBHr(5?>ukl5eUv8Sv
zb!)*{$o>G$eY@;_w1pV^-4CA%@zh|`J=#@n&J7jLuZOO=(!yr(uk8aecE~lDeEdC)
z3J!EV4ZgHWgBzc*r$Y25k|Bk}fLpZCyUqB*T7nI3kq#6#N>jqFBFB%Vd3vxs_T=DF
z1~sHt%5qLHQb3>5!n693b;PJjF)gMi2=ayX(emN{P+(1T*VBMG)DZcar)f_RMs@G}
zB>y4+N7&nDG_019p=v3$TRSB@ZWE(rpB4e3w?ab&wm1m7bEaY$%OBbwnVM@p3BiSy
zO1)t>GvGXzt}<%Q0`|?#U;h=&qhdw_OKV0d2tS*y)_RHuc+Tv0S1MpyI@Sx3Qtd=g
zXLIf3A-1kZ6dVU|#DXwMXJIK`Bm*kG_!`0h84UXu&dB13z%b-|03n_Vm>32vS7XG1
z`0&b6C9G~>i>VNC)T4lo{qgV&X)0*A%UyD4$P5dD9Zb((l0k{nfS~V3Zs4_=z%-`Z
zU|vP<_s)bFA|`tT)8Zw-DZi%oT?`(?h63X})R~|uJ5^(9TNX}wG$}^B<pZN)GP5b4
zWi)p>UybJ}H8hJ_F1kMw1y|Sg4GmX391m2?T<#-+2q8Jea!fN)e4SMFg3Afet|RJO
z$KWSs@^+e*!yR;FrrnRbkqi!b@3lH=vI1v;IvWm~e3Jjjl?vU(fs>x1Il-L;&h~IU
z`E-^SzEodGy=E%|JXfUT+V4=qrH7~eE`<@ngxuA_6mM>5#yh+CIch?NVukxcEfHKN
z87m0i#KR}!bGefmOwfEs^Yi@#9-y*2o$@DB5F$&w<6=IsLkxcz`LYTJm_*p^{g|eO
zHLB(7l<LahAYgW9bDIHzyF=vY?dSkwIBd3K`vujVqxZ`DFpba0f?E&b_Rx^L^XSG;
zGMEl%jQg1*1X791*L^L?;Wz!i-&{BnIO-4{e(jV1q%k}Cw&@wcS)+l;<4J;GTa&Eo
zB#Z+qGO;UCWJIvOF!o(Fniev->J;0tF;4oR`bMA1KLon=wNGO)&L!?**NiO>)MYvw
zs>F%Ijqwxb9kKdguKj`C51lP^<oYQ|*~i?_oi^R^oJJY!aG|**F+}i8c`LCr5ZjOH
zLOfHF7_t0?d-O4c2^LSj$SDqEfyjsUj5&N9kjNmhU9!jlWo<sIcJstg_uJX`x0NKE
z(mk3IkHbS^{M$LVDl(8<|3Kn;QXb5g1<uvqrh_|WxOt7bWyI7I!xx~pf_Cp6wAN^0
z8mTNdA=z#c`26s=2N}~IDn1jN`(~68yqyKP?C)tp=?7|K?GjEPc}+QaGG!7eGZ|c-
zj9El)$V1zQ3h~fT?Y($OTnR+%!-C^6TB8)zN%b#({-Krr??W5qIPfg#H20Ka279;q
zd4mZe$lWmy;XO|YH9|LyL+^3J!IYM#gOWVNMT%PPWBE7chV9~j+dqUlUWhGY^<;?N
z@oS^iWI)Qe+Cp4G3cQpeR+TCw@N@n2Si{Rb<WO-t>yp|kI(xzJ?Gdc*5_n8X+bX(>
z;{NzG+F^Ym-Q|I7;1Pl!v=<3Fm!b*Wfixk1zu^h1j)Wco*++!S7wvLHisT8JJ<YGs
zb8dp;z+0A}_2UFiBOLvVjw>PV){Tp6D`AAv9dpwgrCZuxc0!ZuG;|3#{juA3XH^N_
zZ&`UybR-j&0{!F%3!V}@1+>37pOGX`<&(c8K1W0F@T8qhYa=E&|B>U1_>)GkQH{+~
z)z2gpgyaN|=IRn6r*7S(bMzo6ozu*nt@qX*>q@WgR8%IA-u^9=?Bz;mQGb~ceKL{|
zW*%pBY-B>4kHeEACis!I{qogc9gR8!9^EOPQqpLG`?L2a|2a4i?yeNJ&<2nYrjA*f
zmfzqZaEe@#(TofyMAyV_)3R|A60(6qeViNI!xY%qllKwTn|D*8SU*Gbs%-Bkmlz2B
z4dn4jqlVjehBsp?4$$tc6^YiPLcsH@eEY~L6L1I>!<ADAL+MC{xxUOA@_JSva$KJX
zR!mQ&uLLuJhAIg22kFAiGZ7ZTNB7aI1GnH44D_JO+CTH)3k%rwTi#6X5CTTgk35Q^
zlu%KZLge8^3y|k=gT9pmxY*o}eYR2ovHTgf&@b3NmY^9Dw@(fQW#1<`CwU>$#z)wW
zmmZ=yJPAJwX<_!g>B1#8R`7o7NBEY;1hG%)9v(a*hmpV^iZ)n2-k48&Kx#z_CeK12
zu5*yU6I`)j@=;k(t}}h$^Oq62u9^S+#!3ycZz|JoUfV~Xiklp&$c5mw5B(Fibz)cy
z|K;gSK@3k~Iw?DIgrP`qK<$~LA>0W2$@_@&2pmmXb8MNMMf@C=g+02Ph{9Q?uA?6h
zvI&K`?^5-!{#)#!L((=9lQrWm|I7rR={ekq_?X~6|JTqQa#6r*sdrp$C4sSht~%%c
zAII17ep*p#xbex~l>C7V+&%dH*mai#cn98m{E<ZmR{jox<mUt-Y-=33y3xby@~Om+
zgX|FS$EqQIfC-My&HGeHu)+=hlPNUM@Zi!?mCBGL54?6J)xRWJ!AoXG<!1Rd%GZ`3
z2(*`m|6qaL{{iuO{tLwW52w5Le?dIkiokYQTtqPxf0}MeJVIR+7OkUMTWI3~*E=Q*
zRI#8qCi!Tb2e|h>N#1Wzfrfa+YWrwz82@qemoMcJ(2X$K6Wzf9F2~LPb!ro;BvY#(
zykP^PfDrQQe*`e0uJ76(Y7IaN7m}#GFrmk4B4d$=JWyzFxyxWhOWE4!4bdTX=pj}K
z+uq_qd>5FnGmnY`d2cd}&7vXDehJ!o82AnyUw9aks-g+Scs_c@kDRdk;pp$r%g2zP
zr}Q6B#ouUE;CRz={szii7%4O(5JA#p>$zenR`@U^J|WXGi2_9hyR8G5L6%0V&9!b1
zDPOcWrAXC>^1qtLk_FBn<v0CLB=84FrP#i<&q4s`TZsg|xeXz_|8L-XP7CdhPnXT>
zR?%hWtoG%zT`1SdWGHSOEAG=?s`zcvKvUz9b!kfsgXU4J=1!<Vi^Y^NJ!{;M&2W%C
z{B{KSWY1kr#@#^s<i1k)x+x?#5Es)|wv9xx(p4B6NFmZFDR|vW5I*E84P6OXMTQBx
zTf>4_laf!QXnk}UT|&K?{3~zKw||3FskJ*O{go#DnGtf3Wx6HVb|47bJaeq7KRMvm
z2dzlI#~iTcQ1X%iD^@7C-_Qsr5@DOVv1w>@Hp&(rl)Jvp2&W|i`?tTyg9V51B!1!{
z8Vk5b&AcTHSCU5}rJ8s^N>Wr*rdbw|Ck$Q9-Wx<G51oH4SZ<+#82iNp+XHmR=HxHl
z4lXEltP4nyEJ8{W)^@S4$sk~_EpDlH4868*xwfM}itP2&B`28&(H-4NayP7*9q=kH
z)uZHtXs6qrR52B3KW~JHJcA8NZGTTID(<49cd{<#{|eB>ndZ$)Hp{4cl})0AiX76u
zy+1v3APP`${#M;|BU-H~Avvzf584at-8foFP<^ueda0QKoo-`#qLRLhP9$!9A~Pce
zrk)!QT@)!G$g^7GLg5hzkGS`?!Kwqj+B8dY+&cnEzGVSdo^GOTD&5kzt$)ztWRl01
zlh;u9x~3G<Jt}y<_}KfNgADL<st)MXccXnS_h{nd!qBs-)b@~t6hbK?<APBq(rp)a
zvR7gP`V7C;XI$J+E*CytAEE*QRbQ`uuKj_|I)vwS7R!RbQLV|N?Lr_aUp?O#riqRx
z`LhpK<DfHZmt8ex4J9|OKMpw{1{tQC^*mHO@HMJq<Q1Aj7a6)5La<HmFP`L0>I^+>
zx6+i2YA&GOiAqDa=2>LUT6~@(jRMSxKMF<ViNM3mb}i0~A1Ge#s`IA<tZ53*95pK4
zK>E0QVS9TMXni6uTH(?pGQ81#y~KtQ&WC=EDgG@FG{)prAun4I?pV2Pa5%=LE4ygm
zGQ|wbRd@8=FMA+Yx`RZ+R~QJ^TeD@0U9X4*`LB7|lz`u3vv9x(!0%|8z`Np`sDmN4
zRObUHWXK&v7yVgBM@qRe-iVZ=vyXlFKixb)3Xfl@EYi{g8V;0E)DeRtb9vU!l9(a5
zC!lo8hZ!tIitR{V{X>ef_wa5C2gt}_wj(4s6}@6Hv$Xih2<&znY?X5=u&qoaHO|+B
z&c4lCl@b;Q7MC!wgAGC8-z$p!_UJM)dip{twHyz>va<dbV4ynPh*{t|wyu&ACS^vv
z#lbl9_3V1qD$-s_n`|Oyfu_Kc_6P>7Io{?MEjc@gR4Q)_apRW}$&21&<D4vTi*?Lx
zTwMZAB{1=~%CUg6vrOcvP9aG0KHDV~M-FzNYSZ7mgL+RDmrVP1p}_(>CD9cwOi=V(
z%>d)dknj_I)?@5M_oN1#rlq9eX^>XIXr?Y$oQ&WpyN=MkjN}z2US{a)>o`#@$OAs<
zvbKiTgn<ZC+mlNZ;3nI>%xTFf)G}P<BC?2}A<(vE=C>eth9>Y;t&zf7lvG|SH6_%!
zKH})#76H8g!t1Z4`ruQiA|Xh&gEBt<F(KeFkgJZ8((pJpNdFaPFvsqD)wmlBS08ae
zn7%xHVT~emjR+q#v^WX=4UwTiqJPl{uX?{qpB@ye&Jp*E?4YFxF*V|7btJL$khl;7
zu^t9k^Nn5~M?^!_9q+TI(X{~Y;oDo=C=hEpUIg_bB|D`_C?<y-F~L0Qt8M7`3uyK@
ze1sZ^VpB6eP9rgw%a_iSEh5PniJq#zR8XGiCpf^qjAZwEbZB3#pf3LJ*|F!oqn??z
z`Do!mB$qTn5<QiTc%r>{#8=jl4qiXvd=xh9urYYuu=*Z7^(`cd6QKdk(KaV41`@cv
z+c$Aj`#Qp{%bz+MiHB`Yf2kU~eT2?6+!Vw>dVyzG#E0@2;hRcRVoM4OWG4AVpU@P7
zM&3(C*F|TM*OQStm4y+M+o8=lCPodkmsl4IUC6=HDZSIZR|I79FCGr6vBCr6vHXvi
zaS`3tQ`M&6T|}eNKXXZh2x7LEl<5+BQDLed9p7md$RWAORFf_bb<*=haaon<eY(Lr
zvtkjz=hl>#Vomj<KK&>i1_$J@5)>(iiHz3xJ6^DSSU@>}#>GyHYv>fGe1V%D9&TDE
z7%4gLA$^m;Z2T?*oQ%>dD1s5hM@elhp<95;J8g<W-tVI))DI2WEjH2Su<2;oG7IF|
zcPRXtA%U#=I}a3FC?Ks>A-%_R9(`>YGG7&6MA^}{`R8!`NI*v8fvGDgy!6X1xfLmZ
zfx&(tuwH=NpIB)q*0Dm$sOPNhwjfMI$<Vz>zktFzXl6c4Z6n=NxY#k~e<%yzVDP$x
z5(s-`o}FAGaJb-Em^6rk?!m9t)|-M*NT`$8x%d~UR{i1&;p;}zRMt8#ny7*D^v=Xy
zF+Hdn3aPTC904=ZjZACe9Tcokt}8#n3+HqNOP(6xuyOjSStf0aYo|WAu93Ke^4|oR
z-C1FU-5XlMBMhq0!sikEqNN>Ok6}^wx{hf8p419`uaJQKqq4&pFT_!MXQE+G9x;TP
zQVWkO<KfYZEBWp~3j>zT(>oFpkXgZgiwJe0sr)PVO_~LvK=vQ~#Xo=1mE2^$ZigPk
zR3BkzrST6vb|SCb>7xg=tr;($W;r0(#tImwY#{pk31Q!8L;#ogEYU)V3|Jn}SsfS;
zpah!U-<8*i!C(KeKPw3*D40;#tC6U~=z2!{aDFdZ{%b?D9Uu=6_G&j(6ZVnC<tHr@
z+OH93HUoVFW{7g4dnjB8)4IKoER!Z)m&1k)*18n=dO$O}F7)-P5?rZQpvrN?jEyE9
zzn>~$1jm`D$q&j}(A=x2k3NZVAgFyOBN8)?l1!jVOffKn_5rv)O|Az-%v|4F^K}6K
z@||VtfEbXQ>`>pcl>~}SW2VNr`zTg0MN4~C8%BOkocG_g1yv3gAMKCdQQr|&+70nj
zP%BaLe(5<i*k%>VxUi)nHGSF1v|4&7<u`tKIKvEN8bu8<UHp*wTl1bvs4V<=No!(U
zD-QLOe*81+nixnooAN2181CPEDcaRukJhEHczY;_!eYzLK%j*X&?^(yJ!d@%4Uv-{
zIgcyA6-9=#sw#?b(~yE^{}h&=$un~eJvo8$DfRUyN1Ku7H8@t8EeBmm=9?+=#*nUm
zDRZ!`3(eFzP@mS)fhnAU%5!TBWVWbVbN+S*(L8A$<y@eJyVn=_-7izZlS>_&l>%&V
zy4h^-H;oY3aeS`M!;F$n=uIU0e^&)c!TTA8raLHQkbcAZVjSWp73sVD9^=-Gwz`xz
za>BfB$X%UdY7p-mTRrgs)AnfvH?<xWgd1N}=^pTqz@tGU1yVaEP(itYxUmMLH*q0S
z46Acy9cLOoYU#qrrtKSLy<X(vIH}`Otqx@7yNl~eTG02Zro{1r4%&Zy*tpD31;3o1
zyGR<a0{99`d`aX6wy??@=MpvH()sIl!GT{<qmKP(2M=a^(6QlVkAYY7Od&gCsw9AT
z`lhy0f1{^v(F}ag1OO*6;(gj!3uN*~KIV(9qdwlc{yraNs4sKAo!`I(sqq&^=}%0e
zjtCrAG#?JW{*nF4;VlG9yNkE$4UC~Xo-Q!0dKw*2u6%3o)qp;V%&hVA)ZqHcLHwJi
zDeAvn$)atvf;j6H9$B}Kp);#@EFao#pqiGR<6c8lVELGo^3P&5%6DXJ5`<k8TKi9D
zG-Dr4?y)8(EoC6braXdoS{2IDT;2$e-A4LezE=BKzCI?K)X7sZhHl$L_?dkeMZE6c
zlqJh1(N9U*pwOG+NU39yX)7iIIb?<8t*j73ghwUQaU=jcWE;Ct(XY@HH|*yhV}i+F
z)}d9$bU>Q)d4-sn9iqBUXq#_jfd_GhGS{DQz*hC=>Q;4L_&T80B3!QnNy#x5#2rj<
z#)syEv63!WA52~m8fFCBHcxIoA4*V=nq?drV*^vgqctC`MInJc1z)bF2WPG-GTd71
zLff|r7|TgCq5E`faZoc4_#dRQP)n|$`5PKxp|{zAY0s>f@v9WHH#gs(5IY40I#-jV
ztryTp%Hg}ZbzR84?zgv3p$HwMbZ>|SMG>QOS<R`jT_jvBOH{{93qM(IN7{QZ0ZqVZ
znu(VRP$loy)OGv|I^Ro(&b=!F&u>(<-L_prB~L?^2HmiFa)+$qkbevP3$CaRzRL{s
z0!fkHH<cjEeDEpnjXp#nH{;xLPX>OJ==#65qX!KN?>ukwI@HFy{Ddos7}Nqygp%9&
z!E9Nq)BP;KaFJo$qxoSJJHQw4T0<Eg%L;t*|DgsvC(O;3ObyV5q*~GIhU8EkI#q-Z
z$NDoPy@JFgPWY#nB@u8*6+V8W$dM!)Lp|!v8Ms~vxM~ybCqS}+)<4LlyIag7sT1LX
zs(B2+H>4-)evtzh+y=M5T+xK5XRmlkSpPvxb)M}n-YY}*-Ee~Z6;}B9>v#A_bPxKR
z>E?Gon;D4Qo(Otv2*cYS?^BQ1=z;gb<*h%&<5+zWFOJjEf)^PB<Z*5+ARd2gbg%v<
zA_{1IWG}LXRtUCJldGHP{ZIK?0RaZci?Zo#s*na!aY|k)hk2B|${l2~w~hY(5;dA3
z+Ctyo+!>)h)`0>JYU{HlvFk@^OHo720t%Hg^xtEI;C|eXaobI7xIf!?RMegke$Ivp
zEXYs;^Zp4DksORf!QA!1mRu9<5+UGzLk+tJBt0&RTA=Q{NOzCy3#woHGON<h3szQ&
z(`K5-VEc7YHf4YYYI<n%vy>Av-WsV{ym3qr%pKyV_cg>Ilbd#qmQNQpJ03gD1-GMy
zLRR-Q%s?ecr?%w$f)FT-k>*IZ(t=pU%r&`Rb7*Khj@T;x2-pQlKRRz~2$vix=vm%$
zqoCt<qmteTR^{>QAyL90QIV<7mp6d)C1h-(Ebzb$qE?ni<bk|?u2=q!1qcT9jlQy4
zMJ(rR3FiY1KqAQ~W22W5Dm7VY8aeKxgm<r9#a1zn<mj_DHOy#eK6Wok;T{i|8mk)p
zjgf#)dHoF6FvG^<++qpd<7)8yS<N6%3=<4FTSTlAI?!y^4@3139w=&SU>n3V3x}r|
z^;NC4U^Ur5N2^C3*p-~>Tri_a#jsZkB+JZDR!Le@Qp5&BZDe6$TtCs&-Het9ybLTo
zc=1Dt-w>YL5*C%CIuQE<BXRF<2u?1am!GI60jFz&4~9bZ&@Ua;V3Io{h&++0R_a|V
zO7LtZmGE0YcWazQ56|K;l1N%>)YLq}=YPr{n52Qru316~lG8{wQlMP?VhIX(bdW+g
z#QMIgaaTqhR#Dx#Uwog+=|N<;JV@hvH`*&3iQFs1$RC^oe8{02Io33)aVd<WD&gfR
z_QOuZfV!?de6))`o}rN>d&dcF9P7VL`wP)=|Ka$KX)54IzoUAfivz~?Pl0WY5lFyH
zpvrEC6r$JsE~}X0AzbvN(*sf_SaK@5UEa$Lw?jQHI;aSMxvT4`Flmf5g!lgLOtOIZ
z>=mg(@z)UfXYX$BR6ckU*;u(y#{xyvzn`r8D?;&^%D&)SA&5_|D&t8J2H$<U#_%Ch
zSXS|ze)xzM-idi933eAF`&hc=d?W<#TAdGW+YrE7$0+seTqmlFk4v8AREGD(soka+
z7&-e}L4k4Z9vY?@D3#)4hkFnE{R$^op{}JTOk7qFG<$oNp4F<r9K&|D?PqMroD|R|
zYC?eI+}s{Xe=@i|by@r`#yx-LAiZ+G?Fe*C?|ZpmoPH&T;nS8oM?ob06+e}b6ezFh
zl%CC11lp1QmuF;HVP`_>$b>ovMjY8PGavnfVozGo1gI;3c%seUox7%Bl*G~2@Tw1;
zyTKu&BcKbkpN;7)Gf1GC<P*L&)*I!>IvugZucJuDrk$3=Mda3|-}}jc47PHjh6|<z
z;hXHXsZ7%ha;p9{jaQjR#OORf5B&nd>;&_<^1dT_1^uYwi9gY`o!dnDjg)Y!k9|KX
zlpUsNhErp!8Gvl_sor4_9y6#qDkPkRgSgy6<H$ISbdnK3Yh`>2sRru1pBKdeS*#$7
z8mS`OJNb0`tn_;{5#plV;>!xmU-ip!K8Zpawb#cqC4{aRWv%HZlfWSV&Q^5h4&s|h
z8&g@KgdHaCzx7W<ApXqlCJKyHr`NY0d!bey%0rXl8~>8QSe=6MKy(+<d0N+YafkuJ
zQ-ZP&vHz#yl&BXHCj*sI_bHqI?INx=fjX7}1~_4aZ(TaJj5N38&iy^Nhe~ug8j@HJ
z5Y5!pIrjh_i0|{f*cYM(e?N+S*fr}$GGRfkY{Rk;({E<~@CF53CNl5fIIV*?ixnKr
zB?r-z2_ceReE><$;aXl{xJi~QSM{a~4FrDCRh~OChdvw8QwY5v0qTY&vzGn^G)^p+
zTD*d-r!&<%cU-0s_krHStp}T^oId9{Z5I<{`I{sLe49r32DO7*wex8ECF@*gST734
z`Ja|8oJ8&Q)%Q*(U`}{8)_itWMBs4k;Xa(^gKWd_HG_!9D0b6by;Xr0zPs1)JJ7O2
zLWmxvykii$HeFlR_>B%aqAsfVWnx3bJ)LFSLK>({K7IJ^t{{X|-RvBH&j!p6`m?GL
za=^58WKDT#3+>LB|J)w?g)Arx(hXXfVL$IqX1^;Vlw6s4Yqc&53y-WV)&kjKLW;ce
zjUyN2`^ub@p(BQ8V)EbCe~^Oq)68OyY#fv>T~QU9;RD>uPfJ3{>JYB~{Nf#(W;84)
zd)sPX4xR@IRh`sT1PP&%02Y5=RQyJyYbcN(a?-f9{lY2WZ#Get?qzOh=szYku!tFh
z-f~&zqhSL7xIC)GUS*)IB6(TV%?Iq)DW2ZK_KB@3LA&2r-C^jE9!!}a0KA6$#WWS_
z;OUr>^^u<mDsEb85Do>Py!DYerv)uEFxK;bcuWDa&z(ndYrB!SNdx!ItD;~sHEC#m
z)eu-btYP`tDB71PeLTjY_J82^kN<$%KmQBd{tu_S?|;GV`h1e+Yc*cLDZ}_FN&{=|
z9L)RYhXg=UqQUyrTUWUGEmb&C-Vrz{HLsPp>4TNa$1|fB86hLjw<(g-5@d49sMe=N
zq2ohjvszapk{ge_M#$8G{0J>&iu`LJO!UYnSl0zUYadf2WGcg{jepOsVaZT~PDz3^
z&Jj*VcwP}5y^2i?KTnV=>?5huOMFg8E`o-968|1`2weN1B1rXCA9kc}S>9oA`oG}%
z{}bH6e+7qex(EL^w7q-B#MHfcA>Qr9nk)A%k`s<qgCr8LO!Io)i4z8AJ+`vD80nyu
zi}sn@*+UeeOrCDyA^@kv;>E<pPQZAC?I%e;A(%|tp!KTSM4lu9>7he7c>Cb>y6I;I
z*k>)j6X0e5G%A**w7PppZDV2F=NKEv4=^q3{a}YP&o(Z8eJBopYI7VbnMmR76Tc3E
z#Q}0Uo@X}e!vk_(`2Wn)s(@^ge`doB5isubzR$UhgQb}lHmACoL2*T`=}R*!Xp-lt
z`1aGokO%d1O&(qt_-w}-=)whGU(;!C4HCnzxHO0AMtNv5Ki*)xjEAMeDOxQmJeW2$
z8L(pg>wktFL;pEtzy9l#VVv&a|2<`mtD0x$gw>&@DXNb5#XU4SU>W}J6hBxhSDJn~
z!v_Y$=iJ=S9RaEF7##D}qfo>7&5mbW5bBhEl1xxwJe#tS6K}CpRqZK}D4V{5DC_^m
zU*#7AopVw1zTMI=CLl&X6l??%)91zIwdBEL=PSMKkS6q>FZY?j@q)I#((PnQ4iF9w
z@a)VWhXfVBh)bPnK+LwA`(DZlN|Ij6EyVvuYAr*p$9@_8UuYx$32pShLc=)SWB(hP
zN9jM#e3onA>hENO9V5Ex<?CzjGmgEzoPoXAFks)=`(C-~>geNXf8Eu`*WJt0XX5`K
V!DC0{Wfc`<m3$_#&;Cxb{}0lNdg=fG

literal 0
HcmV?d00001

diff --git a/pmi.py b/pmi.py
new file mode 100644
index 0000000..b7cd658
--- /dev/null
+++ b/pmi.py
@@ -0,0 +1,111 @@
+import numpy as np
+from scipy.sparse import issparse
+
+import logging
+logging.basicConfig(level=logging.DEBUG)
+
+
+class PMICalculator(object):
+    """
+    Parameter:
+    -----------
+    doc2word_vectorizer: object that turns list of text into doc2word matrix
+        for example, sklearn.feature_extraction.test.CountVectorizer
+    """
+    def __init__(self, doc2word_vectorizer=None,
+                 doc2label_vectorizer=None):
+        self._d2w_vect = doc2word_vectorizer
+        self._d2l_vect = doc2label_vectorizer
+
+        self.index2word_ = None
+        self.index2label_ = None
+        
+    def from_matrices(self, d2w, d2l, pseudo_count=1):
+        """
+        Parameter:
+        ------------
+        d2w: numpy.ndarray or scipy.sparse.csr_matrix
+            document-word frequency matrix
+        
+        d2l: numpy.ndarray or scipy.sparse.csr_matrix
+            document-label frequency matrix
+            type should be the same with `d2w`
+
+        pseudo_count: float
+            smoothing parameter to avoid division by zero
+
+        Return:
+        ------------
+        numpy.ndarray: #word x #label
+            the pmi matrix
+        """        
+        denom1 = d2w.T.sum(axis=1)
+        denom2 = d2l.sum(axis=0)
+
+        # both are dense
+        if (not issparse(d2w)) and (not issparse(d2l)):
+            numer = np.matrix(d2w.T > 0) * np.matrix(d2l > 0)
+            denom1 = denom1[:, None]
+            denom2 = denom2[None, :]
+        # both are sparse
+        elif issparse(d2w) and issparse(d2l):
+            numer = ((d2w.T > 0) * (d2l > 0)).todense()
+        else:
+            raise TypeError('Type inconsistency: {} and {}.\n' +
+                            'They should be the same.'.format(
+                                type(d2w), type(d2l)))
+
+        # dtype conversion
+        numer = np.asarray(numer, dtype=np.float64)
+        denom1 = np.asarray(
+            denom1.repeat(repeats=d2l.shape[1], axis=1),
+            dtype=np.float64)
+        denom2 = np.asarray(
+            denom2.repeat(repeats=d2w.shape[1], axis=0),
+            dtype=np.float64)
+
+        # smoothing
+        numer += pseudo_count
+
+        return np.log(d2w.shape[0] * numer / denom1 / denom2)
+
+    def from_texts(self, docs, labels):
+        """
+        Parameter:
+        -----------
+        docs: list of list of string
+            the tokenized documents
+
+        labels: list of list of string
+        
+        Return:
+        -----------
+        numpy.ndarray: #word x #label
+            the pmi matrix
+        """
+        d2w = self._d2w_vect.fit_transform(map(lambda sent: ' '.join(sent),
+                                               docs))
+
+        # save it to avoid re-computation
+        self.d2w_ = d2w
+
+        d2l = self._d2l_vect.transform(docs, labels)
+
+        # remove the labels without any occurrences
+        indices = np.asarray(d2l.sum(axis=0).nonzero()[1]).flatten()
+        d2l = d2l[:, indices]
+
+        indices = set(indices)
+        labels = [l
+                  for i, l in self._d2l_vect.index2label_.items()
+                  if i in indices]
+
+        self.index2label_ = {i: l
+                             for i, l in enumerate(labels)}
+
+        if len(self.index2label_) == 0:
+            logging.warn("After label filtering, there is nothing left.")
+
+        self.index2word_ = {i: w
+                            for w, i in self._d2w_vect.vocabulary_.items()}
+        return self.from_matrices(d2w, d2l)
diff --git a/text.py b/text.py
new file mode 100644
index 0000000..a5a277f
--- /dev/null
+++ b/text.py
@@ -0,0 +1,74 @@
+from scipy.sparse import (csr_matrix, lil_matrix)
+from scipy import int64
+
+
+class LabelCountVectorizer(object):
+    """
+    Count the frequency of labels in each document
+    """
+    
+    def __init__(self):
+        self.index2label_ = None
+        
+    def _label_frequency(self, label_tokens, context_tokens):
+        """
+        Calculate the frequency that the label appears
+        in the context(e.g, sentence)
+        
+        Parameter:
+        ---------------
+
+        label_tokens: list|tuple of str
+            the label tokens
+        context_tokens: list|tuple of str
+            the sentence tokens
+
+        Return:
+        -----------
+        int: the label frequency in the sentence
+        """
+        label_len = len(label_tokens)
+        cnt = 0
+        for i in range(len(context_tokens) - label_len + 1):
+            match = True
+            for j in range(label_len):
+                if label_tokens[j] != context_tokens[i+j]:
+                    match = False
+                    break
+            if match:
+                cnt += 1
+        return cnt
+
+    def transform(self, docs, labels):
+        """
+        Calculate the doc2label frequency table
+
+        Note: docs are not tokenized and frequency is computed
+            based on substring matching
+        
+        Parameter:
+        ------------
+
+        docs: list of list of string
+            tokenized documents
+
+        labels: list of list of string
+
+        Return:
+        -----------
+        scipy.sparse.csr_matrix: #doc x #label
+            the frequency table
+        """
+        labels = sorted(labels)
+        self.index2label_ = {index: label
+                             for index, label in enumerate(labels)}
+
+        ret = lil_matrix((len(docs), len(labels)),
+                         dtype=int64)
+        for i, d in enumerate(docs):
+            for j, l in enumerate(labels):
+                cnt = self._label_frequency(l, d)
+                if cnt > 0:
+                    ret[i, j] = cnt
+        return ret.tocsr()
+
diff --git a/train_academicdata.py b/train_academicdata.py
new file mode 100644
index 0000000..a50ae67
--- /dev/null
+++ b/train_academicdata.py
@@ -0,0 +1,284 @@
+import json
+import xml.etree.ElementTree as ET
+from whoosh.index import create_in, open_dir
+from whoosh.fields import *
+from whoosh.qparser import MultifieldParser, OrGroup, QueryParser
+from whoosh.scoring import BaseScorer
+from whoosh import scoring
+from collections import defaultdict
+
+class Myclass:
+    def __init__(self):
+        self.rel = 0
+        self.kpbm25 = 0.0
+        self.pabm25 = 0.0
+        self.tibm25 = 0.0
+        self.kptf = 0.0
+        self.patf = 0.0
+        self.titf = 0.0
+        self.kppl2 = 0.0
+        self.papl2 = 0.0
+        self.tipl2 = 0.0
+        self.kpdf = 0.0
+        self.padf = 0.0
+        self.tidf = 0.0
+        self.references = 0
+        self.citatedBy = 0
+        self.citations = 0
+
+
+count = 0
+
+rf = open("./supervisedTrain.txt", "w")
+f = open("./train_queries.json", encoding = "utf-8")
+relevance = open("./train_queries_qrel", encoding = "utf-8")
+train_queries = defaultdict(dict)
+for line in relevance:
+    words = line.split()
+    train_queries[words[0]][words[1]] = words[2]
+
+
+fileWrite = defaultdict(dict)
+queries = defaultdict(str)
+for line in f:
+        document = json.loads(line)
+        query = document["query"]
+        query = query.replace("#combine( ", '')
+        query = query.replace(')', '')
+        queries[document["qid"]] = query
+        indexDir = open_dir("index/")
+        og = OrGroup.factory(0.9)
+        #QueryParser("content", schema)
+        #queryParser = MultifieldParser(["keyPhrases", "paperAbstract", "title"], indexSearcher.schema, group=og)
+        indexSearcher = indexDir.searcher()
+        kpqueryParser = QueryParser("keyPhrases", indexSearcher.schema, group=og)
+        kpqueryObject = kpqueryParser.parse(query)
+        paqueryParser = QueryParser("paperAbstract", indexSearcher.schema, group=og)
+        paqueryObject = paqueryParser.parse(query)
+        tiqueryParser = QueryParser("title", indexSearcher.schema, group=og)
+        tiqueryObject = tiqueryParser.parse(query)
+
+        
+        kpresults = indexSearcher.search(kpqueryObject, limit = None)
+        paresults = indexSearcher.search(paqueryObject, limit = None)
+        tiresults = indexSearcher.search(tiqueryObject, limit = None)
+        for result in kpresults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].kpbm25 = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.kpbm25 = result.score
+                    fileWrite[str(count)][docNo] = temp
+        
+        for result in paresults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].pabm25 = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.pabm25 = result.score
+                    fileWrite[str(count)][docNo] = temp
+
+        for result in tiresults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].tibm25 = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.tibm25 = result.score
+                    fileWrite[str(count)][docNo] = temp
+        
+        
+        w = scoring.TF_IDF()
+        idfIndexSearcher = indexDir.searcher(weighting=w)
+        #idfResults = idfIndexSearcher.search(queryObject, limit = 8541)
+        kpidfResults = idfIndexSearcher.search(kpqueryObject, limit = None)
+        paidfResults = idfIndexSearcher.search(paqueryObject, limit = None)
+        tiidfResults = idfIndexSearcher.search(tiqueryObject, limit = None)
+        for result in kpidfResults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].kptf = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.kptf = result.score
+                    fileWrite[str(count)][docNo] = temp
+        
+        for result in paidfResults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].patf = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.patf = result.score
+                    fileWrite[str(count)][docNo] = temp
+
+        for result in tiidfResults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].titf = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.titf = result.score
+                    fileWrite[str(count)][docNo] = temp
+
+        w = scoring.PL2()
+        plIndexSearcher = indexDir.searcher(weighting=w)
+        #plResults = plIndexSearcher.search(queryObject, limit = 8541)
+        kpplResults = plIndexSearcher.search(kpqueryObject, limit = None)
+        paplResults = plIndexSearcher.search(paqueryObject, limit = None)
+        tiplResults = plIndexSearcher.search(tiqueryObject, limit = None)
+
+        for result in kpplResults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].kppl2 = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.kppl2 = result.score
+                    fileWrite[str(count)][docNo] = temp
+        
+        for result in paplResults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].papl2 = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.papl2 = result.score
+                    fileWrite[str(count)][docNo] = temp
+
+        for result in tiplResults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].tipl2 = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.tipl2 = result.score
+                    fileWrite[str(count)][docNo] = temp 
+
+        w = scoring.DFree()
+        dfIndexSearcher = indexDir.searcher(weighting=w)
+        #plResults = plIndexSearcher.search(queryObject, limit = 8541)
+        kpdfResults = dfIndexSearcher.search(kpqueryObject, limit = None)
+        padfResults = dfIndexSearcher.search(paqueryObject, limit = None)
+        tidfResults = dfIndexSearcher.search(tiqueryObject, limit = None)
+
+        for result in kpdfResults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].kpdf = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.kpdf = result.score
+                    fileWrite[str(count)][docNo] = temp
+        
+        for result in padfResults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].padf = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.padf = result.score
+                    fileWrite[str(count)][docNo] = temp
+
+        for result in tidfResults:
+            docNo = "".join(result["docno"])
+            rel = train_queries[str(count)].get(docNo, -1)
+
+            if rel != -1:
+                if (docNo in fileWrite[str(count)]):
+                    fileWrite[str(count)][docNo].tidf = result.score
+                else:
+                    temp = Myclass()
+                    temp.rel = rel
+                    temp.tidf = result.score
+                    fileWrite[str(count)][docNo] = temp
+
+        count += 1
+
+def getJsonValue(jsonValue, key):
+    try:
+        if key != "docno":
+            value = " ".join(jsonValue[key])
+        else:
+            value = jsonValue[key]
+        return value
+    except:
+        return " "
+
+docs = open("../data/Academic_data/docs.json", encoding="utf-8")
+docReferences = defaultdict()
+for line in docs:
+    document = json.loads(line)
+    kp = getJsonValue(document, "keyPhrases")
+    pa = getJsonValue(document, "paperAbstract")
+    ti = getJsonValue(document, "title")
+    docNo = document["docno"]
+    refs = document["numKeyReferences"][0]
+    citatedBy = document["numCitedBy"][0]
+    citations = document["numKeyCitations"][0]
+    docReferences[docNo] = [refs, citatedBy, citations, kp, pa, ti]
+
+for key, value in fileWrite.items():
+    for k, v in value.items():
+        kp = set(docReferences[k][3].split())
+        pa = set(docReferences[k][4].split())
+        ti = set(docReferences[k][5].split())
+        q = set(queries[key].split())
+        #rf.write(str(v[0]) + "\tqid:" + key + "\t1:" + str(v[1]) + "\t2:" + str(v[2]) + "\t3:" + str(v[3]) + "\t#docid = " + str(k) + "\n")
+        rf.write(str(v.rel) + "," + str(v.kpbm25) + "," + str(v.pabm25) + "," + str(v.tibm25) + "," + \
+            str(v.kptf) + "," + str(v.patf) + "," + str(v.titf)  + "," + \
+            str(v.kppl2) + "," + str(v.papl2) + "," + str(v.tipl2) + "," + \
+            str(v.kpdf) + "," + str(v.padf) + "," + str(v.tidf) + "," + \
+            str(docReferences[k][0]) + "," + str(docReferences[k][1]) + "," + str(docReferences[k][2]) + "," + \
+            str(len(kp)) + "," + str(len(pa)) + "," + str(len(ti)) + "," + \
+            str(len(q)) + "," + str(len(q.intersection(kp))) + "," + str(len(q.intersection(pa))) + "," + str(len(q.intersection(ti))) + "," + \
+            str(k) + "\n")
+f.close()
+print(count)
+
-- 
GitLab